Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/builder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { StrKey, Address, nativeToScVal } from '@stellar/stellar-sdk';
import { bigintSafeStringify, toStroops } from './utils.js';
import { bigintSafeStringify, toStroops, isValidAddress } from './utils.js';
import { boolToScVal } from './soroban.js';
import {
buildBatchTransactions,
Expand Down Expand Up @@ -426,7 +426,7 @@ export class StreamBuilder {
}
} else {
// sender / recipient — must be valid Stellar addresses (G-address or C-address)
if (!StrKey.isValidEd25519PublicKey(address) && !StrKey.isValidContract(address)) {
if (!isValidAddress(address)) {
throw new Error(
`Invalid StreamBuilder parameter: ${field} must be a valid Stellar public key or contract address (G-address or C-address), got "${address}"`,
);
Expand Down Expand Up @@ -565,15 +565,15 @@ function validatePayload(streams: unknown): string[] {
// Validate sender field — must be a valid Stellar public key (G-address) or contract ID (C-address)
if (obj.sender !== undefined && obj.sender !== null) {
const sender = String(obj.sender);
if (!StrKey.isValidEd25519PublicKey(sender) && !StrKey.isValidContract(sender)) {
if (!isValidAddress(sender)) {
errors.push(`Batch item at index ${i}: sender must be a valid Stellar public key or contract address (G-address or C-address), got "${sender}"`);
}
}

// Validate recipient field — must be a valid Stellar public key (G-address) or contract ID (C-address)
if (obj.recipient !== undefined && obj.recipient !== null) {
const recipient = String(obj.recipient);
if (!StrKey.isValidEd25519PublicKey(recipient) && !StrKey.isValidContract(recipient)) {
if (!isValidAddress(recipient)) {
errors.push(`Batch item at index ${i}: recipient must be a valid Stellar public key or contract address (G-address or C-address), got "${recipient}"`);
}
}
Expand Down
73 changes: 73 additions & 0 deletions src/tests/builder-contract-recipient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, it, expect } from 'vitest';
import { StreamBuilder, ConduitBatcher } from '../builder.js';
import { Address } from '@stellar/stellar-sdk';

describe('StreamBuilder Contract Recipient Validation (#609)', () => {
const contractRecipient = 'CABAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAFNSZ';
const accountSender = 'GAAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQDZ7H';
const contractToken = 'CAAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQC526';

it('accepts a valid C... contract recipient address', () => {
const builder = new StreamBuilder()
.token(contractToken)
.sender(accountSender)
.recipient(contractRecipient)
.amount(1000)
.ratePerSecond(5n);

const stream = builder.build();
expect(stream.recipient).toBe(contractRecipient);
});

it('correctly encodes C... contract recipient as an ScAddressTypeContract in toContractArgs()', () => {
const builder = new StreamBuilder()
.token(contractToken)
.sender(accountSender)
.recipient(contractRecipient)
.amount(500)
.ratePerSecond(10n);

const args = builder.toContractArgs();
expect(args).toHaveLength(8);

// Arg 1 is recipient ScVal
const recipientScVal = args[1] as any;
expect(recipientScVal.switch().name).toBe('scvAddress');
expect(recipientScVal.value().switch().name).toBe('scAddressTypeContract');
expect(new Address(contractRecipient).toScVal().toXDR('base64')).toEqual(
recipientScVal.toXDR('base64'),
);
});

it('rejects an invalid C... address with a malformed checksum or character', () => {
const malformedContract = 'CABAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBA9999';
expect(() => {
new StreamBuilder().recipient(malformedContract);
}).toThrowError(/must be a valid Stellar public key or contract address/);
});

it('rejects empty or whitespace-only recipient string', () => {
expect(() => {
new StreamBuilder().recipient('');
}).toThrowError(/must be a non-empty string/);

expect(() => {
new StreamBuilder().recipient(' ');
}).toThrowError(/must be a non-empty string/);
});

it('validates C... contract recipients in ConduitBatcher payload', () => {
const batcher = new ConduitBatcher();
const result = batcher.execute([
{
token: contractToken,
sender: accountSender,
recipient: 'CINVALIDADDRESS',
amount: '100',
},
]);

expect(result.success).toBe(false);
expect(result.errors?.[0]).toContain('recipient must be a valid Stellar public key or contract address');
});
});