diff --git a/sdk/README.md b/sdk/README.md index 213cfa8..2e9319f 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -41,12 +41,26 @@ const adminKeypair = Keypair.fromSecret('SXXX...SECRET'); const result = await client.mint( 'GABCDEF...RECIPIENT', BigInt(1000_0000000), // 1000 tokens with 7 decimals - adminKeypair + adminKeypair, ); console.log('Mint TX:', result.hash, 'Success:', result.success); ``` +## Batch Minting Tokens (Admin Only) + +```typescript +const adminKeypair = Keypair.fromSecret('SXXX...SECRET'); + +await client.batchMint( + [ + { to: 'GABCDEF...RECIPIENT1', amount: BigInt(1000_0000000) }, + { to: 'GHIJKL...RECIPIENT2', amount: BigInt(250_0000000) }, + ], + adminKeypair, +); +``` + ## Transferring Tokens ```typescript @@ -56,7 +70,7 @@ await client.transfer( senderKeypair.publicKey(), 'GABCDEF...RECIPIENT', BigInt(100_0000000), - senderKeypair + senderKeypair, ); ``` @@ -66,11 +80,7 @@ await client.transfer( const ownerKeypair = Keypair.fromSecret('SXXX...SECRET'); // Burn 50 tokens from owner's balance -const burnResult = await client.burn( - ownerKeypair.publicKey(), - BigInt(50_0000000), - ownerKeypair -); +const burnResult = await client.burn(ownerKeypair.publicKey(), BigInt(50_0000000), ownerKeypair); console.log('Burn TX:', burnResult.hash, 'Success:', burnResult.success); ``` @@ -82,24 +92,18 @@ await client.approve( ownerKeypair.publicKey(), 'GSPENDER...ADDR', BigInt(500_0000000), - ownerKeypair + ownerKeypair, ); // Check allowance -const allowance = await client.getAllowance( - ownerKeypair.publicKey(), - 'GSPENDER...ADDR' -); +const allowance = await client.getAllowance(ownerKeypair.publicKey(), 'GSPENDER...ADDR'); console.log('Allowance:', allowance); ``` ## Querying Allowance ```typescript -const allowance = await client.getAllowance( - 'GOWNER...ADDR', - 'GSPENDER...ADDR' -); +const allowance = await client.getAllowance('GOWNER...ADDR', 'GSPENDER...ADDR'); console.log('Allowance:', allowance); ``` @@ -125,28 +129,29 @@ await client.unpause(adminKeypair); ### Read-Only Methods -| Method | Returns | Description | -|--------|---------|-------------| -| `getBalance(address)` | `bigint` | Token balance for an address | -| `getTotalSupply()` | `bigint` | Total circulating supply | -| `getName()` | `string` | Token name | -| `getSymbol()` | `string` | Token symbol | -| `getDecimals()` | `number` | Decimal places | -| `getAllowance(owner, spender)` | `bigint` | Spending allowance | -| `getVersion()` | `string` | Contract version | +| Method | Returns | Description | +| ------------------------------ | -------- | ---------------------------- | +| `getBalance(address)` | `bigint` | Token balance for an address | +| `getTotalSupply()` | `bigint` | Total circulating supply | +| `getName()` | `string` | Token name | +| `getSymbol()` | `string` | Token symbol | +| `getDecimals()` | `number` | Decimal places | +| `getAllowance(owner, spender)` | `bigint` | Spending allowance | +| `getVersion()` | `string` | Contract version | ### Write Methods (require Keypair) -| Method | Description | -|--------|-------------| -| `initialize(admin, decimals, name, symbol, source)` | One-time contract setup | -| `mint(to, amount, source)` | Mint tokens (admin-only) | -| `transfer(from, to, amount, source)` | Transfer tokens | -| `approve(from, spender, amount, source)` | Set spending allowance | -| `burn(from, amount, source)` | Burn tokens | -| `transferOwnership(newAdmin, source)` | Transfer admin role | -| `pause(source)` | Pause contract (admin-only) | -| `unpause(source)` | Unpause contract (admin-only) | +| Method | Description | +| --------------------------------------------------- | --------------------------------------------------------------------------- | +| `initialize(admin, decimals, name, symbol, source)` | One-time contract setup | +| `mint(to, amount, source)` | Mint tokens (admin-only) | +| `batchMint(recipients, source)` | Mint tokens to multiple `{ to, amount }` recipients atomically (admin-only) | +| `transfer(from, to, amount, source)` | Transfer tokens | +| `approve(from, spender, amount, source)` | Set spending allowance | +| `burn(from, amount, source)` | Burn tokens | +| `transferOwnership(newAdmin, source)` | Transfer admin role | +| `pause(source)` | Pause contract (admin-only) | +| `unpause(source)` | Unpause contract (admin-only) | ## License diff --git a/sdk/src/client.test.ts b/sdk/src/client.test.ts index 4404da8..da6263f 100644 --- a/sdk/src/client.test.ts +++ b/sdk/src/client.test.ts @@ -3,12 +3,12 @@ */ import { bcForgeClient } from './client'; -import { Keypair, Networks } from '@stellar/stellar-sdk'; +import { Keypair, Networks, xdr } from '@stellar/stellar-sdk'; // Mock data for testing const MOCK_RPC_URL = 'https://soroban-testnet.stellar.org'; const MOCK_NETWORK = Networks.TESTNET; -const MOCK_CONTRACT_ID = 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABK4I'; +const MOCK_CONTRACT_ID = 'CAAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQC526'; describe('bcForgeClient Offline Transaction Builders', () => { let client: bcForgeClient; @@ -27,8 +27,6 @@ describe('bcForgeClient Offline Transaction Builders', () => { it('should build an unsigned mint transaction XDR', async () => { // This test would require mocking the RPC server // For now, we're testing the method signature and structure - const toAddress = Keypair.random().publicKey(); - const amount = BigInt(1000); // The actual call would fail without a real RPC server // In production, you would mock the server.getResponse @@ -37,12 +35,49 @@ describe('bcForgeClient Offline Transaction Builders', () => { }); }); + describe('batchMint', () => { + it('should invoke batch_mint with object recipients', async () => { + const recipientA = Keypair.random().publicKey(); + const recipientB = Keypair.random().publicKey(); + const invokeContract = jest.fn().mockResolvedValue({ + success: true, + hash: 'mock-hash', + returnValue: null, + }); + (client as unknown as { invokeContract: typeof invokeContract }).invokeContract = + invokeContract; + + await client.batchMint( + [ + { to: recipientA, amount: 100n }, + { to: recipientB, amount: 250n }, + ], + adminKeypair, + ); + + expect(invokeContract).toHaveBeenCalledTimes(1); + const [method, args, source] = invokeContract.mock.calls[0]; + expect(method).toBe('batch_mint'); + expect(args).toHaveLength(1); + expect(source).toBe(adminKeypair); + + const recipientsVec = args[0] as xdr.ScVal; + const recipients = recipientsVec.vec(); + if (recipients === null) { + throw new Error('Expected batch_mint argument to be an ScVal vec'); + } + expect(recipients).toHaveLength(2); + const firstRecipient = recipients[0].map(); + if (firstRecipient === null) { + throw new Error('Expected batch_mint recipients to be ScVal maps'); + } + expect(firstRecipient[0].key().sym().toString()).toBe('address'); + expect(firstRecipient[1].key().sym().toString()).toBe('amount'); + }); + }); + describe('buildTransferTx', () => { it('should build an unsigned transfer transaction XDR', async () => { - const fromAddress = Keypair.random().publicKey(); - const toAddress = Keypair.random().publicKey(); - const amount = BigInt(500); - expect(typeof client.buildTransferTx).toBe('function'); expect(client.buildTransferTx.length).toBe(4); // 4 parameters }); @@ -50,11 +85,6 @@ describe('bcForgeClient Offline Transaction Builders', () => { describe('buildApproveTx', () => { it('should build an unsigned approve transaction XDR', async () => { - const fromAddress = Keypair.random().publicKey(); - const spenderAddress = Keypair.random().publicKey(); - const amount = BigInt(1000); - const exp = 1000000; - expect(typeof client.buildApproveTx).toBe('function'); expect(client.buildApproveTx.length).toBe(5); // 5 parameters }); @@ -62,9 +92,6 @@ describe('bcForgeClient Offline Transaction Builders', () => { describe('buildBurnTx', () => { it('should build an unsigned burn transaction XDR', async () => { - const fromAddress = Keypair.random().publicKey(); - const amount = BigInt(200); - expect(typeof client.buildBurnTx).toBe('function'); expect(client.buildBurnTx.length).toBe(3); // 3 parameters }); @@ -74,8 +101,7 @@ describe('bcForgeClient Offline Transaction Builders', () => { it('should sign a transaction XDR', () => { // Create a mock unsigned transaction XDR (simplified for testing) // In production, this would be a real XDR from buildMintTx, etc. - const mockXdr = 'AAAAAgAAAAB7NXRFP5sGdM0P6T0qMvqN0k3jTmGmZ3K7hE6m8Y1V5gAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='; - + expect(typeof client.signTx).toBe('function'); expect(client.signTx.length).toBe(2); // 2 parameters }); diff --git a/sdk/src/client.ts b/sdk/src/client.ts index 401842c..5afb91c 100644 --- a/sdk/src/client.ts +++ b/sdk/src/client.ts @@ -13,8 +13,6 @@ import { xdr, nativeToScVal, } from '@stellar/stellar-sdk'; -import { SorobanRpc, Contract, TransactionBuilder, Keypair, xdr } from '@stellar/stellar-sdk'; - import { buildInvokeTransaction, @@ -52,6 +50,13 @@ export interface TransactionResult { returnValue?: any; } +export interface BatchMintRecipient { + /** Recipient Stellar public key (G... address) */ + to: string; + /** Number of tokens to mint */ + amount: bigint; +} + // ─── Client ────────────────────────────────────────────────────────────────── export class bcForgeClient { @@ -204,26 +209,23 @@ export class bcForgeClient { /** * Batch mint tokens to multiple recipients. Admin-only. * - * @param recipients - Array of [address, amount] tuples + * @param recipients - Array of recipient objects * @param source - Admin keypair */ - async batchMint( - recipients: [string, bigint][], - source: Keypair - ): Promise { - // Convert recipients to the format expected by the contract - const recipientScVals = recipients.map(([address, amount]) => { - return nativeToScVal( - { - address: new Address(address).toScVal(), - amount: nativeToScVal(amount, { type: 'i128' }), - }, - { type: 'map' } - ); - }); - - const recipientsVec = nativeToScVal(recipientScVals, { type: 'vec' }); - + async batchMint(recipients: BatchMintRecipient[], source: Keypair): Promise { + const recipientScVals = recipients.map(({ to, amount }) => + xdr.ScVal.scvMap([ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('address'), + val: addressToScVal(to), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('amount'), + val: i128ToScVal(amount), + }), + ]), + ); + const recipientsVec = xdr.ScVal.scvVec(recipientScVals); return this.invokeContract('batch_mint', [recipientsVec], source); } @@ -323,18 +325,14 @@ export class bcForgeClient { * @param sourcePublicKey - Admin's public key * @returns Unsigned transaction XDR string */ - async buildMintTx( - to: string, - amount: bigint, - sourcePublicKey: string - ): Promise { + async buildMintTx(to: string, amount: bigint, sourcePublicKey: string): Promise { return buildUnsignedTransaction( this.rpcUrl, this.networkPassphrase, this.contractId, 'mint', [addressToScVal(to), i128ToScVal(amount)], - sourcePublicKey + sourcePublicKey, ); } @@ -351,7 +349,7 @@ export class bcForgeClient { from: string, to: string, amount: bigint, - sourcePublicKey: string + sourcePublicKey: string, ): Promise { return buildUnsignedTransaction( this.rpcUrl, @@ -359,7 +357,7 @@ export class bcForgeClient { this.contractId, 'transfer', [addressToScVal(from), addressToScVal(to), i128ToScVal(amount)], - sourcePublicKey + sourcePublicKey, ); } @@ -378,7 +376,7 @@ export class bcForgeClient { spender: string, amount: bigint, exp: number, - sourcePublicKey: string + sourcePublicKey: string, ): Promise { return buildUnsignedTransaction( this.rpcUrl, @@ -386,7 +384,7 @@ export class bcForgeClient { this.contractId, 'approve', [addressToScVal(from), addressToScVal(spender), i128ToScVal(amount), u32ToScVal(exp)], - sourcePublicKey + sourcePublicKey, ); } @@ -398,18 +396,14 @@ export class bcForgeClient { * @param sourcePublicKey - Burner's public key * @returns Unsigned transaction XDR string */ - async buildBurnTx( - from: string, - amount: bigint, - sourcePublicKey: string - ): Promise { + async buildBurnTx(from: string, amount: bigint, sourcePublicKey: string): Promise { return buildUnsignedTransaction( this.rpcUrl, this.networkPassphrase, this.contractId, 'burn', [addressToScVal(from), i128ToScVal(amount)], - sourcePublicKey + sourcePublicKey, ); } @@ -432,18 +426,14 @@ export class bcForgeClient { * @param sourcePublicKey - Public key for simulation context * @returns Simulation result with return value and cost */ - async simulate( - method: string, - args: xdr.ScVal[], - sourcePublicKey: string - ): Promise { + async simulate(method: string, args: xdr.ScVal[], sourcePublicKey: string): Promise { return simulateTransaction( this.rpcUrl, this.networkPassphrase, this.contractId, method, args, - sourcePublicKey + sourcePublicKey, ); } @@ -455,11 +445,7 @@ export class bcForgeClient { * @param sourcePublicKey - Admin's public key * @returns Simulation result */ - async simulateMint( - to: string, - amount: bigint, - sourcePublicKey: string - ): Promise { + async simulateMint(to: string, amount: bigint, sourcePublicKey: string): Promise { return this.simulate('mint', [addressToScVal(to), i128ToScVal(amount)], sourcePublicKey); } @@ -476,9 +462,15 @@ export class bcForgeClient { from: string, to: string, amount: bigint, - sourcePublicKey: string + sourcePublicKey: string, ): Promise { - return this.simulate('transfer', [addressToScVal(from), addressToScVal(to), i128ToScVal(amount)], sourcePublicKey); + return this.simulate( + 'transfer', + [addressToScVal(from), addressToScVal(to), i128ToScVal(amount)], + sourcePublicKey, + ); + } + // ─── Multi-Sig / Admin Pool ────────────────────────────────────────────── /** @@ -488,10 +480,24 @@ export class bcForgeClient { * @param threshold - Quorum threshold * @param source - Current admin keypair */ - async setAdminPool(pool: string[], threshold: number, source: Keypair): Promise { - return this.invokeContract('set_admin_pool', [ - nativeToScVal(pool.map(addr => addressToScVal(addr)), { type: 'vec' }), - u32ToScVal(threshold), + async setAdminPool( + pool: string[], + threshold: number, + source: Keypair, + ): Promise { + return this.invokeContract( + 'set_admin_pool', + [ + nativeToScVal( + pool.map((addr) => addressToScVal(addr)), + { type: 'vec' }, + ), + u32ToScVal(threshold), + ], + source, + ); + } + /** * Upgrades the contract to a new WASM hash. Admin-only. * @@ -499,9 +505,7 @@ export class bcForgeClient { * @param source - Admin keypair */ async upgrade(newWasmHash: string | Buffer, source: Keypair): Promise { - return this.invokeContract('upgrade', [ - hashToScVal(newWasmHash), - ], source); + return this.invokeContract('upgrade', [hashToScVal(newWasmHash)], source); } /** @@ -516,36 +520,46 @@ export class bcForgeClient { admin: string, action: { Mint: [string, bigint] } | { Pause: [] } | { Unpause: [] }, description: string, - source: Keypair + source: Keypair, ): Promise { - const actionScVal = action.hasOwnProperty('Mint') - ? nativeToScVal({ Mint: [addressToScVal((action as any).Mint[0]), i128ToScVal((action as any).Mint[1])] }) - : nativeToScVal(action); + const actionScVal = + 'Mint' in action + ? nativeToScVal({ + Mint: [addressToScVal(action.Mint[0]), i128ToScVal(action.Mint[1])], + }) + : nativeToScVal(action); - return this.invokeContract('propose_action', [ - addressToScVal(admin), - actionScVal, - stringToScVal(description), - ], source); + return this.invokeContract( + 'propose_action', + [addressToScVal(admin), actionScVal, stringToScVal(description)], + source, + ); } /** * Approve a pending proposal. */ - async approveProposal(admin: string, proposalId: bigint, source: Keypair): Promise { - return this.invokeContract('approve_proposal', [ - addressToScVal(admin), - nativeToScVal(proposalId, { type: 'u64' }), - ], source); + async approveProposal( + admin: string, + proposalId: bigint, + source: Keypair, + ): Promise { + return this.invokeContract( + 'approve_proposal', + [addressToScVal(admin), nativeToScVal(proposalId, { type: 'u64' })], + source, + ); } /** * Execute a proposal once quorum is reached. */ async executeProposal(proposalId: bigint, source: Keypair): Promise { - return this.invokeContract('execute_proposal', [ - nativeToScVal(proposalId, { type: 'u64' }), - ], source); + return this.invokeContract( + 'execute_proposal', + [nativeToScVal(proposalId, { type: 'u64' })], + source, + ); } // ─── Clawback / Regulatory ─────────────────────────────────────────────── @@ -554,28 +568,33 @@ export class bcForgeClient { * Set the designated clawback administrator. */ async setClawbackAdmin(admin: string, source: Keypair): Promise { - return this.invokeContract('set_clawback_admin', [ - addressToScVal(admin), + return this.invokeContract('set_clawback_admin', [addressToScVal(admin)], source); + } + + /** * Update the token name. Admin-only. * * @param newName - The new token name * @param source - Admin keypair */ async updateName(newName: string, source: Keypair): Promise { - return this.invokeContract('update_name', [ - stringToScVal(newName), - ], source); + return this.invokeContract('update_name', [stringToScVal(newName)], source); } /** * Execute a clawback operation. */ - async clawback(from: string, to: string, amount: bigint, source: Keypair): Promise { - return this.invokeContract('clawback', [ - addressToScVal(from), - addressToScVal(to), - i128ToScVal(amount), - ], source); + async clawback( + from: string, + to: string, + amount: bigint, + source: Keypair, + ): Promise { + return this.invokeContract( + 'clawback', + [addressToScVal(from), addressToScVal(to), i128ToScVal(amount)], + source, + ); } // ─── Locking / Vesting ─────────────────────────────────────────────────── @@ -583,21 +602,24 @@ export class bcForgeClient { /** * Lock tokens for a user until a specific timestamp. */ - async lockTokens(user: string, amount: bigint, unlockTime: bigint, source: Keypair): Promise { - return this.invokeContract('lock_tokens', [ - addressToScVal(user), - i128ToScVal(amount), - nativeToScVal(unlockTime, { type: 'u64' }), - ], source); + async lockTokens( + user: string, + amount: bigint, + unlockTime: bigint, + source: Keypair, + ): Promise { + return this.invokeContract( + 'lock_tokens', + [addressToScVal(user), i128ToScVal(amount), nativeToScVal(unlockTime, { type: 'u64' })], + source, + ); } /** * Withdraw matured locked tokens. */ async withdrawLocked(user: string, source: Keypair): Promise { - return this.invokeContract('withdraw_locked', [ - addressToScVal(user), - ], source); + return this.invokeContract('withdraw_locked', [addressToScVal(user)], source); } // ─── Events ────────────────────────────────────────────────────────────── @@ -613,20 +635,18 @@ export class bcForgeClient { return response.events; } + /** * Update the token symbol. Admin-only. * * @param newSymbol - The new token symbol * @param source - Admin keypair */ async updateSymbol(newSymbol: string, source: Keypair): Promise { - return this.invokeContract('update_symbol', [ - stringToScVal(newSymbol), - ], source); + return this.invokeContract('update_symbol', [stringToScVal(newSymbol)], source); } // ─── Internal Helpers ──────────────────────────────────────────────────── - /** * Internal helper to execute a task with retries. */ @@ -643,28 +663,6 @@ export class bcForgeClient { await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1))); } } - private async queryContract(method: string, args: xdr.ScVal[]): Promise { - const account = new (await import('@stellar/stellar-sdk')).Account( - 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF', - '0', - ); - - const tx = new TransactionBuilder(account, { - fee: '100', - networkPassphrase: this.networkPassphrase, - }) - .addOperation(this.contract.call(method, ...args)) - .setTimeout(30) - .build(); - - const simulated = await this.server.simulateTransaction(tx); - - if (SorobanRpc.Api.isSimulationError(simulated)) { - throw new Error(`Query failed: ${simulated.error}`); - } - - if (!SorobanRpc.Api.isSimulationSuccess(simulated) || !simulated.result) { - throw new Error('Query returned no result'); } throw lastError; } @@ -677,7 +675,7 @@ export class bcForgeClient { try { const account = new (await import('@stellar/stellar-sdk')).Account( 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF', - '0' + '0', ); const tx = new TransactionBuilder(account, { @@ -722,7 +720,7 @@ export class bcForgeClient { this.contractId, method, args, - source + source, ); const response = await submitTransaction(this.rpcUrl, txXdr); @@ -745,28 +743,5 @@ export class bcForgeClient { throw error; } }); - const txXdr = await buildInvokeTransaction( - this.rpcUrl, - this.networkPassphrase, - this.contractId, - method, - args, - source, - ); - - const response = await submitTransaction(this.rpcUrl, txXdr); - - if (response.status === SorobanRpc.Api.GetTransactionStatus.SUCCESS) { - return { - success: true, - hash: (response as any).hash, - returnValue: response.returnValue ? scValToNative(response.returnValue) : undefined, - }; - } - - return { - success: false, - hash: (response as any).hash, - }; } } diff --git a/sdk/src/events.ts b/sdk/src/events.ts index e10e308..7aa47a0 100644 --- a/sdk/src/events.ts +++ b/sdk/src/events.ts @@ -55,10 +55,10 @@ export function decodeEvent(event: SorobanRpc.Api.EventResponse): bcForgeEvent | return { type, ledger: event.ledger, - contractId: event.contractId, + contractId: event.contractId?.toString() ?? '', data: scValToNative(event.value), }; - } catch (e) { + } catch { return null; } } @@ -86,7 +86,7 @@ export function decodeDiagnosticEvent(rawEvent: xdr.DiagnosticEvent): bcForgeEve contractId: event.contractId()?.toString('hex') || '', data: scValToNative(body.data()), }; - } catch (e) { + } catch { return null; } } @@ -104,10 +104,10 @@ export async function subscribeEvents( rpcUrl: string, contractId: string, callback: (event: bcForgeEvent) => void, - options: SubscriptionOptions = {} + options: SubscriptionOptions = {}, ): Promise<() => void> { const server = new SorobanRpc.Server(rpcUrl); - + // Default to starting from the latest ledger if not specified let lastLedger = options.startLedger; if (!lastLedger) { @@ -140,7 +140,7 @@ export async function subscribeEvents( lastLedger = event.ledger + 1; } } - } catch (err) { + } catch { // Retry in the next poll cycle on failure } diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 52f2094..d88a55e 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -19,9 +19,8 @@ */ export { bcForgeClient } from './client'; -export type { bcForgeClientConfig, TransactionResult } from './client'; +export type { BatchMintRecipient, bcForgeClientConfig, TransactionResult } from './client'; export { buildInvokeTransaction, submitTransaction, scValToNative } from './utils'; export { bcForgeEventType, decodeEvent, decodeDiagnosticEvent, subscribeEvents } from './events'; export type { bcForgeEvent, SubscriptionOptions } from './events'; export * from './mockClient'; - diff --git a/sdk/src/mockClient.test.ts b/sdk/src/mockClient.test.ts index f1bd0bb..b722142 100644 --- a/sdk/src/mockClient.test.ts +++ b/sdk/src/mockClient.test.ts @@ -10,6 +10,19 @@ describe('MockBcForgeClient', () => { expect(await client.getBalance('B')).toBe(400n); }); + it('should batch mint tokens in-memory', async () => { + const client = new MockBcForgeClient({} as any); + const result = await client.batchMint([ + { to: 'A', amount: 100n }, + { to: 'B', amount: 250n }, + ]); + + expect(result.success).toBe(true); + expect(await client.getBalance('A')).toBe(100n); + expect(await client.getBalance('B')).toBe(250n); + expect(await client.getTotalSupply()).toBe(350n); + }); + it('should handle allowances and transferFrom', async () => { const client = new MockBcForgeClient({} as any); await client.mint('A', 1000n); diff --git a/sdk/src/mockClient.ts b/sdk/src/mockClient.ts index d80e3ac..cbb29c6 100644 --- a/sdk/src/mockClient.ts +++ b/sdk/src/mockClient.ts @@ -3,7 +3,7 @@ * * Allows frontend devs to test logic without a live Soroban RPC. */ -import { bcForgeClientConfig, TransactionResult } from './client'; +import type { BatchMintRecipient, bcForgeClientConfig, TransactionResult } from './client'; interface AccountState { balance: bigint; @@ -17,7 +17,7 @@ export class MockBcForgeClient { private symbol: string = 'MOCK'; private decimals: number = 7; - constructor(config: bcForgeClientConfig) {} + constructor(_config: bcForgeClientConfig) {} async getBalance(address: string): Promise { return this.accounts[address]?.balance ?? 0n; @@ -50,6 +50,22 @@ export class MockBcForgeClient { return { success: true, hash: 'mock-hash', returnValue: null }; } + async batchMint(recipients: BatchMintRecipient[]): Promise { + if (recipients.length === 0) { + return { success: false, hash: 'mock-hash', returnValue: 'Recipients list cannot be empty' }; + } + if (recipients.some(({ amount }) => amount <= 0n)) { + return { success: false, hash: 'mock-hash', returnValue: 'Mint amount must be positive' }; + } + + for (const { to, amount } of recipients) { + if (!this.accounts[to]) this.accounts[to] = { balance: 0n, allowances: {} }; + this.accounts[to].balance += amount; + this.totalSupply += amount; + } + return { success: true, hash: 'mock-hash', returnValue: null }; + } + async transfer(from: string, to: string, amount: bigint): Promise { if ((this.accounts[from]?.balance ?? 0n) < amount) { return { success: false, hash: 'mock-hash', returnValue: 'Insufficient balance' }; @@ -66,7 +82,12 @@ export class MockBcForgeClient { return { success: true, hash: 'mock-hash', returnValue: null }; } - async transferFrom(owner: string, spender: string, to: string, amount: bigint): Promise { + async transferFrom( + owner: string, + spender: string, + to: string, + amount: bigint, + ): Promise { const allowance = this.accounts[owner]?.allowances[spender] ?? 0n; if (allowance < amount) { return { success: false, hash: 'mock-hash', returnValue: 'Insufficient allowance' }; diff --git a/sdk/src/utils.ts b/sdk/src/utils.ts index 8e1df39..404686a 100644 --- a/sdk/src/utils.ts +++ b/sdk/src/utils.ts @@ -7,6 +7,7 @@ import { TransactionBuilder, Networks, xdr, + Account, Address, nativeToScVal, scValToNative as sdkScValToNative, @@ -159,7 +160,7 @@ export async function buildUnsignedTransaction( contractId: string, method: string, args: xdr.ScVal[], - sourcePublicKey: string + sourcePublicKey: string, ): Promise { const server = new SorobanRpc.Server(rpcUrl); const sourceAccount = await server.getAccount(sourcePublicKey); @@ -198,7 +199,7 @@ export async function buildUnsignedTransaction( export function signTransaction( txXdr: string, networkPassphrase: string, - keypair: Keypair + keypair: Keypair, ): string { const tx = TransactionBuilder.fromXDR(txXdr, networkPassphrase); tx.sign(keypair); @@ -222,13 +223,13 @@ export async function simulateTransaction( contractId: string, method: string, args: xdr.ScVal[], - sourcePublicKey: string + sourcePublicKey: string, ): Promise { const server = new SorobanRpc.Server(rpcUrl); - + // Create a dummy account for simulation const account = new Account(sourcePublicKey, '0'); - + const contract = new Contract(contractId); const tx = new TransactionBuilder(account, { @@ -246,6 +247,9 @@ export async function simulateTransaction( } return simulated; +} + +/** * Converts a 32-byte hex string or Buffer to an ScVal. */ export function hashToScVal(hash: string | Buffer): xdr.ScVal {