diff --git a/raydiumv4.go b/raydiumv4.go index 6931097..6040d48 100644 --- a/raydiumv4.go +++ b/raydiumv4.go @@ -402,11 +402,14 @@ func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions Inne func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { accountsLen := len(instruction.Accounts) - if accountsLen != 8 { + if accountsLen < 8 { return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 swapv2 instruction, offset %d, %d", offset[0], offset[1]) } var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + // Raydium's documented V2 layout uses the first 8 accounts. Routed CPI calls + // may append extra readonly accounts (for example the Raydium program id) at + // the tail, so we only require the canonical prefix here. ammAccount := tx.rawTx.accountList[instruction.Accounts[1]] user := tx.rawTx.accountList[instruction.Accounts[7]] userSourceTokenAccount := tx.rawTx.accountList[instruction.Accounts[5]] diff --git a/raydiumv4_test.go b/raydiumv4_test.go new file mode 100644 index 0000000..e8e24fc --- /dev/null +++ b/raydiumv4_test.go @@ -0,0 +1,136 @@ +package pump_parser + +import ( + "encoding/binary" + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/shopspring/decimal" +) + +func transferInstructionData(amount uint64) solana.Base58 { + data := make([]byte, 9) + data[0] = 3 + binary.LittleEndian.PutUint64(data[1:], amount) + return solana.Base58(data) +} + +func TestRaydiumV4SwapV2ParserAllowsTrailingReadonlyAccounts(t *testing.T) { + t.Parallel() + + accountList := make([]solana.PublicKey, 32) + for i := range accountList { + accountList[i] = testPublicKey(byte(i + 1)) + } + + accountList[0] = solana.TokenProgramID + accountList[8] = raydiumV4Program + accountList[20] = testPublicKey(200) + accountList[21] = testPublicKey(201) + accountList[22] = testPublicKey(202) + + outerInstruction := Instruction{ProgramIDIndex: 20} + swapInstruction := Instruction{ + Accounts: []int{0, 1, 2, 3, 4, 5, 6, 7, 8}, + ProgramIDIndex: 8, + Data: solana.Base58([]byte{raydiumV4SwapBaseInV2Discriminator}), + } + innerInstructions := InnerInstructions{ + Index: 0, + Instructions: []Instruction{ + swapInstruction, + { + Accounts: []int{5, 4, 7}, + ProgramIDIndex: 0, + Data: transferInstructionData(55), + }, + { + Accounts: []int{3, 6, 2}, + ProgramIDIndex: 0, + Data: transferInstructionData(42), + }, + }, + } + + rawTx := &RawTx{ + accountList: accountList, + Meta: Meta{ + PostTokenBalances: []TokenBalance{ + { + AccountIndex: 3, + MintAccount: accountList[21], + ProgramIDAccount: solana.TokenProgramID, + UITokenAmount: UITokenAmount{ + Amount: "1000", + Decimals: 6, + }, + }, + { + AccountIndex: 4, + MintAccount: accountList[22], + ProgramIDAccount: solana.TokenProgramID, + UITokenAmount: UITokenAmount{ + Amount: "2000", + Decimals: 9, + }, + }, + { + AccountIndex: 5, + MintAccount: accountList[22], + ProgramIDAccount: solana.TokenProgramID, + UITokenAmount: UITokenAmount{ + Amount: "300", + Decimals: 9, + }, + }, + { + AccountIndex: 6, + MintAccount: accountList[21], + ProgramIDAccount: solana.TokenProgramID, + UITokenAmount: UITokenAmount{ + Amount: "400", + Decimals: 6, + }, + }, + }, + }, + Transaction: Transaction{ + Message: Message{ + Instructions: []Instruction{outerInstruction}, + }, + }, + } + + tx := &Tx{rawTx: rawTx} + + swaps, nextOffset, err := raydiumv4SwapV2Parser(tx, swapInstruction, innerInstructions, [2]uint{0, 1}) + if err != nil { + t.Fatalf("raydiumv4SwapV2Parser() error = %v", err) + } + if len(swaps) != 1 { + t.Fatalf("raydiumv4SwapV2Parser() swaps len = %d, want 1", len(swaps)) + } + if nextOffset != [2]uint{0, 4} { + t.Fatalf("raydiumv4SwapV2Parser() nextOffset = %v, want [0 4]", nextOffset) + } + + swap := swaps[0] + if swap.Event != "buy" { + t.Fatalf("swap.Event = %q, want %q", swap.Event, "buy") + } + if !swap.Pool.Equals(accountList[1]) { + t.Fatalf("swap.Pool = %s, want %s", swap.Pool, accountList[1]) + } + if !swap.User.Equals(accountList[7]) { + t.Fatalf("swap.User = %s, want %s", swap.User, accountList[7]) + } + if !swap.EntryContract.Equals(accountList[20]) { + t.Fatalf("swap.EntryContract = %s, want %s", swap.EntryContract, accountList[20]) + } + if !swap.BaseAmount.Equal(decimal.NewFromInt(42)) { + t.Fatalf("swap.BaseAmount = %s, want 42", swap.BaseAmount) + } + if !swap.QuoteAmount.Equal(decimal.NewFromInt(55)) { + t.Fatalf("swap.QuoteAmount = %s, want 55", swap.QuoteAmount) + } +}