Skip to content
Draft
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
26 changes: 25 additions & 1 deletion modules/sdk-coin-sol/src/lib/tokenTransferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { InstructionBuilderTypes } from './constants';
import { AtaInit, TokenAssociateRecipient, TokenTransfer, SetPriorityFee } from './iface';
import assert from 'assert';
import { TransactionBuilder } from './transactionBuilder';
import _ from 'lodash';
import * as _ from 'lodash';

export interface SendParams {
address: string;
Expand Down Expand Up @@ -117,6 +117,30 @@ export class TokenTransferBuilder extends TransactionBuilder {
/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
assert(this._sender, 'Sender must be set before building the transaction');

// Validate transaction size limits
// Solana legacy transactions are limited to 1232 bytes
// Empirically determined safe limits: 9 recipients with ATA, 18 without ATA
const uniqueAtaCount = _.uniqBy(this._createAtaParams, (recipient: TokenAssociateRecipient) => {
return recipient.ownerAddress + recipient.tokenName;
}).length;

if (uniqueAtaCount > 0 && this._sendParams.length > 9) {
throw new BuildTransactionError(
`Transaction too large: ${this._sendParams.length} recipients with ${uniqueAtaCount} ATA creations. ` +
`Solana legacy transactions are limited to 1232 bytes (maximum 9 recipients with ATA creation). ` +
`Please split into multiple transactions with max 9 recipients each.`
);
}

if (uniqueAtaCount === 0 && this._sendParams.length > 18) {
throw new BuildTransactionError(
`Transaction too large: ${this._sendParams.length} recipients. ` +
`Solana legacy transactions are limited to 1232 bytes (maximum 18 recipients without ATA creation). ` +
`Please split into multiple transactions with max 18 recipients each.`
);
}

const sendInstructions = await Promise.all(
this._sendParams.map(async (sendParams: SendParams): Promise<TokenTransfer> => {
const coin = getSolTokenFromTokenName(sendParams.tokenName);
Expand Down
24 changes: 24 additions & 0 deletions modules/sdk-coin-sol/src/lib/transferBuilderV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,30 @@ export class TransferBuilderV2 extends TransactionBuilder {
/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
assert(this._sender, 'Sender must be set before building the transaction');

// Validate transaction size limits
// Solana legacy transactions are limited to 1232 bytes
// Empirically determined safe limits: 9 recipients with ATA, 18 without ATA
const uniqueAtaCount = _.uniqBy(this._createAtaParams, (recipient: TokenAssociateRecipient) => {
return recipient.ownerAddress + recipient.tokenName;
}).length;

if (uniqueAtaCount > 0 && this._sendParams.length > 9) {
throw new BuildTransactionError(
`Transaction too large: ${this._sendParams.length} recipients with ${uniqueAtaCount} ATA creations. ` +
`Solana legacy transactions are limited to 1232 bytes (maximum 9 recipients with ATA creation). ` +
`Please split into multiple transactions with max 9 recipients each.`
);
}

if (uniqueAtaCount === 0 && this._sendParams.length > 18) {
throw new BuildTransactionError(
`Transaction too large: ${this._sendParams.length} recipients. ` +
`Solana legacy transactions are limited to 1232 bytes (maximum 18 recipients without ATA creation). ` +
`Please split into multiple transactions with max 18 recipients each.`
);
}

const sendInstructions = await Promise.all(
this._sendParams.map(async (sendParams: SendParams): Promise<Transfer | TokenTransfer> => {
if (sendParams.tokenName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -796,5 +796,89 @@ describe('Sol Token Transfer Builder', () => {
})
).throwError('Invalid token name, got: ' + invalidTokenName);
});

it('should fail with more than 9 recipients with ATA creation', async () => {
const txBuilder = factory.getTokenTransferBuilder();
txBuilder.nonce(recentBlockHash);
txBuilder.sender(authAccount.pub);

// Generate 10 unique recipients for ATA creation
const recipients: string[] = [];
for (let i = 0; i < 10; i++) {
const keypair = new KeyPair();
recipients.push(keypair.getKeys().pub);
}

for (const address of recipients) {
txBuilder.send({ address, amount, tokenName: nameUSDC });
txBuilder.createAssociatedTokenAccount({
ownerAddress: address,
tokenName: nameUSDC,
});
}

await txBuilder
.build()
.should.be.rejectedWith(
/Transaction too large: 10 recipients with 10 ATA creations.*maximum 9 recipients with ATA creation/
);
});

it('should fail with more than 18 recipients without ATA creation', async () => {
const txBuilder = factory.getTokenTransferBuilder();
txBuilder.nonce(recentBlockHash);
txBuilder.sender(authAccount.pub);

// Add 19 recipients without ATA creation (reusing addresses is fine without ATA)
for (let i = 0; i < 19; i++) {
// Only use first 3 addresses to avoid invalid address at index 3
const address = testData.addresses.validAddresses[i % 3];
txBuilder.send({ address, amount, tokenName: nameUSDC });
}

await txBuilder
.build()
.should.be.rejectedWith(/Transaction too large: 19 recipients.*maximum 18 recipients without ATA creation/);
});

it('should succeed with 9 recipients with ATA creation', async () => {
const txBuilder = factory.getTokenTransferBuilder();
txBuilder.nonce(recentBlockHash);
txBuilder.sender(authAccount.pub);

// Generate exactly 9 unique recipients for ATA creation
const recipients: string[] = [];
for (let i = 0; i < 9; i++) {
const keypair = new KeyPair();
recipients.push(keypair.getKeys().pub);
}

for (const address of recipients) {
txBuilder.send({ address, amount, tokenName: nameUSDC });
txBuilder.createAssociatedTokenAccount({
ownerAddress: address,
tokenName: nameUSDC,
});
}

const tx = await txBuilder.build();
tx.should.be.ok();
});

it('should succeed with 18 recipients without ATA creation', async () => {
const txBuilder = factory.getTokenTransferBuilder();
txBuilder.nonce(recentBlockHash);
txBuilder.sender(authAccount.pub);

// Add exactly 18 recipients without ATA creation (reusing addresses is fine without ATA)
for (let i = 0; i < 18; i++) {
// Only use first 3 addresses to avoid invalid address at index 3
const address = testData.addresses.validAddresses[i % 3];
txBuilder.send({ address, amount, tokenName: nameUSDC });
}

const tx = await txBuilder.build();
tx.should.be.ok();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -872,5 +872,93 @@ describe('Sol Transfer Builder V2', () => {
`input amount ${excessiveAmount} exceeds max safe int 9007199254740991`
);
});

it('should fail with more than 9 recipients with ATA creation', async () => {
const txBuilder = factory.getTransferBuilderV2();
txBuilder.nonce(recentBlockHash);
txBuilder.sender(authAccount.pub);
txBuilder.feePayer(feePayerAccount.pub);

// Generate 10 unique recipients for ATA creation
const recipients: string[] = [];
for (let i = 0; i < 10; i++) {
const keypair = new KeyPair();
recipients.push(keypair.getKeys().pub);
}

for (const address of recipients) {
txBuilder.send({ address, amount, tokenName: nameUSDC });
txBuilder.createAssociatedTokenAccount({
ownerAddress: address,
tokenName: nameUSDC,
});
}

await txBuilder
.build()
.should.be.rejectedWith(
/Transaction too large: 10 recipients with 10 ATA creations.*maximum 9 recipients with ATA creation/
);
});

it('should fail with more than 18 token recipients without ATA creation', async () => {
const txBuilder = factory.getTransferBuilderV2();
txBuilder.nonce(recentBlockHash);
txBuilder.sender(authAccount.pub);
txBuilder.feePayer(feePayerAccount.pub);

// Add 19 token recipients without ATA creation (reusing addresses is fine without ATA)
for (let i = 0; i < 19; i++) {
// Only use first 3 addresses to avoid invalid address at index 3
const address = testData.addresses.validAddresses[i % 3];
txBuilder.send({ address, amount, tokenName: nameUSDC });
}

await txBuilder
.build()
.should.be.rejectedWith(/Transaction too large: 19 recipients.*maximum 18 recipients without ATA creation/);
});

it('should succeed with 9 recipients with ATA creation', async () => {
const txBuilder = factory.getTransferBuilderV2();
txBuilder.nonce(recentBlockHash);
txBuilder.sender(authAccount.pub);
txBuilder.feePayer(feePayerAccount.pub);

// Generate exactly 9 unique recipients for ATA creation
const recipients: string[] = [];
for (let i = 0; i < 9; i++) {
const keypair = new KeyPair();
recipients.push(keypair.getKeys().pub);
}

for (const address of recipients) {
txBuilder.send({ address, amount, tokenName: nameUSDC });
txBuilder.createAssociatedTokenAccount({
ownerAddress: address,
tokenName: nameUSDC,
});
}

const tx = await txBuilder.build();
tx.should.be.ok();
});

it('should succeed with 18 token recipients without ATA creation', async () => {
const txBuilder = factory.getTransferBuilderV2();
txBuilder.nonce(recentBlockHash);
txBuilder.sender(authAccount.pub);
txBuilder.feePayer(feePayerAccount.pub);

// Add exactly 18 token recipients without ATA creation (reusing addresses is fine without ATA)
for (let i = 0; i < 18; i++) {
// Only use first 3 addresses to avoid invalid address at index 3
const address = testData.addresses.validAddresses[i % 3];
txBuilder.send({ address, amount, tokenName: nameUSDC });
}

const tx = await txBuilder.build();
tx.should.be.ok();
});
});
});