Skip to content
Merged
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
42 changes: 42 additions & 0 deletions modules/sdk-coin-sol/src/config/token2022StaticConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Token2022Config } from '../lib/token2022Config';

export const TOKEN_2022_STATIC_CONFIGS: Token2022Config[] = [
{
mintAddress: '4MmJVdwYN8LwvbGeCowYjSx7KoEi6BJWg8XXnW4fDDp6',
symbol: 'tbill',
name: 'OpenEden T-Bills',
decimals: 6,
programId: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb',
transferHook: {
programId: '48n7YGEww7fKMfJ5gJ3sQC3rM6RWGjpUsghqVfXVkR5A',
authority: 'CPNEkz5SaAcWqGMezXTti39ekErzMpDCtuPMGw9tt4CZ',
extraAccountMetas: [
{
pubkey: '4zDeEh2D6K39H8Zzn99CpQkaUApbpUWfbCgqbwgZ2Yf',
isSigner: false,
isWritable: true,
},
],
extraAccountMetasPDA: '9sQhAH7vV3RKTCK13VY4EiNjs3qBq1srSYxdNufdAAXm',
},
},
{
mintAddress: '3BW95VLH2za2eUQ1PGfjxwMbpsnDFnmkA7m5LDgMKbX7',
symbol: 't1test',
name: 'T1TEST',
decimals: 6,
programId: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb',
transferHook: {
programId: '2Te6MFDwstRP2sZi6DLbkhVcSfaQVffmpbudN6pmvAXo',
authority: 'BLZvvaQgPUvL2RWoJeovudbHMhqH4S3kdenN5eg1juDr',
extraAccountMetas: [
{
pubkey: '4zDeEh2D6K39H8Zzn99CpQkaUApbpUWfbCgqbwgZ2Yf',
isSigner: false,
isWritable: true,
},
],
extraAccountMetasPDA: 'FR5YBEisx8mDe4ruhWKmpH5nirdJopj4uStBAVufqjMo',
},
},
];
50 changes: 49 additions & 1 deletion modules/sdk-coin-sol/src/lib/solInstructionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
createApproveInstruction,
} from '@solana/spl-token';
import {
AccountMeta,
Authorized,
Lockup,
PublicKey,
Expand Down Expand Up @@ -45,6 +46,7 @@ import {
} from './iface';
import { getSolTokenFromTokenName, isValidBase64, isValidHex } from './utils';
import { depositSolInstructions, withdrawStakeInstructions } from './jitoStakePoolOperations';
import { getToken2022Config, TransferHookConfig } from './token2022Config';

/**
* Construct Solana instructions from instructions params
Expand Down Expand Up @@ -193,7 +195,10 @@ function tokenTransferInstruction(data: TokenTransfer): TransactionInstruction[]
}

let transferInstruction: TransactionInstruction;
const instructions: TransactionInstruction[] = [];

if (programId === TOKEN_2022_PROGRAM_ID.toString()) {
// Create the base transfer instruction
transferInstruction = createTransferCheckedInstruction(
new PublicKey(sourceAddress),
new PublicKey(tokenAddress),
Expand All @@ -204,6 +209,11 @@ function tokenTransferInstruction(data: TokenTransfer): TransactionInstruction[]
[],
TOKEN_2022_PROGRAM_ID
);
// Check if this token has a transfer hook configuration
const tokenConfig = getToken2022Config(tokenAddress);
if (tokenConfig?.transferHook) {
addTransferHookAccounts(transferInstruction, tokenConfig.transferHook);
}
} else {
transferInstruction = createTransferCheckedInstruction(
new PublicKey(sourceAddress),
Expand All @@ -214,7 +224,8 @@ function tokenTransferInstruction(data: TokenTransfer): TransactionInstruction[]
decimalPlaces
);
}
return [transferInstruction];
instructions.push(transferInstruction);
return instructions;
}

/**
Expand Down Expand Up @@ -686,3 +697,40 @@ function customInstruction(data: InstructionParams): TransactionInstruction[] {

return [convertedInstruction];
}

function upsertAccountMeta(keys: AccountMeta[], meta: AccountMeta): void {
const existing = keys.find((account) => account.pubkey.equals(meta.pubkey));
if (existing) {
existing.isWritable = existing.isWritable || meta.isWritable;
existing.isSigner = existing.isSigner || meta.isSigner;
} else {
keys.push(meta);
}
}

function buildStaticTransferHookAccounts(transferHook: TransferHookConfig): AccountMeta[] {
const metas: AccountMeta[] = [];
if (transferHook.extraAccountMetas?.length) {
for (const meta of transferHook.extraAccountMetas) {
metas.push({
pubkey: new PublicKey(meta.pubkey),
isSigner: meta.isSigner,
isWritable: meta.isWritable,
});
}
}
metas.push({ pubkey: new PublicKey(transferHook.programId), isSigner: false, isWritable: false });

if (transferHook.extraAccountMetasPDA) {
metas.push({ pubkey: new PublicKey(transferHook.extraAccountMetasPDA), isSigner: false, isWritable: false });
}

return metas;
}

function addTransferHookAccounts(instruction: TransactionInstruction, transferHook: TransferHookConfig): void {
const extraMetas = buildStaticTransferHookAccounts(transferHook);
for (const meta of extraMetas) {
upsertAccountMeta(instruction.keys, meta);
}
}
85 changes: 85 additions & 0 deletions modules/sdk-coin-sol/src/lib/token2022Config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Token-2022 Configuration for Solana tokens with transfer hooks
* This file contains static configurations for Token-2022 tokens to avoid RPC calls
* when building transfer transactions with transfer hooks.
*/

import { TOKEN_2022_STATIC_CONFIGS } from '../config/token2022StaticConfig';

/**
* Interface for extra account metadata needed by transfer hooks
*/
export interface ExtraAccountMeta {
/** The public key of the account */
pubkey: string;
/** Whether the account is a signer */
isSigner: boolean;
/** Whether the account is writable */
isWritable: boolean;
/** Optional seed for PDA derivation */
seeds?: Array<{
/** Literal seed value or instruction account index reference */
value: string | number;
/** Type of seed: 'literal' for string/buffer, 'accountKey' for instruction account index */
type: 'literal' | 'accountKey';
}>;
}

/**
* Interface for transfer hook configuration
*/
export interface TransferHookConfig {
/** The transfer hook program ID */
programId: string;
/** The transfer hook authority */
authority: string;
/** Extra account metas required by the transfer hook */
extraAccountMetas: ExtraAccountMeta[];
/** The PDA address for extra account metas (cached) */
extraAccountMetasPDA?: string;
}

/**
* Interface for Token-2022 configuration
*/
export interface Token2022Config {
/** The mint address of the token */
mintAddress: string;
/** Token symbol */
symbol: string;
/** Token name */
name: string;
/** Number of decimal places */
decimals: number;
/** Program ID (TOKEN_2022_PROGRAM_ID) */
programId: string;
/** Transfer hook configuration if applicable */
transferHook?: TransferHookConfig;
/** Whether the token has transfer fees */
hasTransferFees?: boolean;
}

/**
* Token configurations map
* Key: mintAddress or symbol
*/
export const TOKEN_2022_CONFIGS: Record<string, Token2022Config> = {};

TOKEN_2022_STATIC_CONFIGS.forEach((config) => {
TOKEN_2022_CONFIGS[config.mintAddress] = config;
TOKEN_2022_CONFIGS[config.symbol] = config;
});

// Create symbol mappings for convenience
Object.values(TOKEN_2022_CONFIGS).forEach((config) => {
TOKEN_2022_CONFIGS[config.symbol] = config;
});

/**
* Get token configuration by mint address
* @param mintAddress - The mint address of the token
* @returns Token configuration or undefined if not found
*/
export function getToken2022Config(mintAddress: string): Token2022Config | undefined {
return TOKEN_2022_CONFIGS[mintAddress];
}
70 changes: 70 additions & 0 deletions modules/sdk-coin-sol/test/unit/solInstructionFactory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import should from 'should';
import * as testData from '../resources/sol';
import { solInstructionFactory } from '../../src/lib/solInstructionFactory';
import { getToken2022Config } from '../../src/lib/token2022Config';
import { InstructionBuilderTypes, MEMO_PROGRAM_PK } from '../../src/lib/constants';
import { InstructionParams } from '../../src/lib/iface';
import { PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js';
Expand Down Expand Up @@ -149,6 +150,75 @@ describe('Instruction Builder Tests: ', function () {
]);
});

it('Token Transfer - Token-2022 with transfer hook config', () => {
const tokenConfig = getToken2022Config('4MmJVdwYN8LwvbGeCowYjSx7KoEi6BJWg8XXnW4fDDp6');
should.exist(tokenConfig);
should.exist(tokenConfig?.transferHook);
const transferHook = tokenConfig!.transferHook!;

const fromAddress = testData.authAccount.pub;
const toAddress = testData.nonceAccount.pub;
const sourceAddress = testData.associatedTokenAccounts.accounts[0].ata;
const amount = '500000';

const transferParams: InstructionParams = {
type: InstructionBuilderTypes.TokenTransfer,
params: {
fromAddress,
toAddress,
amount,
tokenName: tokenConfig!.symbol,
sourceAddress,
tokenAddress: tokenConfig!.mintAddress,
decimalPlaces: tokenConfig!.decimals,
programId: tokenConfig!.programId,
},
};

const result = solInstructionFactory(transferParams);
result.should.have.length(1);

const builtInstruction = result[0];
builtInstruction.programId.equals(TOKEN_2022_PROGRAM_ID).should.be.true();

const baseInstruction = createTransferCheckedInstruction(
new PublicKey(sourceAddress),
new PublicKey(tokenConfig!.mintAddress),
new PublicKey(toAddress),
new PublicKey(fromAddress),
BigInt(amount),
tokenConfig!.decimals,
[],
TOKEN_2022_PROGRAM_ID
);

const baseKeyCount = baseInstruction.keys.length;
builtInstruction.keys.slice(0, baseKeyCount).should.deepEqual(baseInstruction.keys);

const extraKeys = builtInstruction.keys.slice(baseKeyCount);
const expectedExtraKeys = [
...transferHook.extraAccountMetas.map((meta) => ({
pubkey: new PublicKey(meta.pubkey),
isSigner: meta.isSigner,
isWritable: meta.isWritable,
})),
{ pubkey: new PublicKey(transferHook.programId), isSigner: false, isWritable: false },
];

if (transferHook.extraAccountMetasPDA) {
expectedExtraKeys.push({
pubkey: new PublicKey(transferHook.extraAccountMetasPDA),
isSigner: false,
isWritable: false,
});
}
extraKeys.should.deepEqual(expectedExtraKeys);

for (const expectedMeta of expectedExtraKeys) {
builtInstruction.keys.filter((meta) => meta.pubkey.equals(expectedMeta.pubkey)).should.have.length(1);
}
});

it('Mint To - Standard SPL Token', () => {
const mintAddress = testData.tokenTransfers.mintUSDC;
const destinationAddress = testData.tokenTransfers.sourceUSDC;
Expand Down
2 changes: 1 addition & 1 deletion modules/statics/src/coins/solTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3172,7 +3172,7 @@ export const solTokens = [
'50a59f79-033b-4bd0-aae1-49270f97cae2',
'tsol:t1test',
'T1TEST',
9,
6,
'3BW95VLH2za2eUQ1PGfjxwMbpsnDFnmkA7m5LDgMKbX7',
'3BW95VLH2za2eUQ1PGfjxwMbpsnDFnmkA7m5LDgMKbX7',
UnderlyingAsset['tsol:t1test'],
Expand Down