Skip to content

Commit

Permalink
feat(solana): more generic transaction parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinZemanik committed Jan 23, 2025
1 parent c0c793f commit aa3855e
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 14 deletions.
36 changes: 32 additions & 4 deletions packages/blockchain-link-utils/src/solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const SYSTEM_PROGRAM_PUBLIC_KEY = '11111111111111111111111111111111';
// when parsing tx effects.
export const WSOL_MINT = 'So11111111111111111111111111111111111111112';
export const STAKE_PROGRAM_PUBLIC_KEY = 'Stake11111111111111111111111111111111111111';
export const COMPUTE_BUDGET_PROGRAM_ID = 'ComputeBudget111111111111111111111111111111';

const tokenProgramNames = ['spl-token', 'spl-token-2022'] as const;
export type TokenProgramName = (typeof tokenProgramNames)[number];
Expand Down Expand Up @@ -376,7 +377,23 @@ export const getTxType = (
return 'failed';
}

// we consider only parsed instructions because only based on them we can determine the type of transaction
const isUnknownProgramInstruction = (
instruction: ParsedTransactionWithMeta['transaction']['message']['instructions'][number],
) =>
![
SYSTEM_PROGRAM_PUBLIC_KEY,
...Object.values(tokenProgramsInfo).map(info => info.publicKey),
ASSOCIATED_TOKEN_PROGRAM_PUBLIC_KEY,
STAKE_PROGRAM_PUBLIC_KEY,
COMPUTE_BUDGET_PROGRAM_ID,
].includes(instruction.programId);

// if there are any unknown program instructions, we interpret the transaction as `contract`
if (transaction.transaction.message.instructions.some(isUnknownProgramInstruction)) {
return 'contract';
}

// then, we consider only parsed instructions because only based on them we can determine the type of transaction
const parsedInstructions = transaction.transaction.message.instructions.filter(
(instruction): instruction is ParsedInstruction => 'parsed' in instruction,
);
Expand All @@ -397,7 +414,6 @@ export const getTxType = (
isInstructionCreatingTokenAccount(instruction),
);

// for now we support only transfers, so we interpret all other transactions as `unknown`
if (isTransfer) {
return tokenTransfers.length > 0
? getTokenTransferTxType(tokenTransfers)
Expand Down Expand Up @@ -559,8 +575,20 @@ export const getTokens = (
address === parsed.info.destination ||
address === parsed.info?.authority;

const effects = tx.transaction.message.instructions
.filter(isTokenTransferInstruction)
const instructions = [
...tx.transaction.message.instructions,
...(tx.meta?.innerInstructions?.flatMap(innerIx => innerIx.instructions) ?? []),
];

const effects = instructions
// filter token transfer instructions that are relevant to the user token accounts
.filter(
(instruction): instruction is TokenTransferInstruction =>
isTokenTransferInstruction(instruction) &&
tokenAccountsInfos.some(tokenAccountInfo =>
matchTokenAccountInfo(instruction, tokenAccountInfo.address),
),
)
.map<TokenTransfer>((ix): TokenTransfer => {
const { parsed, program } = ix;

Expand Down
50 changes: 42 additions & 8 deletions packages/blockchain-link-utils/src/tests/fixtures/solana.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { BigNumber } from '@trezor/utils/src/bigNumber';

import { TOKEN_PROGRAM_PUBLIC_KEY } from '../../solana';

const instructions = {
transfer: {
parsed: {
type: 'transfer',
},
program: 'spl-token',
programId: TOKEN_PROGRAM_PUBLIC_KEY,
},
nonTransfer: {
parsed: {
type: 'nonTransfer',
},
},
notParsed: {},
tokenTransferNotParsed: {
program: 'spl-token',
programId: TOKEN_PROGRAM_PUBLIC_KEY,
},
tokenTransfer: {
parsed: {
info: {
Expand All @@ -29,6 +36,7 @@ const instructions = {
type: 'transferChecked',
},
program: 'spl-token',
programId: TOKEN_PROGRAM_PUBLIC_KEY,
},
secondTokenTransfer: {
parsed: {
Expand All @@ -47,6 +55,7 @@ const instructions = {
type: 'transferChecked',
},
program: 'spl-token',
programId: TOKEN_PROGRAM_PUBLIC_KEY,
},
ownReceivedTokenTransfer: {
parsed: {
Expand All @@ -65,6 +74,7 @@ const instructions = {
type: 'transferChecked',
},
program: 'spl-token',
programId: TOKEN_PROGRAM_PUBLIC_KEY,
},
// without mint and tokenAmount
tokenTransferToAssociated: {
Expand All @@ -78,6 +88,7 @@ const instructions = {
type: 'transfer',
},
program: 'spl-token',
programId: TOKEN_PROGRAM_PUBLIC_KEY,
},
};

Expand Down Expand Up @@ -421,7 +432,7 @@ export const fixtures = {
expectedOutput: 'failed',
},
{
description: 'should return "unknown" if instructions are not transfer',
description: 'should return "contract" if instructions are not transfer',
input: {
transaction: {
transaction: {
Expand All @@ -434,15 +445,15 @@ export const fixtures = {
accountAddress: effects.negative.address,
tokenEffects: [],
},
expectedOutput: 'unknown',
expectedOutput: 'contract',
},
{
description: 'should return "unknown" if at least single instruction is not parsed',
description: 'should return "unknown" if all transfer instruction are not parsed',
input: {
transaction: {
transaction: {
message: {
instructions: [instructions.notParsed],
instructions: [instructions.tokenTransferNotParsed],
},
},
},
Expand Down Expand Up @@ -776,7 +787,13 @@ export const fixtures = {
transaction: parsedTransactions.multiTokenTransfer.transaction,
accountAddress: 'H8TGGw7Z85w1wDxcH3aTBAyCoCYsDQZrKUim7fwtKAMs',
map: sampleMintToDetailMap,
tokenAccountsInfos: [],
tokenAccountsInfos: [
{
address: 'H8TGGw7Z85w1wDxcH3aTBAyCoCYsDQZrKUim7fwtKAMs',
mint: 'DH1nKg3QZStnVh4bjm8kyWfsRJkiweXcnL4j7Ug3PfYA',
decimals: 1,
},
],
},
expectedOutput: [
{
Expand All @@ -798,7 +815,13 @@ export const fixtures = {
transaction: parsedTransactions.singleTokenTransfer.transaction,
accountAddress: 'ETxHeBBcuw9Yu4dGuP3oXrD12V5RECvmi8ogQ9PkjyVF',
map: sampleMintToDetailMap,
tokenAccountsInfos: [],
tokenAccountsInfos: [
{
address: 'ETxHeBBcuw9Yu4dGuP3oXrD12V5RECvmi8ogQ9PkjyVF',
mint: 'So11111111111111111111111111111111111111112',
decimals: 9,
},
],
},
expectedOutput: [
{
Expand All @@ -820,7 +843,18 @@ export const fixtures = {
transaction: parsedTransactions.multiTokenTransfer.transaction,
accountAddress: 'ETxHeBBcuw9Yu4dGuP3oXrD12V5RECvmi8ogQ9PkjyVF',
map: sampleMintToDetailMap,
tokenAccountsInfos: [],
tokenAccountsInfos: [
{
address: 'ETxHeBBcuw9Yu4dGuP3oXrD12V5RECvmi8ogQ9PkjyVF',
mint: 'So11111111111111111111111111111111111111112',
decimals: 9,
},
{
address: 'ETxHeBBcuw9Yu4dGuP3oXrD12V5RECvmi8ogQ9PkjyVF',
mint: 'DH1nKg3QZStnVh4bjm8kyWfsRJkiweXcnL4j7Ug3PfYA',
decimals: 1,
},
],
},
expectedOutput: [
{
Expand Down
3 changes: 1 addition & 2 deletions packages/blockchain-link/src/workers/solana/fee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ import {
TransactionMessageBytesBase64,
} from '@solana/web3.js';

import { COMPUTE_BUDGET_PROGRAM_ID } from '@trezor/blockchain-link-utils/src/solana';
import { BigNumber } from '@trezor/utils/src/bigNumber';

const COMPUTE_BUDGET_PROGRAM_ID =
'ComputeBudget111111111111111111111111111111' as Address<'ComputeBudget111111111111111111111111111111'>;
const DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = BigInt(300_000); // micro-lamports, value taken from other wallets

const stripComputeBudgetInstructions = (message: CompiledTransactionMessage) => ({
Expand Down

0 comments on commit aa3855e

Please sign in to comment.