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
3 changes: 2 additions & 1 deletion modules/sdk-coin-sol/src/lib/ataInitializationBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ export class AtaInitializationBuilder extends TransactionBuilder {
mintAddress: tokenAddress,
ataAddress: ataPk,
ownerAddress: recipient.ownerAddress,
payerAddress: this._sender,
// Match transactionBuilder fee payer selection when a distinct fee payer is set.
payerAddress: this._feePayer ?? this._sender,
tokenName: recipient.tokenName,
programId: programId,
},
Expand Down
4 changes: 2 additions & 2 deletions modules/sdk-coin-sol/src/lib/jitoStakePoolOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@solana/spl-stake-pool';
import {
createApproveInstruction,
createAssociatedTokenAccountInstruction,
createAssociatedTokenAccountIdempotentInstruction,
getAssociatedTokenAddressSync,
TOKEN_PROGRAM_ID,
} from '@solana/spl-token';
Expand Down Expand Up @@ -129,7 +129,7 @@ export function depositSolInstructions(
const instructions: TransactionInstruction[] = [];

if (createAssociatedTokenAccount) {
instructions.push(createAssociatedTokenAccountInstruction(from, associatedAddress, from, poolMint));
instructions.push(createAssociatedTokenAccountIdempotentInstruction(from, associatedAddress, from, poolMint));
}

instructions.push(
Expand Down
16 changes: 10 additions & 6 deletions modules/sdk-coin-sol/src/lib/solInstructionFactory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SolStakingTypeEnum } from '@bitgo/public-types';
import { SolCoin } from '@bitgo/statics';
import {
createAssociatedTokenAccountInstruction,
createAssociatedTokenAccountIdempotentInstruction,
createCloseAccountInstruction,
createMintToInstruction,
createBurnInstruction,
Expand Down Expand Up @@ -520,10 +520,14 @@ function stakingWithdrawInstruction(data: StakingWithdraw): TransactionInstructi
}

/**
* Construct Create and Initialize Nonce Solana instructions
* Construct an idempotent Create Associated Token Account instruction.
*
* @param {WalletInit} data - the data to build the instruction
* @returns {TransactionInstruction[]} An array containing Create and Initialize Nonce Solana instruction
* Uses createAssociatedTokenAccountIdempotentInstruction so the instruction is a
* no-op when the ATA already exists (discriminator byte = 1), instead of failing
* like the legacy createAssociatedTokenAccountInstruction (empty data).
*
* @param {AtaInit} data - the data to build the instruction
* @returns {TransactionInstruction[]} An array containing the Create ATA instruction
*/
function createATAInstruction(data: AtaInit): TransactionInstruction[] {
const {
Expand All @@ -536,15 +540,15 @@ function createATAInstruction(data: AtaInit): TransactionInstruction[] {

let associatedTokenAccountInstruction: TransactionInstruction;
if (programId && programId === TOKEN_2022_PROGRAM_ID.toString()) {
associatedTokenAccountInstruction = createAssociatedTokenAccountInstruction(
associatedTokenAccountInstruction = createAssociatedTokenAccountIdempotentInstruction(
new PublicKey(payerAddress),
new PublicKey(ataAddress),
new PublicKey(ownerAddress),
new PublicKey(mintAddress),
TOKEN_2022_PROGRAM_ID
);
} else {
associatedTokenAccountInstruction = createAssociatedTokenAccountInstruction(
associatedTokenAccountInstruction = createAssociatedTokenAccountIdempotentInstruction(
new PublicKey(payerAddress),
new PublicKey(ataAddress),
new PublicKey(ownerAddress),
Expand Down
4 changes: 3 additions & 1 deletion modules/sdk-coin-sol/src/lib/tokenTransferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,9 @@ export class TokenTransferBuilder extends TransactionBuilder {
ownerAddress: recipient.ownerAddress,
mintAddress: tokenAddress,
ataAddress,
payerAddress: this._sender,
// Match transactionBuilder fee payer selection: when a distinct fee payer
// is set (e.g. gas tank), it must fund ATA rent — not the token sender.
payerAddress: this._feePayer ?? this._sender,
tokenName: tokenName,
programId: programId,
},
Expand Down
5 changes: 4 additions & 1 deletion modules/sdk-coin-sol/src/lib/transferBuilderV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,10 @@ export class TransferBuilderV2 extends TransactionBuilder {
tokenName: tokenName,
mintAddress: tokenAddress,
ataAddress: recipientTokenAddress,
payerAddress: this._sender,
// Match transactionBuilder fee payer selection: when a distinct fee payer
// is set (e.g. gas tank on consolidation), it must fund ATA rent — the
// token sender/authority often has no SOL.
payerAddress: this._feePayer ?? this._sender,
programId: programId,
},
};
Expand Down
58 changes: 29 additions & 29 deletions modules/sdk-coin-sol/test/resources/sol.ts

Large diffs are not rendered by default.

48 changes: 39 additions & 9 deletions modules/sdk-coin-sol/test/unit/solInstructionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import { InstructionBuilderTypes, MEMO_PROGRAM_PK } from '../../src/lib/constant
import { InstructionParams } from '../../src/lib/iface';
import { PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js';
import {
createAssociatedTokenAccountInstruction,
createAssociatedTokenAccountIdempotentInstruction,
createTransferCheckedInstruction,
createMintToInstruction,
createBurnInstruction,
TOKEN_2022_PROGRAM_ID,
createTransferCheckedWithFeeInstruction,
} from '@solana/spl-token';
import BigNumber from 'bignumber.js';
import { isIdempotentAtaInstruction } from '../../src/lib/utils';

describe('Instruction Builder Tests: ', function () {
describe('Succeed ', function () {
Expand Down Expand Up @@ -109,14 +110,43 @@ describe('Instruction Builder Tests: ', function () {
};

const result = solInstructionFactory(createATAParams);
should.deepEqual(result, [
createAssociatedTokenAccountInstruction(
new PublicKey(payerAddress),
new PublicKey(ataAddress),
new PublicKey(ownerAddress),
new PublicKey(mintAddress)
),
]);
const expected = createAssociatedTokenAccountIdempotentInstruction(
new PublicKey(payerAddress),
new PublicKey(ataAddress),
new PublicKey(ownerAddress),
new PublicKey(mintAddress)
);
should.deepEqual(result, [expected]);
isIdempotentAtaInstruction(result[0]).should.equal(true);
});

it('Create associated token account for Token-2022', () => {
const mintAddress = testData.associatedTokenAccounts.mintId;
const ataAddress = testData.associatedTokenAccounts.accounts[0].ata;
const ownerAddress = testData.associatedTokenAccounts.accounts[0].pub;
const payerAddress = testData.associatedTokenAccounts.accounts[0].pub;
const createATAParams: InstructionParams = {
type: InstructionBuilderTypes.CreateAssociatedTokenAccount,
params: {
mintAddress,
ataAddress,
ownerAddress,
payerAddress,
tokenName: testData.associatedTokenAccounts.mint,
programId: TOKEN_2022_PROGRAM_ID.toString(),
},
};

const result = solInstructionFactory(createATAParams);
const expected = createAssociatedTokenAccountIdempotentInstruction(
new PublicKey(payerAddress),
new PublicKey(ataAddress),
new PublicKey(ownerAddress),
new PublicKey(mintAddress),
TOKEN_2022_PROGRAM_ID
);
should.deepEqual(result, [expected]);
isIdempotentAtaInstruction(result[0]).should.equal(true);
});

it('Token Transfer', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -797,4 +797,48 @@ describe('Sol Token Transfer Builder', () => {
).throwError('Invalid token name, got: ' + invalidTokenName);
});
});

describe('Create ATA rent payer selection', () => {
const feePayerAccount = new KeyPair(testData.feePayerAccount).getKeys();

before(async () => {
ataAddress = await Utils.getAssociatedTokenAccountAddress(mintUSDC, otherAccount.pub);
});

it('uses feePayer as Create-ATA rent payer when feePayer is set', async () => {
const txBuilder = factory.getTokenTransferBuilder();
txBuilder.nonce(recentBlockHash);
txBuilder.feePayer(feePayerAccount.pub);
txBuilder.sender(walletPK);
txBuilder.send({ address: otherAccount.pub, amount, tokenName: nameUSDC });
txBuilder.createAssociatedTokenAccount({
ownerAddress: otherAccount.pub,
tokenName: nameUSDC,
ataAddress,
});
txBuilder.setPriorityFee(priorityFee);
const tx = await txBuilder.build();
const createAta = tx.toJson().instructionsData.find((i) => i.type === 'CreateAssociatedTokenAccount');
should.exist(createAta);
createAta.params.payerAddress.should.equal(feePayerAccount.pub);
createAta.params.payerAddress.should.not.equal(walletPK);
});

it('falls back to sender as Create-ATA rent payer when feePayer is unset', async () => {
const txBuilder = factory.getTokenTransferBuilder();
txBuilder.nonce(recentBlockHash);
txBuilder.sender(walletPK);
txBuilder.send({ address: otherAccount.pub, amount, tokenName: nameUSDC });
txBuilder.createAssociatedTokenAccount({
ownerAddress: otherAccount.pub,
tokenName: nameUSDC,
ataAddress,
});
txBuilder.setPriorityFee(priorityFee);
const tx = await txBuilder.build();
const createAta = tx.toJson().instructionsData.find((i) => i.type === 'CreateAssociatedTokenAccount');
should.exist(createAta);
createAta.params.payerAddress.should.equal(walletPK);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ describe('Sol Transfer Builder V2', () => {
mintAddress: mintUSDC,
ataAddress: ataAddress,
ownerAddress: otherAccount.pub,
payerAddress: walletPK,
payerAddress: feePayerAccount.pub,
tokenName: nameUSDC,
programId: 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL',
});
Expand Down Expand Up @@ -530,7 +530,7 @@ describe('Sol Transfer Builder V2', () => {
mintAddress: mintAMS,
ataAddress: '8KLnroP6hHkr1ZsQL4k6A3i2yhhnv2kr2Teedx7a26Eg',
ownerAddress: otherAccount.pub,
payerAddress: walletPK,
payerAddress: feePayerAccount.pub,
tokenName: nameAMS,
programId: 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL',
});
Expand Down Expand Up @@ -873,4 +873,56 @@ describe('Sol Transfer Builder V2', () => {
);
});
});

describe('Create ATA rent payer selection', () => {
before(async () => {
ataAddress = await Utils.getAssociatedTokenAccountAddress(mintUSDC, otherAccount.pub);
});

it('uses feePayer as Create-ATA rent payer when feePayer is set', async () => {
const txBuilder = factory.getTransferBuilderV2();
txBuilder.nonce(recentBlockHash);
txBuilder.feePayer(feePayerAccount.pub);
txBuilder.sender(walletPK);
txBuilder.send({ address: otherAccount.pub, amount, tokenName: nameUSDC });
txBuilder.createAssociatedTokenAccount({ ownerAddress: otherAccount.pub, tokenName: nameUSDC });
txBuilder.setPriorityFee(priorityFee);
const tx = await txBuilder.build();
const createAta = tx.toJson().instructionsData.find((i) => i.type === 'CreateAssociatedTokenAccount');
should.exist(createAta);
createAta.params.payerAddress.should.equal(feePayerAccount.pub);
createAta.params.payerAddress.should.not.equal(walletPK);
});

it('falls back to sender as Create-ATA rent payer when feePayer is unset', async () => {
const txBuilder = factory.getTransferBuilderV2();
txBuilder.nonce(recentBlockHash);
txBuilder.sender(walletPK);
txBuilder.send({ address: otherAccount.pub, amount, tokenName: nameUSDC });
txBuilder.createAssociatedTokenAccount({ ownerAddress: otherAccount.pub, tokenName: nameUSDC });
txBuilder.setPriorityFee(priorityFee);
const tx = await txBuilder.build();
const createAta = tx.toJson().instructionsData.find((i) => i.type === 'CreateAssociatedTokenAccount');
should.exist(createAta);
createAta.params.payerAddress.should.equal(walletPK);
});

it('emits idempotent Create-ATA instruction data', async () => {
const txBuilder = factory.getTransferBuilderV2();
txBuilder.nonce(recentBlockHash);
txBuilder.feePayer(feePayerAccount.pub);
txBuilder.sender(walletPK);
txBuilder.send({ address: otherAccount.pub, amount, tokenName: nameUSDC });
txBuilder.createAssociatedTokenAccount({ ownerAddress: otherAccount.pub, tokenName: nameUSDC });
txBuilder.setPriorityFee(priorityFee);
const tx = await txBuilder.build();
// Re-parse via factory to get wire instructions and assert idempotent discriminator.
const rebuilt = await factory.from(tx.toBroadcastFormat()).build();
const raw = rebuilt.toBroadcastFormat();
should.equal(Utils.isValidRawTransaction(raw), true);
// Explain/instruction path already accepts idempotent ATA; ensure create is present.
const createAta = rebuilt.toJson().instructionsData.find((i) => i.type === 'CreateAssociatedTokenAccount');
should.exist(createAta);
});
});
});
Loading