Skip to content

Commit 8af5ae6

Browse files
committed
fix(sdk-coin-sol): add early validation for transaction size limits
TICKET: WIN-8401
1 parent f1c1244 commit 8af5ae6

File tree

4 files changed

+221
-1
lines changed

4 files changed

+221
-1
lines changed

modules/sdk-coin-sol/src/lib/tokenTransferBuilder.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { InstructionBuilderTypes } from './constants';
1313
import { AtaInit, TokenAssociateRecipient, TokenTransfer, SetPriorityFee } from './iface';
1414
import assert from 'assert';
1515
import { TransactionBuilder } from './transactionBuilder';
16-
import _ from 'lodash';
16+
import * as _ from 'lodash';
1717

1818
export interface SendParams {
1919
address: string;
@@ -117,6 +117,30 @@ export class TokenTransferBuilder extends TransactionBuilder {
117117
/** @inheritdoc */
118118
protected async buildImplementation(): Promise<Transaction> {
119119
assert(this._sender, 'Sender must be set before building the transaction');
120+
121+
// Validate transaction size limits
122+
// Solana legacy transactions are limited to 1232 bytes
123+
// Empirically determined safe limits: 9 recipients with ATA, 18 without ATA
124+
const uniqueAtaCount = _.uniqBy(this._createAtaParams, (recipient: TokenAssociateRecipient) => {
125+
return recipient.ownerAddress + recipient.tokenName;
126+
}).length;
127+
128+
if (uniqueAtaCount > 0 && this._sendParams.length > 9) {
129+
throw new BuildTransactionError(
130+
`Transaction too large: ${this._sendParams.length} recipients with ${uniqueAtaCount} ATA creations. ` +
131+
`Solana legacy transactions are limited to 1232 bytes (maximum 9 recipients with ATA creation). ` +
132+
`Please split into multiple transactions with max 9 recipients each.`
133+
);
134+
}
135+
136+
if (uniqueAtaCount === 0 && this._sendParams.length > 18) {
137+
throw new BuildTransactionError(
138+
`Transaction too large: ${this._sendParams.length} recipients. ` +
139+
`Solana legacy transactions are limited to 1232 bytes (maximum 18 recipients without ATA creation). ` +
140+
`Please split into multiple transactions with max 18 recipients each.`
141+
);
142+
}
143+
120144
const sendInstructions = await Promise.all(
121145
this._sendParams.map(async (sendParams: SendParams): Promise<TokenTransfer> => {
122146
const coin = getSolTokenFromTokenName(sendParams.tokenName);

modules/sdk-coin-sol/src/lib/transferBuilderV2.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,30 @@ export class TransferBuilderV2 extends TransactionBuilder {
130130
/** @inheritdoc */
131131
protected async buildImplementation(): Promise<Transaction> {
132132
assert(this._sender, 'Sender must be set before building the transaction');
133+
134+
// Validate transaction size limits
135+
// Solana legacy transactions are limited to 1232 bytes
136+
// Empirically determined safe limits: 9 recipients with ATA, 18 without ATA
137+
const uniqueAtaCount = _.uniqBy(this._createAtaParams, (recipient: TokenAssociateRecipient) => {
138+
return recipient.ownerAddress + recipient.tokenName;
139+
}).length;
140+
141+
if (uniqueAtaCount > 0 && this._sendParams.length > 9) {
142+
throw new BuildTransactionError(
143+
`Transaction too large: ${this._sendParams.length} recipients with ${uniqueAtaCount} ATA creations. ` +
144+
`Solana legacy transactions are limited to 1232 bytes (maximum 9 recipients with ATA creation). ` +
145+
`Please split into multiple transactions with max 9 recipients each.`
146+
);
147+
}
148+
149+
if (uniqueAtaCount === 0 && this._sendParams.length > 18) {
150+
throw new BuildTransactionError(
151+
`Transaction too large: ${this._sendParams.length} recipients. ` +
152+
`Solana legacy transactions are limited to 1232 bytes (maximum 18 recipients without ATA creation). ` +
153+
`Please split into multiple transactions with max 18 recipients each.`
154+
);
155+
}
156+
133157
const sendInstructions = await Promise.all(
134158
this._sendParams.map(async (sendParams: SendParams): Promise<Transfer | TokenTransfer> => {
135159
if (sendParams.tokenName) {

modules/sdk-coin-sol/test/unit/transactionBuilder/tokenTransferBuilder.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,5 +796,89 @@ describe('Sol Token Transfer Builder', () => {
796796
})
797797
).throwError('Invalid token name, got: ' + invalidTokenName);
798798
});
799+
800+
it('should fail with more than 9 recipients with ATA creation', async () => {
801+
const txBuilder = factory.getTokenTransferBuilder();
802+
txBuilder.nonce(recentBlockHash);
803+
txBuilder.sender(authAccount.pub);
804+
805+
// Generate 10 unique recipients for ATA creation
806+
const recipients: string[] = [];
807+
for (let i = 0; i < 10; i++) {
808+
const keypair = new KeyPair();
809+
recipients.push(keypair.getKeys().pub);
810+
}
811+
812+
for (const address of recipients) {
813+
txBuilder.send({ address, amount, tokenName: nameUSDC });
814+
txBuilder.createAssociatedTokenAccount({
815+
ownerAddress: address,
816+
tokenName: nameUSDC,
817+
});
818+
}
819+
820+
await txBuilder
821+
.build()
822+
.should.be.rejectedWith(
823+
/Transaction too large: 10 recipients with 10 ATA creations.*maximum 9 recipients with ATA creation/
824+
);
825+
});
826+
827+
it('should fail with more than 18 recipients without ATA creation', async () => {
828+
const txBuilder = factory.getTokenTransferBuilder();
829+
txBuilder.nonce(recentBlockHash);
830+
txBuilder.sender(authAccount.pub);
831+
832+
// Add 19 recipients without ATA creation (reusing addresses is fine without ATA)
833+
for (let i = 0; i < 19; i++) {
834+
// Only use first 3 addresses to avoid invalid address at index 3
835+
const address = testData.addresses.validAddresses[i % 3];
836+
txBuilder.send({ address, amount, tokenName: nameUSDC });
837+
}
838+
839+
await txBuilder
840+
.build()
841+
.should.be.rejectedWith(/Transaction too large: 19 recipients.*maximum 18 recipients without ATA creation/);
842+
});
843+
844+
it('should succeed with 9 recipients with ATA creation', async () => {
845+
const txBuilder = factory.getTokenTransferBuilder();
846+
txBuilder.nonce(recentBlockHash);
847+
txBuilder.sender(authAccount.pub);
848+
849+
// Generate exactly 9 unique recipients for ATA creation
850+
const recipients: string[] = [];
851+
for (let i = 0; i < 9; i++) {
852+
const keypair = new KeyPair();
853+
recipients.push(keypair.getKeys().pub);
854+
}
855+
856+
for (const address of recipients) {
857+
txBuilder.send({ address, amount, tokenName: nameUSDC });
858+
txBuilder.createAssociatedTokenAccount({
859+
ownerAddress: address,
860+
tokenName: nameUSDC,
861+
});
862+
}
863+
864+
const tx = await txBuilder.build();
865+
tx.should.be.ok();
866+
});
867+
868+
it('should succeed with 18 recipients without ATA creation', async () => {
869+
const txBuilder = factory.getTokenTransferBuilder();
870+
txBuilder.nonce(recentBlockHash);
871+
txBuilder.sender(authAccount.pub);
872+
873+
// Add exactly 18 recipients without ATA creation (reusing addresses is fine without ATA)
874+
for (let i = 0; i < 18; i++) {
875+
// Only use first 3 addresses to avoid invalid address at index 3
876+
const address = testData.addresses.validAddresses[i % 3];
877+
txBuilder.send({ address, amount, tokenName: nameUSDC });
878+
}
879+
880+
const tx = await txBuilder.build();
881+
tx.should.be.ok();
882+
});
799883
});
800884
});

modules/sdk-coin-sol/test/unit/transactionBuilder/transferBuilderV2.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,5 +872,93 @@ describe('Sol Transfer Builder V2', () => {
872872
`input amount ${excessiveAmount} exceeds max safe int 9007199254740991`
873873
);
874874
});
875+
876+
it('should fail with more than 9 recipients with ATA creation', async () => {
877+
const txBuilder = factory.getTransferBuilderV2();
878+
txBuilder.nonce(recentBlockHash);
879+
txBuilder.sender(authAccount.pub);
880+
txBuilder.feePayer(feePayerAccount.pub);
881+
882+
// Generate 10 unique recipients for ATA creation
883+
const recipients: string[] = [];
884+
for (let i = 0; i < 10; i++) {
885+
const keypair = new KeyPair();
886+
recipients.push(keypair.getKeys().pub);
887+
}
888+
889+
for (const address of recipients) {
890+
txBuilder.send({ address, amount, tokenName: nameUSDC });
891+
txBuilder.createAssociatedTokenAccount({
892+
ownerAddress: address,
893+
tokenName: nameUSDC,
894+
});
895+
}
896+
897+
await txBuilder
898+
.build()
899+
.should.be.rejectedWith(
900+
/Transaction too large: 10 recipients with 10 ATA creations.*maximum 9 recipients with ATA creation/
901+
);
902+
});
903+
904+
it('should fail with more than 18 token recipients without ATA creation', async () => {
905+
const txBuilder = factory.getTransferBuilderV2();
906+
txBuilder.nonce(recentBlockHash);
907+
txBuilder.sender(authAccount.pub);
908+
txBuilder.feePayer(feePayerAccount.pub);
909+
910+
// Add 19 token recipients without ATA creation (reusing addresses is fine without ATA)
911+
for (let i = 0; i < 19; i++) {
912+
// Only use first 3 addresses to avoid invalid address at index 3
913+
const address = testData.addresses.validAddresses[i % 3];
914+
txBuilder.send({ address, amount, tokenName: nameUSDC });
915+
}
916+
917+
await txBuilder
918+
.build()
919+
.should.be.rejectedWith(/Transaction too large: 19 recipients.*maximum 18 recipients without ATA creation/);
920+
});
921+
922+
it('should succeed with 9 recipients with ATA creation', async () => {
923+
const txBuilder = factory.getTransferBuilderV2();
924+
txBuilder.nonce(recentBlockHash);
925+
txBuilder.sender(authAccount.pub);
926+
txBuilder.feePayer(feePayerAccount.pub);
927+
928+
// Generate exactly 9 unique recipients for ATA creation
929+
const recipients: string[] = [];
930+
for (let i = 0; i < 9; i++) {
931+
const keypair = new KeyPair();
932+
recipients.push(keypair.getKeys().pub);
933+
}
934+
935+
for (const address of recipients) {
936+
txBuilder.send({ address, amount, tokenName: nameUSDC });
937+
txBuilder.createAssociatedTokenAccount({
938+
ownerAddress: address,
939+
tokenName: nameUSDC,
940+
});
941+
}
942+
943+
const tx = await txBuilder.build();
944+
tx.should.be.ok();
945+
});
946+
947+
it('should succeed with 18 token recipients without ATA creation', async () => {
948+
const txBuilder = factory.getTransferBuilderV2();
949+
txBuilder.nonce(recentBlockHash);
950+
txBuilder.sender(authAccount.pub);
951+
txBuilder.feePayer(feePayerAccount.pub);
952+
953+
// Add exactly 18 token recipients without ATA creation (reusing addresses is fine without ATA)
954+
for (let i = 0; i < 18; i++) {
955+
// Only use first 3 addresses to avoid invalid address at index 3
956+
const address = testData.addresses.validAddresses[i % 3];
957+
txBuilder.send({ address, amount, tokenName: nameUSDC });
958+
}
959+
960+
const tx = await txBuilder.build();
961+
tx.should.be.ok();
962+
});
875963
});
876964
});

0 commit comments

Comments
 (0)