diff --git a/src/utils/stellar.ts b/src/utils/stellar.ts index 8d4c4ec..0e3b213 100644 --- a/src/utils/stellar.ts +++ b/src/utils/stellar.ts @@ -1,9 +1,12 @@ import { + Account, Memo as StellarMemo, TransactionBuilder, Asset, + MuxedAccount, Networks, Operation, + StrKey, Transaction, } from '@stellar/stellar-sdk'; import { ValidationUtils } from './validation'; @@ -112,6 +115,18 @@ export const StellarUtils = { async buildPaymentXdr(params: PaymentParams): Promise { const { source, destination, amount, assetCode, issuer, memo, network } = params; + if (!isValidPaymentAccountAddress(source)) { + throw new Error('source must be a valid Stellar public or muxed public key'); + } + + if (!isValidPaymentAccountAddress(destination)) { + throw new Error('destination must be a valid Stellar public or muxed public key'); + } + + if (assetCode !== 'XLM' && !StrKey.isValidEd25519PublicKey(issuer ?? '')) { + throw new Error('issuer must be a valid Stellar public key for non-native assets'); + } + const networkPassphrase = network === 'public' ? Networks.PUBLIC @@ -123,11 +138,9 @@ export const StellarUtils = { // We use a dummy sequence number because the actual submission will be handled later // or by a signer that manages sequence numbers. - const sourceAccount = { - sequenceNumber: () => '0', - incrementSequenceNumber: () => {}, - accountId: () => source, - }; + const sourceAccount = StrKey.isValidMed25519PublicKey(source) + ? MuxedAccount.fromAddress(source, '0') + : new Account(source, '0'); const builder = new TransactionBuilder(sourceAccount, { fee: '100', @@ -176,3 +189,7 @@ export const StellarUtils = { return ValidationUtils.isValidStellarAddress(accountId); }, }; + +function isValidPaymentAccountAddress(address: string): boolean { + return StrKey.isValidEd25519PublicKey(address) || StrKey.isValidMed25519PublicKey(address); +} diff --git a/tests/utils/stellar.test.ts b/tests/utils/stellar.test.ts index f53ba87..c133501 100644 --- a/tests/utils/stellar.test.ts +++ b/tests/utils/stellar.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect } from 'vitest'; +import { Account, Keypair, MuxedAccount } from '@stellar/stellar-sdk'; import { StellarUtils } from '@/utils/stellar.ts'; interface ParsedPaymentOperation { @@ -92,6 +93,61 @@ describe('StellarUtils', () => { expect(parseFloat(operation.amount)).toBe(parseFloat(params.amount)); }); + it('should fail early for an invalid source public key', async () => { + await expect( + StellarUtils.buildPaymentXdr({ + source: invalidAccountId, + destination: validAccountId, + amount: '1', + assetCode: 'XLM', + network: 'testnet', + }), + ).rejects.toThrow('source must be a valid Stellar public or muxed public key'); + }); + + it('should fail early for an invalid destination public key', async () => { + await expect( + StellarUtils.buildPaymentXdr({ + source: validAccountId, + destination: invalidAccountId, + amount: '1', + assetCode: 'XLM', + network: 'testnet', + }), + ).rejects.toThrow('destination must be a valid Stellar public or muxed public key'); + }); + + it('should fail early for an invalid issuer on non-native assets', async () => { + await expect( + StellarUtils.buildPaymentXdr({ + source: validAccountId, + destination: validAccountId, + amount: '1', + assetCode: 'USDC', + issuer: invalidAccountId, + network: 'testnet', + }), + ).rejects.toThrow('issuer must be a valid Stellar public key for non-native assets'); + }); + + it('should accept muxed source and destination accounts', async () => { + const baseAccount = Keypair.random().publicKey(); + const source = new MuxedAccount(new Account(baseAccount, '0'), '123').accountId(); + const destination = new MuxedAccount(new Account(baseAccount, '0'), '456').accountId(); + + const xdr = await StellarUtils.buildPaymentXdr({ + source, + destination, + amount: '1', + assetCode: 'XLM', + network: 'testnet', + }); + + const parsed = StellarUtils.parseXdrTransaction(xdr); + expect(parsed.source).toBe(source); + expect(parsed.operations.length).toBe(1); + }); + it('should throw when parsing invalid XDR', () => { expect(() => StellarUtils.parseXdrTransaction('invalid-xdr')).toThrow(/Failed to parse XDR/); });