diff --git a/src/Angor/Avalonia/Angor.Sdk.Wasm/Angor.Sdk.Wasm.csproj b/src/Angor/Avalonia/Angor.Sdk.Wasm/Angor.Sdk.Wasm.csproj new file mode 100644 index 000000000..405e666d9 --- /dev/null +++ b/src/Angor/Avalonia/Angor.Sdk.Wasm/Angor.Sdk.Wasm.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + + + false + + + true + false + + + + + + + + + + + + diff --git a/src/Angor/Avalonia/Angor.Sdk.Wasm/AngorSdkInterop.cs b/src/Angor/Avalonia/Angor.Sdk.Wasm/AngorSdkInterop.cs new file mode 100644 index 000000000..6bc5c207f --- /dev/null +++ b/src/Angor/Avalonia/Angor.Sdk.Wasm/AngorSdkInterop.cs @@ -0,0 +1,186 @@ +using System.Text.Json; +using Microsoft.JSInterop; + +namespace Angor.Sdk.Wasm; + +/// +/// JavaScript interop bindings for Angor SDK. +/// Methods decorated with [JSInvokable] are callable from TypeScript/JavaScript. +/// +public class AngorSdkInterop +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false + }; + + /// + /// Initialize the SDK. Must be called before using other methods. + /// + [JSInvokable] + public static string Initialize(string network) + { + try + { + // TODO: Initialize SDK services with the specified network (mainnet/testnet) + return JsonSerializer.Serialize(new { success = true, message = "SDK initialized", network }, JsonOptions); + } + catch (Exception ex) + { + return JsonSerializer.Serialize(new { success = false, error = ex.Message }, JsonOptions); + } + } + + /// + /// Generate a new wallet with seed words. + /// + [JSInvokable] + public static string GenerateWallet(int wordCount = 12) + { + try + { + // TODO: Use Angor.Sdk wallet generation + // var hdOperations = new HdOperations(); + // var words = hdOperations.GenerateWords(wordCount); + + return JsonSerializer.Serialize(new + { + success = true, + // seedWords = words, + message = "Wallet generation not yet implemented", + wordCount + }, JsonOptions); + } + catch (Exception ex) + { + return JsonSerializer.Serialize(new { success = false, error = ex.Message }, JsonOptions); + } + } + + /// + /// Get project details by project ID. + /// + [JSInvokable] + public static async Task GetProject(string projectId) + { + try + { + // TODO: Use IProjectService to fetch project + await Task.CompletedTask; + + return JsonSerializer.Serialize(new + { + success = true, + message = "GetProject not yet implemented", + projectId + }, JsonOptions); + } + catch (Exception ex) + { + return JsonSerializer.Serialize(new { success = false, error = ex.Message }, JsonOptions); + } + } + + /// + /// Create an investment transaction draft. + /// Note: Using double for satoshi amounts for JavaScript compatibility. + /// JavaScript's Number can safely represent integers up to 2^53-1, which is ~90 million BTC in sats. + /// + [JSInvokable] + public static async Task CreateInvestment( + string walletId, + string projectId, + double amountSats, + double feeRateSatsPerVb) + { + try + { + // Convert from double to long for internal use + var amountSatsLong = (long)amountSats; + var feeRateLong = (long)feeRateSatsPerVb; + + // TODO: Use IInvestmentAppService to create investment + await Task.CompletedTask; + + return JsonSerializer.Serialize(new + { + success = true, + message = "CreateInvestment not yet implemented", + walletId, + projectId, + amountSats = amountSatsLong, + feeRateSatsPerVb = feeRateLong + }, JsonOptions); + } + catch (Exception ex) + { + return JsonSerializer.Serialize(new { success = false, error = ex.Message }, JsonOptions); + } + } + + /// + /// Sign a transaction with wallet credentials. + /// + [JSInvokable] + public static string SignTransaction(string transactionHex, string walletSeedWords) + { + try + { + // TODO: Use signing services + return JsonSerializer.Serialize(new + { + success = true, + message = "SignTransaction not yet implemented" + }, JsonOptions); + } + catch (Exception ex) + { + return JsonSerializer.Serialize(new { success = false, error = ex.Message }, JsonOptions); + } + } + + /// + /// Broadcast a signed transaction. + /// + [JSInvokable] + public static async Task BroadcastTransaction(string signedTransactionHex) + { + try + { + // TODO: Use ITransactionService to broadcast + await Task.CompletedTask; + + return JsonSerializer.Serialize(new + { + success = true, + message = "BroadcastTransaction not yet implemented" + }, JsonOptions); + } + catch (Exception ex) + { + return JsonSerializer.Serialize(new { success = false, error = ex.Message }, JsonOptions); + } + } + + /// + /// Derive project keys for a founder. + /// + [JSInvokable] + public static string DeriveProjectKeys(string walletSeedWords, string angorRootKey) + { + try + { + // TODO: Use IDerivationOperations + return JsonSerializer.Serialize(new + { + success = true, + message = "DeriveProjectKeys not yet implemented" + }, JsonOptions); + } + catch (Exception ex) + { + return JsonSerializer.Serialize(new { success = false, error = ex.Message }, JsonOptions); + } + } +} diff --git a/src/Angor/Avalonia/Angor.Sdk.Wasm/Program.cs b/src/Angor/Avalonia/Angor.Sdk.Wasm/Program.cs new file mode 100644 index 000000000..207fe73b8 --- /dev/null +++ b/src/Angor/Avalonia/Angor.Sdk.Wasm/Program.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Angor.Sdk.Wasm; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +// Configure services if needed +// builder.Services.AddSingleton(); + +Console.WriteLine("Angor SDK WASM initialized"); + +await builder.Build().RunAsync(); diff --git a/src/Angor/Avalonia/Angor.Sdk.Wasm/README.md b/src/Angor/Avalonia/Angor.Sdk.Wasm/README.md new file mode 100644 index 000000000..f1e0119b0 --- /dev/null +++ b/src/Angor/Avalonia/Angor.Sdk.Wasm/README.md @@ -0,0 +1,143 @@ +# Angor SDK WebAssembly + +This project compiles the Angor SDK to WebAssembly (WASM) for use in TypeScript/JavaScript applications. + +## Build + +```bash +cd Angor.Sdk.Wasm +dotnet build +``` + +The output will be in `bin/Debug/net9.0/wwwroot/_framework/`. + +## Production Build + +For a smaller, optimized bundle: + +```bash +dotnet publish -c Release +``` + +## Usage in TypeScript/JavaScript + +### Option 1: Script Tag + +```html + + + + + + + + + + + +``` + +### Option 2: ES Module (TypeScript) + +```typescript +import { loadAngorSdk, IAngorSdk, SdkResult } from './angor-sdk'; + +async function main(): Promise { + const sdk: IAngorSdk = await loadAngorSdk(); + + // Initialize the SDK + const result = await sdk.initialize('testnet'); + if (!result.success) { + console.error('Failed to initialize:', result.error); + return; + } + + // Create an investment + const investment = await sdk.createInvestment( + 'wallet-id', + 'project-id', + 100000, // 100,000 sats + 5 // 5 sat/vB fee rate + ); + + console.log('Investment draft:', investment); +} +``` + +## API Reference + +### `loadAngorSdk(): Promise` + +Loads and initializes the WASM module. Must be called before using any SDK methods. + +### `IAngorSdk.initialize(network: 'mainnet' | 'testnet'): Promise` + +Initialize the SDK with the specified Bitcoin network. + +### `IAngorSdk.generateWallet(wordCount?: 12 | 24): Promise>` + +Generate a new HD wallet with BIP39 seed words. + +### `IAngorSdk.getProject(projectId: string): Promise>` + +Fetch project details by Angor project ID. + +### `IAngorSdk.createInvestment(...): Promise>` + +Create an investment transaction draft for a project. + +### `IAngorSdk.signTransaction(...): Promise>` + +Sign a transaction with wallet seed words. + +### `IAngorSdk.broadcastTransaction(...): Promise>` + +Broadcast a signed transaction to the Bitcoin network. + +### `IAngorSdk.deriveProjectKeys(...): Promise>` + +Derive project-specific keys for founders. + +## Type Definitions + +TypeScript type definitions are available in `typescript/angor-sdk.d.ts`. + +## Bundle Size + +The uncompressed bundle is large due to including the .NET runtime. For production: + +1. Use gzip compression (`.gz` files are pre-generated) +2. Enable HTTP/2 for parallel loading +3. Consider using a CDN + +## Browser Compatibility + +Requires browsers with WebAssembly support: +- Chrome 57+ +- Firefox 52+ +- Safari 11+ +- Edge 16+ + +## Notes + +- The SDK runs entirely in the browser - no server required +- Private keys never leave the browser +- All Bitcoin transactions are constructed and signed client-side diff --git a/src/Angor/Avalonia/Angor.Sdk.Wasm/typescript/angor-sdk.d.ts b/src/Angor/Avalonia/Angor.Sdk.Wasm/typescript/angor-sdk.d.ts new file mode 100644 index 000000000..013838b38 --- /dev/null +++ b/src/Angor/Avalonia/Angor.Sdk.Wasm/typescript/angor-sdk.d.ts @@ -0,0 +1,155 @@ +/** + * TypeScript type definitions for Angor SDK WASM bindings. + * + * Usage: + * ```typescript + * import { AngorSdk } from './angor-sdk'; + * + * const sdk = await AngorSdk.create(); + * const result = await sdk.initialize('testnet'); + * ``` + */ + +export interface SdkResult { + success: boolean; + error?: string; + message?: string; + data?: T; +} + +export interface WalletInfo { + seedWords: string[]; + publicKey: string; + address: string; +} + +export interface ProjectInfo { + id: string; + name: string; + description: string; + targetAmount: number; + stages: StageInfo[]; + founderPubKey: string; + nostrPubKey: string; +} + +export interface StageInfo { + index: number; + percentage: number; + releaseDate?: string; +} + +export interface InvestmentDraft { + transactionHex: string; + totalAmount: number; + fee: number; + stageBreakdown: StageAmount[]; +} + +export interface StageAmount { + stageIndex: number; + amount: number; +} + +export interface SignedTransaction { + transactionHex: string; + txId: string; +} + +export interface BroadcastResult { + txId: string; + confirmed: boolean; +} + +/** + * Angor SDK interface for TypeScript consumers. + * All methods return promises that resolve to SdkResult objects. + */ +export interface IAngorSdk { + /** + * Initialize the SDK with network configuration. + * @param network - 'mainnet' or 'testnet' + */ + initialize(network: 'mainnet' | 'testnet'): Promise; + + /** + * Generate a new wallet with BIP39 seed words. + * @param wordCount - Number of seed words (12 or 24) + */ + generateWallet(wordCount?: 12 | 24): Promise>; + + /** + * Fetch project details by project ID. + * @param projectId - The Angor project identifier + */ + getProject(projectId: string): Promise>; + + /** + * Create an investment transaction draft. + * @param walletId - Wallet identifier + * @param projectId - Target project ID + * @param amountSats - Investment amount in satoshis + * @param feeRateSatsPerVb - Fee rate in sats/vByte + */ + createInvestment( + walletId: string, + projectId: string, + amountSats: number, + feeRateSatsPerVb: number + ): Promise>; + + /** + * Sign a transaction with wallet credentials. + * @param transactionHex - Unsigned transaction hex + * @param walletSeedWords - Space-separated seed words + */ + signTransaction( + transactionHex: string, + walletSeedWords: string + ): Promise>; + + /** + * Broadcast a signed transaction to the network. + * @param signedTransactionHex - Signed transaction hex + */ + broadcastTransaction( + signedTransactionHex: string + ): Promise>; + + /** + * Derive project keys for a founder. + * @param walletSeedWords - Founder's wallet seed words + * @param angorRootKey - Angor root key for derivation + */ + deriveProjectKeys( + walletSeedWords: string, + angorRootKey: string + ): Promise>; +} + +/** + * Load and initialize the Angor SDK WASM module. + * Uses Blazor WebAssembly and DotNet.invokeMethodAsync for interop. + * + * @example + * ```typescript + * // In your HTML, include the Blazor script first: + * // + * // + * + * const sdk = await loadAngorSdk(); + * const result = await sdk.initialize('testnet'); + * ``` + */ +export function loadAngorSdk(): Promise; + +// Global declaration for script tag usage +declare global { + interface Window { + loadAngorSdk: typeof loadAngorSdk; + angorSdkReady: Promise; + DotNet: { + invokeMethodAsync(assemblyName: string, methodName: string, ...args: unknown[]): Promise; + }; + } +} diff --git a/src/Angor/Avalonia/Angor.Sdk.Wasm/wwwroot/angor-sdk.js b/src/Angor/Avalonia/Angor.Sdk.Wasm/wwwroot/angor-sdk.js new file mode 100644 index 000000000..d97151c40 --- /dev/null +++ b/src/Angor/Avalonia/Angor.Sdk.Wasm/wwwroot/angor-sdk.js @@ -0,0 +1,130 @@ +/** + * Angor SDK - JavaScript wrapper for C# WASM bindings + * Usage: + * const sdk = await loadAngorSdk(); + * const result = await sdk.initialize('testnet'); + */ + +// Wait for Blazor to be ready +window.angorSdkReady = new Promise((resolve) => { + window.angorSdkReadyResolver = resolve; +}); + +// Called after Blazor WASM is initialized +Blazor.start().then(() => { + window.angorSdkReadyResolver(); +}); + +/** + * Load and initialize the Angor SDK + * @returns {Promise} The SDK interface + */ +async function loadAngorSdk() { + await window.angorSdkReady; + + return { + /** + * Initialize the SDK with network configuration + * @param {string} network - 'mainnet' or 'testnet' + * @returns {Promise} + */ + async initialize(network) { + const result = await DotNet.invokeMethodAsync('Angor.Sdk.Wasm', 'Initialize', network); + return JSON.parse(result); + }, + + /** + * Generate a new wallet with seed words + * @param {number} wordCount - Number of seed words (12 or 24) + * @returns {Promise>} + */ + async generateWallet(wordCount = 12) { + const result = await DotNet.invokeMethodAsync('Angor.Sdk.Wasm', 'GenerateWallet', wordCount); + return JSON.parse(result); + }, + + /** + * Get project details by ID + * @param {string} projectId - The project ID (nostr pubkey) + * @returns {Promise>} + */ + async getProject(projectId) { + const result = await DotNet.invokeMethodAsync('Angor.Sdk.Wasm', 'GetProject', projectId); + return JSON.parse(result); + }, + + /** + * Create an investment transaction + * @param {string} walletId - The investor's wallet ID + * @param {string} projectId - The target project ID + * @param {number} amountSats - Investment amount in satoshis + * @param {number} feeRateSatsPerVb - Fee rate in sats/vB + * @returns {Promise>} + */ + async createInvestment(walletId, projectId, amountSats, feeRateSatsPerVb) { + const result = await DotNet.invokeMethodAsync( + 'Angor.Sdk.Wasm', + 'CreateInvestment', + walletId, + projectId, + amountSats, + feeRateSatsPerVb + ); + return JSON.parse(result); + }, + + /** + * Sign a transaction with wallet credentials + * @param {string} transactionHex - Raw transaction hex + * @param {string} walletSeedWords - Space-separated seed words + * @returns {Promise>} + */ + async signTransaction(transactionHex, walletSeedWords) { + const result = await DotNet.invokeMethodAsync( + 'Angor.Sdk.Wasm', + 'SignTransaction', + transactionHex, + walletSeedWords + ); + return JSON.parse(result); + }, + + /** + * Broadcast a signed transaction + * @param {string} signedTransactionHex - Signed transaction hex + * @returns {Promise>} + */ + async broadcastTransaction(signedTransactionHex) { + const result = await DotNet.invokeMethodAsync( + 'Angor.Sdk.Wasm', + 'BroadcastTransaction', + signedTransactionHex + ); + return JSON.parse(result); + }, + + /** + * Derive project keys for a founder + * @param {string} walletSeedWords - Space-separated seed words + * @param {string} angorRootKey - Angor root key + * @returns {Promise>} + */ + async deriveProjectKeys(walletSeedWords, angorRootKey) { + const result = await DotNet.invokeMethodAsync( + 'Angor.Sdk.Wasm', + 'DeriveProjectKeys', + walletSeedWords, + angorRootKey + ); + return JSON.parse(result); + } + }; +} + +// Export for ES modules +if (typeof module !== 'undefined' && module.exports) { + module.exports = { loadAngorSdk }; +} + +// Export globally +window.loadAngorSdk = loadAngorSdk; diff --git a/src/Angor/Avalonia/Angor.Sdk.Wasm/wwwroot/index.html b/src/Angor/Avalonia/Angor.Sdk.Wasm/wwwroot/index.html new file mode 100644 index 000000000..db0435ee1 --- /dev/null +++ b/src/Angor/Avalonia/Angor.Sdk.Wasm/wwwroot/index.html @@ -0,0 +1,21 @@ + + + + + + Angor SDK + + + +
Loading Angor SDK...
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + diff --git a/src/Angor/Avalonia/Directory.Packages.props b/src/Angor/Avalonia/Directory.Packages.props index c9dc05695..72491ec9c 100644 --- a/src/Angor/Avalonia/Directory.Packages.props +++ b/src/Angor/Avalonia/Directory.Packages.props @@ -66,5 +66,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file