From 523203a57bcdc76cff97861e521bae8c0dc0cb39 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sun, 22 Mar 2026 22:47:14 +0900 Subject: [PATCH 1/3] add createAssociatedTokenAccountIdempotentInstruction for token transfer --- packages/swapper/src/orca/provider.test.ts | 20 +++--- packages/swapper/src/orca/provider.ts | 73 ++++++++++++---------- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/packages/swapper/src/orca/provider.test.ts b/packages/swapper/src/orca/provider.test.ts index a3fe9a2..56cef85 100644 --- a/packages/swapper/src/orca/provider.test.ts +++ b/packages/swapper/src/orca/provider.test.ts @@ -39,7 +39,7 @@ function createQuote(mint: string, referralBps = 100): Quote { ); } -describe("OrcaWhirlpoolProvider.buildReferralInstruction", () => { +describe("OrcaWhirlpoolProvider.buildReferralInstructions", () => { let provider: OrcaWhirlpoolProvider; const userKey = new PublicKey("9iqKg7nZFkC6xhnoWvyvCSdrgSX1uxPxL4X4fb97aotW"); @@ -49,13 +49,13 @@ describe("OrcaWhirlpoolProvider.buildReferralInstruction", () => { mockFetchAllMint.fetchAllMint?.mockReset?.(); }); - it("returns null when referral amount is zero", async () => { + it("returns empty array when referral amount is zero", async () => { const quote = createQuote(TEST_LEGACY_MINT, 0); // @ts-expect-error accessing private method for test purposes - const instruction = await provider.buildReferralInstruction(quote, userKey); + const instructions = await provider.buildReferralInstructions(quote, userKey); - expect(instruction).toBeNull(); + expect(instructions).toEqual([]); }); it("skips referral when mint uses token-2022 program", async () => { @@ -68,12 +68,12 @@ describe("OrcaWhirlpoolProvider.buildReferralInstruction", () => { const quote = createQuote(TEST_TOKEN2022_MINT); // @ts-expect-error accessing private method for test purposes - const instruction = await provider.buildReferralInstruction(quote, userKey); + const instructions = await provider.buildReferralInstructions(quote, userKey); - expect(instruction).toBeNull(); + expect(instructions).toEqual([]); }); - it("builds transfer instruction for legacy token program", async () => { + it("builds create-ata and transfer instructions for legacy token program", async () => { mockFetchAllMint.fetchAllMint.mockResolvedValue([ { exists: true, @@ -83,9 +83,11 @@ describe("OrcaWhirlpoolProvider.buildReferralInstruction", () => { const quote = createQuote(TEST_LEGACY_MINT); // @ts-expect-error accessing private method for test purposes - const instruction = await provider.buildReferralInstruction(quote, userKey); + const instructions = await provider.buildReferralInstructions(quote, userKey); - expect(instruction).toBeInstanceOf(TransactionInstruction); + expect(instructions).toHaveLength(2); + expect(instructions[0]).toBeInstanceOf(TransactionInstruction); + expect(instructions[1]).toBeInstanceOf(TransactionInstruction); }); }); diff --git a/packages/swapper/src/orca/provider.ts b/packages/swapper/src/orca/provider.ts index 8247eca..2f836cb 100644 --- a/packages/swapper/src/orca/provider.ts +++ b/packages/swapper/src/orca/provider.ts @@ -25,7 +25,12 @@ import { } from "@orca-so/whirlpools-core"; import { AccountRole, type AccountLookupMeta, type AccountMeta, type Instruction } from "@solana/instructions"; import { createSolanaRpc, address as toAddress, type Account, type Address, type TransactionSigner } from "@solana/kit"; -import { createTransferInstruction, getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { + createAssociatedTokenAccountIdempotentInstruction, + createTransferInstruction, + getAssociatedTokenAddressSync, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; import { Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction } from "@solana/web3.js"; import { BigIntMath } from "../bigint_math"; @@ -153,7 +158,7 @@ export class OrcaWhirlpoolProvider implements Protocol { const signer = this.createPassthroughSigner(userPublicKey); - const [{ instructions }, priorityFee, { blockhash, lastValidBlockHeight }, referralInstruction] = + const [{ instructions }, priorityFee, { blockhash, lastValidBlockHeight }, referralInstructions] = await Promise.all([ swapInstructions( this.rpc, @@ -164,18 +169,14 @@ export class OrcaWhirlpoolProvider implements Protocol { ), this.getPriorityFee(), getRecentBlockhash(this.connection, DEFAULT_COMMITMENT), - this.buildReferralInstruction(quote, userPublicKey), + this.buildReferralInstructions(quote, userPublicKey), ]); const legacyInstructions = instructions.map((instruction) => this.toLegacyInstruction(instruction)); const computeBudgetInstructions = addComputeBudgetInstructions([], DEFAULT_COMPUTE_UNIT_LIMIT, priorityFee); const transaction = new Transaction(); - transaction.add(...computeBudgetInstructions, ...legacyInstructions); - - if (referralInstruction) { - transaction.add(referralInstruction); - } + transaction.add(...computeBudgetInstructions, ...legacyInstructions, ...referralInstructions); transaction.feePayer = userPublicKey; setTransactionBlockhash(transaction, blockhash, lastValidBlockHeight); @@ -453,13 +454,13 @@ export class OrcaWhirlpoolProvider implements Protocol { }; } - private async buildReferralInstruction( + private async buildReferralInstructions( quote: Quote, userPublicKey: PublicKey, - ): Promise { + ): Promise { const referralAmount = calculateReferralFeeAmount(quote); if (referralAmount.isZero()) { - return null; + return []; } const referrer = getReferrerAddresses().solana; @@ -469,34 +470,42 @@ export class OrcaWhirlpoolProvider implements Protocol { const fromAsset = AssetId.fromString(quote.quote.from_asset.id); if (fromAsset.isNative()) { - return SystemProgram.transfer({ - fromPubkey: userPublicKey, - toPubkey: new PublicKey(referrer), - lamports: bnToNumberSafe(referralAmount), - }); + return [ + SystemProgram.transfer({ + fromPubkey: userPublicKey, + toPubkey: new PublicKey(referrer), + lamports: bnToNumberSafe(referralAmount), + }), + ]; } const tokenId = fromAsset.getTokenId(); const fromMintKey = parsePublicKey(tokenId); const programId = await this.getTokenProgram(fromMintKey); if (!programId.equals(TOKEN_PROGRAM_ID)) { - return null; + return []; } - const userTokenAccount = getAssociatedTokenAddressSync(fromMintKey, userPublicKey, false, programId); - const referrerTokenAccount = getAssociatedTokenAddressSync( - fromMintKey, - new PublicKey(referrer), - false, - programId, - ); - return createTransferInstruction( - userTokenAccount, - referrerTokenAccount, - userPublicKey, - bnToNumberSafe(referralAmount), - [], - programId, - ); + const referrerPublicKey = new PublicKey(referrer); + const userTokenAccount = getAssociatedTokenAddressSync(fromMintKey, userPublicKey, false, programId); + const referrerTokenAccount = getAssociatedTokenAddressSync(fromMintKey, referrerPublicKey, false, programId); + + return [ + createAssociatedTokenAccountIdempotentInstruction( + userPublicKey, + referrerTokenAccount, + referrerPublicKey, + fromMintKey, + programId, + ), + createTransferInstruction( + userTokenAccount, + referrerTokenAccount, + userPublicKey, + bnToNumberSafe(referralAmount), + [], + programId, + ), + ]; } } From 844d44c739abcddc008496ab79673670f0bb3d22 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:47:06 +0900 Subject: [PATCH 2/3] add token 2022 support --- packages/swapper/src/orca/fee.ts | 17 +------------ packages/swapper/src/orca/provider.test.ts | 29 ++++++++++------------ packages/swapper/src/orca/provider.ts | 15 ++++------- 3 files changed, 19 insertions(+), 42 deletions(-) diff --git a/packages/swapper/src/orca/fee.ts b/packages/swapper/src/orca/fee.ts index e27c845..75786e3 100644 --- a/packages/swapper/src/orca/fee.ts +++ b/packages/swapper/src/orca/fee.ts @@ -1,10 +1,7 @@ import { AssetId, Quote } from "@gemwallet/types"; -import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; import BN from "bn.js"; -import { parsePublicKey } from "../chain/solana/account"; - export const BASIS_POINTS_DENOMINATOR = 10_000; export const MAX_SAFE_NUMBER_BN = new BN(Number.MAX_SAFE_INTEGER.toString()); @@ -35,12 +32,7 @@ export function bnToNumberSafe(value: BN): number { return value.toNumber(); } -export async function applyReferralFee( - asset: AssetId, - amountIn: bigint, - referralBps: bigint, - resolveProgram: (mint: PublicKey) => Promise, -): Promise { +export async function applyReferralFee(asset: AssetId, amountIn: bigint, referralBps: bigint): Promise { if (referralBps <= BigInt(0)) { return amountIn; } @@ -50,13 +42,6 @@ export async function applyReferralFee( return amountIn - referralFee; } - const tokenId = asset.getTokenId(); - const mintKey = parsePublicKey(tokenId); - const programId = await resolveProgram(mintKey); - if (!programId.equals(TOKEN_PROGRAM_ID)) { - return amountIn; - } - const referralFee = (amountIn * referralBps) / BigInt(10_000); return amountIn - referralFee; } diff --git a/packages/swapper/src/orca/provider.test.ts b/packages/swapper/src/orca/provider.test.ts index 56cef85..b4f35d3 100644 --- a/packages/swapper/src/orca/provider.test.ts +++ b/packages/swapper/src/orca/provider.test.ts @@ -17,6 +17,7 @@ const TEST_RPC = "https://example.org"; const TEST_LEGACY_MINT = "So11111111111111111111111111111111111111112"; const TEST_TOKEN2022_MINT = "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo"; +const TOKEN2022_PROGRAM_ADDRESS = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"; function createQuote(mint: string, referralBps = 100): Quote { return buildOrcaQuoteFixture( @@ -58,29 +59,31 @@ describe("OrcaWhirlpoolProvider.buildReferralInstructions", () => { expect(instructions).toEqual([]); }); - it("skips referral when mint uses token-2022 program", async () => { + it("builds create-ata and transfer instructions for legacy token program", async () => { mockFetchAllMint.fetchAllMint.mockResolvedValue([ { exists: true, - programAddress: "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", + programAddress: TOKEN_PROGRAM_ID.toBase58(), }, ]); - const quote = createQuote(TEST_TOKEN2022_MINT); + const quote = createQuote(TEST_LEGACY_MINT); // @ts-expect-error accessing private method for test purposes const instructions = await provider.buildReferralInstructions(quote, userKey); - expect(instructions).toEqual([]); + expect(instructions).toHaveLength(2); + expect(instructions[0]).toBeInstanceOf(TransactionInstruction); + expect(instructions[1]).toBeInstanceOf(TransactionInstruction); }); - it("builds create-ata and transfer instructions for legacy token program", async () => { + it("builds create-ata and transfer instructions for token-2022 program", async () => { mockFetchAllMint.fetchAllMint.mockResolvedValue([ { exists: true, - programAddress: TOKEN_PROGRAM_ID.toBase58(), + programAddress: TOKEN2022_PROGRAM_ADDRESS, }, ]); - const quote = createQuote(TEST_LEGACY_MINT); + const quote = createQuote(TEST_TOKEN2022_MINT); // @ts-expect-error accessing private method for test purposes const instructions = await provider.buildReferralInstructions(quote, userKey); @@ -120,7 +123,6 @@ describe("OrcaWhirlpoolProvider.get_quote referral handling", () => { it("reduces swap amount for legacy SPL tokens when referral applies", async () => { jest.spyOn(provider as any, "findBestPool").mockResolvedValue({ account: { address: "PoolAddress" } }); - const mintProgramSpy = jest.spyOn(provider as any, "getTokenProgram").mockResolvedValueOnce(TOKEN_PROGRAM_ID); const quote = await provider.get_quote( createOrcaQuoteRequest({ @@ -136,14 +138,10 @@ describe("OrcaWhirlpoolProvider.get_quote referral handling", () => { expect(capturedAmount).toBe(BigInt(990000)); expect(quote.route_data).toMatchObject({ amount: "990000" }); - expect(mintProgramSpy).toHaveBeenCalledWith(new PublicKey(TEST_LEGACY_MINT)); }); - it("uses full input amount for Token-2022 tokens when referral cannot be collected", async () => { + it("reduces swap amount for Token-2022 tokens when referral applies", async () => { jest.spyOn(provider as any, "findBestPool").mockResolvedValue({ account: { address: "PoolAddress" } }); - const getProgramSpy = jest - .spyOn(provider as any, "getTokenProgram") - .mockResolvedValueOnce(new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")); const quote = await provider.get_quote( createOrcaQuoteRequest({ @@ -157,8 +155,7 @@ describe("OrcaWhirlpoolProvider.get_quote referral handling", () => { }), ); - expect(capturedAmount).toBe(BigInt(1000000)); - expect(quote.route_data).toMatchObject({ amount: "1000000" }); - expect(getProgramSpy).toHaveBeenCalledWith(new PublicKey(TEST_TOKEN2022_MINT)); + expect(capturedAmount).toBe(BigInt(990000)); + expect(quote.route_data).toMatchObject({ amount: "990000" }); }); }); diff --git a/packages/swapper/src/orca/provider.ts b/packages/swapper/src/orca/provider.ts index 2f836cb..dd2fe2f 100644 --- a/packages/swapper/src/orca/provider.ts +++ b/packages/swapper/src/orca/provider.ts @@ -27,9 +27,8 @@ import { AccountRole, type AccountLookupMeta, type AccountMeta, type Instruction import { createSolanaRpc, address as toAddress, type Account, type Address, type TransactionSigner } from "@solana/kit"; import { createAssociatedTokenAccountIdempotentInstruction, - createTransferInstruction, + createTransferCheckedInstruction, getAssociatedTokenAddressSync, - TOKEN_PROGRAM_ID, } from "@solana/spl-token"; import { Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction } from "@solana/web3.js"; @@ -115,9 +114,7 @@ export class OrcaWhirlpoolProvider implements Protocol { const amountIn = BigIntMath.parseString(quoteRequest.from_value); const referralBps = BigInt(quoteRequest.referral_bps ?? 0); - const swapAmount = await applyReferralFee(fromAsset, amountIn, referralBps, (mint) => - this.getTokenProgram(mint), - ); + const swapAmount = await applyReferralFee(fromAsset, amountIn, referralBps); if (swapAmount <= BigInt(0)) { throw new Error("Swap amount must be greater than zero"); @@ -482,10 +479,6 @@ export class OrcaWhirlpoolProvider implements Protocol { const tokenId = fromAsset.getTokenId(); const fromMintKey = parsePublicKey(tokenId); const programId = await this.getTokenProgram(fromMintKey); - if (!programId.equals(TOKEN_PROGRAM_ID)) { - return []; - } - const referrerPublicKey = new PublicKey(referrer); const userTokenAccount = getAssociatedTokenAddressSync(fromMintKey, userPublicKey, false, programId); const referrerTokenAccount = getAssociatedTokenAddressSync(fromMintKey, referrerPublicKey, false, programId); @@ -498,11 +491,13 @@ export class OrcaWhirlpoolProvider implements Protocol { fromMintKey, programId, ), - createTransferInstruction( + createTransferCheckedInstruction( userTokenAccount, + fromMintKey, referrerTokenAccount, userPublicKey, bnToNumberSafe(referralAmount), + quote.quote.from_asset.decimals, [], programId, ), From 782faaa65da8bd137847b6e7ce870865f28cf427 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:50:57 +0900 Subject: [PATCH 3/3] Update provider.test.ts --- packages/swapper/src/orca/provider.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/swapper/src/orca/provider.test.ts b/packages/swapper/src/orca/provider.test.ts index b4f35d3..b5e5e9f 100644 --- a/packages/swapper/src/orca/provider.test.ts +++ b/packages/swapper/src/orca/provider.test.ts @@ -1,5 +1,5 @@ import { Quote } from "@gemwallet/types"; -import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; import { TransactionInstruction, PublicKey } from "@solana/web3.js"; import { SOL_ASSET, buildOrcaQuoteFixture, createOrcaQuoteRequest } from "../testkit/mock"; @@ -72,8 +72,8 @@ describe("OrcaWhirlpoolProvider.buildReferralInstructions", () => { const instructions = await provider.buildReferralInstructions(quote, userKey); expect(instructions).toHaveLength(2); - expect(instructions[0]).toBeInstanceOf(TransactionInstruction); - expect(instructions[1]).toBeInstanceOf(TransactionInstruction); + expect(instructions[0].programId).toEqual(ASSOCIATED_TOKEN_PROGRAM_ID); + expect(instructions[1].programId).toEqual(TOKEN_PROGRAM_ID); }); it("builds create-ata and transfer instructions for token-2022 program", async () => { @@ -89,8 +89,8 @@ describe("OrcaWhirlpoolProvider.buildReferralInstructions", () => { const instructions = await provider.buildReferralInstructions(quote, userKey); expect(instructions).toHaveLength(2); - expect(instructions[0]).toBeInstanceOf(TransactionInstruction); - expect(instructions[1]).toBeInstanceOf(TransactionInstruction); + expect(instructions[0].programId).toEqual(ASSOCIATED_TOKEN_PROGRAM_ID); + expect(instructions[1].programId).toEqual(TOKEN_2022_PROGRAM_ID); }); });