Compare commits
8 Commits
v0.2.44
...
9f17ffce61
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f17ffce61 | ||
|
|
e4eaddec4e | ||
|
|
9454c3f6c7 | ||
|
|
39bfeb085f | ||
|
|
10885d5e08 | ||
|
|
2406f6d087 | ||
|
|
8b608889cb | ||
|
|
8d4aad1932 |
249
README.md
Normal file
249
README.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# pump-parser
|
||||
|
||||
Solana transaction parser focused on swap, liquidity, migration, platform, MEV, compute budget, and compact binary persistence workflows.
|
||||
|
||||
The package works with a normalized `RawTx` representation, parses it into `Tx`, and emits one or more `Swap` records when a supported protocol action is found.
|
||||
|
||||
## Features
|
||||
|
||||
- Parse Solana RPC / Yellowstone transactions into local `RawTx`.
|
||||
- Extract swap and liquidity events into `Tx.Swaps`.
|
||||
- Preserve transaction metadata such as slot, block time, signer, fee, CU limit, CU consumed, token balances, platform fees, and MEV agent hints.
|
||||
- Encode/decode parsed transactions with the `PTXB` / `PTXS` binary formats.
|
||||
- Encode/decode raw transactions with the `PRTX` / `PRTS` / `PRBS` binary formats.
|
||||
- Stream decode large `PTXS` / `PRTS` payloads without loading every transaction into memory.
|
||||
- Merge `PTXS` batches while remapping address tables.
|
||||
|
||||
## Supported Parsers
|
||||
|
||||
Default parser initialization enables the hot-path Pump parsers only:
|
||||
|
||||
- Pump
|
||||
- Pump AMM
|
||||
|
||||
`EnableAllParsers()` additionally enables:
|
||||
|
||||
- Meteora DLMM
|
||||
- Meteora Pools
|
||||
- Meteora DAMM v2
|
||||
- Meteora Bonding Curve
|
||||
- Orca Whirlpool
|
||||
- Raydium AMM v4
|
||||
- Raydium CLMM
|
||||
- Raydium CPMM
|
||||
- Raydium LaunchLab
|
||||
|
||||
Use `InitParser(WithMeteoraDlmm())` when only Meteora DLMM should be added to the default parser set.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get github.com/thloyi/pump-parser
|
||||
```
|
||||
|
||||
This module currently declares `go 1.25.1` in `go.mod`.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pump_parser "github.com/thloyi/pump-parser"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Enable all known parser programs. Omit this call to keep the default
|
||||
// Pump + Pump AMM parser set.
|
||||
pump_parser.EnableAllParsers()
|
||||
|
||||
var rawTx *pump_parser.RawTx
|
||||
// Fill rawTx from RPC, Yellowstone, JSON, or RawTx binary decoding.
|
||||
|
||||
tx, err := pump_parser.ParseRawTx(rawTx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("tx:", tx.GetTxHash())
|
||||
for _, swap := range tx.Swaps {
|
||||
fmt.Printf("%s %s base=%s quote=%s pool=%s user=%s\n",
|
||||
swap.Program,
|
||||
swap.Event,
|
||||
swap.BaseAmount,
|
||||
swap.QuoteAmount,
|
||||
swap.Pool,
|
||||
swap.User,
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Swap Result
|
||||
|
||||
`ParseRawTx` returns a `Tx`. A transaction can contain zero, one, or multiple parsed swap records in `Tx.Swaps`.
|
||||
|
||||
Each `Swap` describes one protocol-level swap, liquidity, migration, or pool operation that the parser can normalize. The most important fields are:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `Program` | Normalized protocol name, such as `Pump`, `PumpAMM`, `MeteoraDLMM`, `RaydiumV4`, or `OrcaWhirPool`. |
|
||||
| `Event` | Normalized action, such as `buy`, `sell`, `add_liquidity`, `remove_liquidity`, `create`, `complete`, or `migrate`. |
|
||||
| `TxIndex` | Approximate execution order across outer and inner instructions. |
|
||||
| `InstrIdx` / `InnerIdx` | Outer instruction index and inner instruction index where the swap was found. |
|
||||
| `Pool` | Pool, pair, bonding curve, or market account for the parsed action. |
|
||||
| `BaseMint` / `QuoteMint` | Base and quote mint accounts after parser normalization. |
|
||||
| `BaseTokenProgram` / `QuoteTokenProgram` | Token program for each side, useful when Token-2022 is involved. |
|
||||
| `BaseMintDecimals` / `QuoteMintDecimals` | Decimals used to interpret raw token amounts. |
|
||||
| `User` | User or effective owner account for the action. If the parsed user is not on-curve, the parser may fall back to the transaction signer. |
|
||||
| `BaseAmount` / `QuoteAmount` | Actual parsed base-side and quote-side amounts, stored as `decimal.Decimal`. |
|
||||
| `BaseReserve` / `QuoteReserve` | Pool reserves when the protocol event or accounts expose them. |
|
||||
| `UserBaseBalance` / `UserQuoteBalance` | User token balances after the transaction when available from token balance metadata. |
|
||||
| `AfterSOLBalance` | User or signer SOL balance after the transaction. |
|
||||
| `EntryContract` | Known router / entry contract account when detected. |
|
||||
| `Mayhem` / `Cashback` | Protocol or platform-specific labels detected from the transaction path. |
|
||||
|
||||
Swap amount and slippage fields normalize instruction intent:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `SwapMode` | `exact_in`, `exact_out`, or empty when unknown. |
|
||||
| `FixedAmount` / `FixedAmountSide` / `FixedMint` | The user-specified fixed side of the swap. For `exact_in`, this is the input amount. For `exact_out`, this is the target output amount. |
|
||||
| `LimitAmountType` | `min_out` for `exact_in`, `max_in` for `exact_out`, or empty when unknown. |
|
||||
| `LimitAmount` / `LimitAmountSide` / `LimitMint` | User-specified limit on the opposite side. |
|
||||
| `ActualLimitAmount` / `ActualLimitAmountSide` | Actual executed amount on the limited side. |
|
||||
| `SlippageBps` | Remaining headroom to the user's limit in basis points. See `SLIPPAGE_MAPPING.md` for protocol-specific derivation rules. |
|
||||
|
||||
Liquidity, migration, and DLMM-specific fields are populated only for protocols that expose them:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `Creator` | Creator account when exposed by pool or launch events. |
|
||||
| `MigrateToPool` / `MigrateTopProgram` | Destination pool and program for migration events. |
|
||||
| `LpMint` | LP token mint for liquidity operations when available. |
|
||||
| `ActiveBinId` / `StartBinId` / `EndBinId` | Meteora DLMM bin identifiers. |
|
||||
| `RemoveBp` | DLMM remove-liquidity basis points. |
|
||||
| `PositionAccount` | DLMM position account. |
|
||||
| `FeeAmount` / `LpFeeAmount` | Parsed fee amounts when the protocol exposes fee breakdowns. |
|
||||
| `FeeSide` / `FeeMint` / `FeeTokenProgram` / `FeeMintDecimals` | Fee side and mint metadata. |
|
||||
| `ConsumeUnit` | Per-swap compute unit value when available. |
|
||||
|
||||
Amounts are normalized into decimals in parser output, but the exact scale depends on the source event and parser path. For persisted binary output, `tx_binary.go` defines the conversion rules and schema version.
|
||||
|
||||
## Convert Transactions
|
||||
|
||||
RPC transactions can be converted with `FromRpcTransactionWithMeta`:
|
||||
|
||||
```go
|
||||
rawTx, err := pump_parser.FromRpcTransactionWithMeta(
|
||||
txWithMeta,
|
||||
blockTime,
|
||||
slot,
|
||||
indexWithinBlock,
|
||||
)
|
||||
```
|
||||
|
||||
Yellowstone transactions can be converted with `ConvertYellowstoneGrpcTransactionToSolanaTransaction`:
|
||||
|
||||
```go
|
||||
rawTx, err := pump_parser.ConvertYellowstoneGrpcTransactionToSolanaTransaction(
|
||||
update,
|
||||
createdUnix,
|
||||
)
|
||||
```
|
||||
|
||||
Both conversion paths accept optional `RawTxConvertOptions`:
|
||||
|
||||
```go
|
||||
rawTx, err := pump_parser.FromRpcTransactionWithMeta(
|
||||
txWithMeta,
|
||||
blockTime,
|
||||
slot,
|
||||
indexWithinBlock,
|
||||
pump_parser.RawTxConvertOptions{
|
||||
ParseLogEvents: true,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
`ParseLogEvents` attaches decoded program log events to matching instructions. `IgnoreLogMessages` skips log-message retention.
|
||||
|
||||
## Binary Formats
|
||||
|
||||
Parsed transaction binary:
|
||||
|
||||
- `EncodeTxBinary` / `DecodeTxBinary` for one parsed `Tx`.
|
||||
- `EncodeTxsBinary` / `DecodeTxsBinary` for a batch of parsed `Tx`.
|
||||
- `DecodeTxsBinaryReader` for streaming `PTXS` reads.
|
||||
- `MergeTxsBinaryBytes` and `MergeTxsBinarySourcesToWriter` for merging `PTXS` batches.
|
||||
|
||||
Raw transaction binary:
|
||||
|
||||
- `EncodeRawTxBinary` / `DecodeRawTxBinary` for one `RawTx`.
|
||||
- `EncodeRawTxsBinary` / `DecodeRawTxsBinary` for a batch of `RawTx`.
|
||||
- `EncodeRawTxBlocksBinary` / `DecodeRawTxBlocksBinary` for grouped block data.
|
||||
- `DecodeRawTxsBinaryReader` for streaming `PRTS` reads.
|
||||
|
||||
Format magic values:
|
||||
|
||||
- `PTXB`: one parsed `Tx`.
|
||||
- `PTXS`: parsed `Tx` batch.
|
||||
- `PRTX`: one raw `RawTx`.
|
||||
- `PRTS`: raw `RawTx` batch.
|
||||
- `PRBS`: raw block-grouped `RawTx` batch.
|
||||
|
||||
When adding or renaming transaction-facing enum values, update `tx_binary.go` enum tables by appending new values only. Reordering existing values changes persisted numeric IDs.
|
||||
|
||||
## Commands
|
||||
|
||||
Parse a live transaction by signature:
|
||||
|
||||
```bash
|
||||
TX_HASH=<signature> go run ./cmd/rpc_parse
|
||||
```
|
||||
|
||||
Collect Yellowstone transactions into a `.prbs` raw-block binary file:
|
||||
|
||||
```bash
|
||||
YELLOWSTONE_X_TOKEN=<token> go run ./cmd/collect_yellowstone_rawtx_binary \
|
||||
-endpoint ams.rpc.orbitflare.com:10000 \
|
||||
-duration 5m \
|
||||
-output testdata/rawtx-binary/sample.prbs
|
||||
```
|
||||
|
||||
Measure parsed `Tx` binary size from a `getBlock` JSON payload:
|
||||
|
||||
```bash
|
||||
go run ./cmd/measure_tx_binary_block \
|
||||
-file /path/to/block.json \
|
||||
-slot 413539056 \
|
||||
-swaps-only
|
||||
```
|
||||
|
||||
Analyze raw transaction binary size distribution:
|
||||
|
||||
```bash
|
||||
go run ./cmd/analyze_rawtx_binary_size \
|
||||
-file testdata/rawtx-binary/sample.prbs
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Run the full test suite:
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Useful focused checks:
|
||||
|
||||
```bash
|
||||
go test -run 'TestTxBinary|TestRawTxBinary' .
|
||||
go test -run 'TestMetaoraPoolSwapEventFromLogsUsesMatchingInvocation|TestMetaoraPoolSwapInstructionOccurrenceIncludesInnerInstructions|TestAttachLogEventsToInstructions' .
|
||||
TX_HASH=<signature> go run ./cmd/rpc_parse
|
||||
```
|
||||
|
||||
For parser regressions, reproduce with the exact transaction signature first, then add a targeted unit test or fixture around the corrected parser path.
|
||||
@@ -25,6 +25,9 @@ func chainLinkParser(tx *Tx, instruction Instruction, inners InnerInstructions,
|
||||
}
|
||||
|
||||
decode := instruction.Data
|
||||
if len(decode) < 4 {
|
||||
return increaseOffset(offset), nil
|
||||
}
|
||||
discriminator := binary.LittleEndian.Uint32(decode[0:4])
|
||||
|
||||
switch discriminator {
|
||||
@@ -55,6 +58,9 @@ func chainLinkSubmitParser(instruction Instruction, inners InnerInstructions, of
|
||||
if storeInstruction.Accounts[0] >= len(tx.rawTx.accountList) || tx.rawTx.accountList[storeInstruction.Accounts[0]] != chainlinkSOLUSDFeedAccount {
|
||||
return increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
if len(storeInstruction.Data) < 8 {
|
||||
return increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
if !bytes.Equal(storeInstruction.Data[0:8], chainlinkSubmitDiscriminator[:]) {
|
||||
return increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func main() {
|
||||
const rpcURL = "https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"
|
||||
txHash := os.Getenv("TX_HASH")
|
||||
if txHash == "" {
|
||||
txHash = "29v7u2ewLr3Se6cWYC2xwN8jszqMWwvVgPz7MqkctTveMo1csWWYDBcUsjuJwb5ciugc5so1jc9QcmR7syJTjEns"
|
||||
txHash = "24wP3rk2ZfSVDB5YGyEQbhuy1jRXKZYkzXDRrDwwoKgzD6G4Kyyh4vmnir9ye98uLVKA5bBMj5Fq4cwgbDxp2Gie"
|
||||
}
|
||||
|
||||
if txHash == "" {
|
||||
|
||||
21
codex.md
Normal file
21
codex.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Codex Notes
|
||||
|
||||
## Tx Binary enum synchronization
|
||||
|
||||
When adding or renaming transaction-facing enum values, keep the binary format definitions in sync.
|
||||
|
||||
This includes, but is not limited to:
|
||||
|
||||
- tx events
|
||||
- programs
|
||||
- platforms
|
||||
- MEV agents
|
||||
- swap modes, amount sides, limit types, and fee sides
|
||||
|
||||
Checklist:
|
||||
|
||||
1. Add the public constant in the normal source location, such as `enum.go`.
|
||||
2. Add any address mapping in `consts.go` when the enum is account-derived, such as platform or MEV agent detection.
|
||||
3. Append the new value to the matching versioned enum list in `tx_binary.go` under `txBinaryEnumTables`.
|
||||
4. Do not reorder or insert into existing `tx_binary.go` enum lists unless the binary version is intentionally changed; append to preserve existing numeric IDs.
|
||||
5. Add or update tx-binary round-trip coverage so encoding and decoding the new enum value is exercised.
|
||||
18
consts.go
18
consts.go
@@ -186,6 +186,12 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
||||
solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("soyasF3QPWPAKKmgA3GjfWax1kmTT1aoqSGxPzVLNUQ"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("soyasi59njacMUPvo3TM5paHjeK8pYSdovXgFi32gRt"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("soyasQYhJxv8uZgWDxhg72td6piAf7XTkoyWHtSATEz"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("soyastP66xyYC8XADXZjdMM5BAVGD2YRvz8dwtLsqb8"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("soyasvdgUJWYcUCzDxpmjUnNjH7KamXLXTzLwFvdVPE"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("soyasvxAunisNxaoRxkKGjNir7KmbwYnr37JmefkX9G"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("soyas5doVFUwH8s5zK8gEvCL5KR5ogDmf52LsrJEZ9h"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium,
|
||||
solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium,
|
||||
solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium,
|
||||
@@ -200,6 +206,8 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
||||
solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane,
|
||||
solana.MustPublicKeyFromBase58("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"): MevAgentAstralane,
|
||||
solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane,
|
||||
solana.MustPublicKeyFromBase58("astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk"): MevAgentAstralane,
|
||||
solana.MustPublicKeyFromBase58("astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK"): MevAgentAstralane,
|
||||
solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane,
|
||||
solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane,
|
||||
solana.MustPublicKeyFromBase58("b1ooMDLjzz4QqecNsJ8bBXzJTzfAPDCP3CxijTS2K93"): MevAgentAstralane,
|
||||
@@ -381,6 +389,16 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
||||
solana.MustPublicKeyFromBase58("t7qUQU35sLpPydh42BcPmtEfTWW8gBe4Ry3gjwVnokJ"): MevAgentRaiden,
|
||||
solana.MustPublicKeyFromBase58("t8pPgarSK3TnuLbbHmoE1RCQdLxfxuPqNTyFjBKahok"): MevAgentRaiden,
|
||||
solana.MustPublicKeyFromBase58("t96GGdw3MiaGR993XN8PSsRpKGXx56t5Wf6zcF1hBpY"): MevAgentRaiden,
|
||||
solana.MustPublicKeyFromBase58("7HkiWXe5deJvzn4D6kgMUFCADwX9Z4DMrdjNSSxN6bPp"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zanrUknLZXzT9JPj968A7RfgCjp77Lx1W1xKRAtfshb"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zanHbk2UsiT3jKsKjD7UuEqS5Vgpmcd4pG9HycAAV8g"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zanNazKCXNRoKnPS9BBbFTELTpNwUDJxeKEb1JtZJer"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zan3gbFhXCjGLHhRe2vaXRDta5fCrYiYr3Dq4RLvpfU"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zan6WoE3DX5aK7FMQT1vSGsGrgZG1ngns3oCsFMnBHU"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zan8Nb9fB4zMDsuTRP9R65QZbc9v2Cjn5a4Hjwnj8D3"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zanJgoR7ALJAJ6ohoKs6aS9T71D9ZkNN9gYM5xUsi3u"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zanAtYifQP7Bo6kStB97mJvzqSDW1toKNibWibwcKDd"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("GWT5UjDheZzoqinLavJkYvSRH5sakW8vDRdAgrUS5ZcS"): MevAgentTunneling,
|
||||
}
|
||||
|
||||
var entryContractAddresses = map[solana.PublicKey]string{
|
||||
|
||||
2
enum.go
2
enum.go
@@ -21,6 +21,8 @@ const (
|
||||
MevAgentSpeedlanding = "speedlanding"
|
||||
MevAgentAllenhark = "allenhark"
|
||||
MevAgentRaiden = "raiden"
|
||||
MevAgentZan = "zan"
|
||||
MevAgentTunneling = "tunneling"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -25,7 +25,7 @@ func main() {
|
||||
// laserstream-mainnet-slc.helius-rpc.com:80
|
||||
|
||||
ch := make(chan example.SubscriptionMessage, 1)
|
||||
go example.RunLoopWithReConnect(context.Background(), "", "", parser.SolProgramPump, ch)
|
||||
go example.RunLoopWithReConnect(context.Background(), "ams.rpc.orbitflare.com:10000", "ORBIT-EPUZGQ-177605-508881", parser.SolProgramPump, ch)
|
||||
// var tokenTxs = make(map[string]*types.Tx)
|
||||
// currentBlock := uint64(0)
|
||||
for msg := range ch {
|
||||
|
||||
@@ -741,6 +741,9 @@ func metaoraPoolRemoveLiquidity(tx *Tx, instruction Instruction, innerInstructio
|
||||
}
|
||||
|
||||
func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||
if len(instruction.Accounts) < 13 {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction")
|
||||
}
|
||||
swapOffset := offset
|
||||
var args metaoraPoolSwapArgs
|
||||
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {
|
||||
|
||||
@@ -83,7 +83,13 @@ type MetaoraDammInitializePoolEvent struct {
|
||||
}
|
||||
|
||||
func meteoraDammV2InitializePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||
if len(instruction.Accounts) < 12 {
|
||||
requiredAccounts := 12
|
||||
if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializePoolWithDynamicConfig[:]) {
|
||||
requiredAccounts = 13
|
||||
} else if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializeCustomizablePoolDiscriminator[:]) {
|
||||
requiredAccounts = 11
|
||||
}
|
||||
if len(instruction.Accounts) < requiredAccounts {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
||||
}
|
||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||
@@ -439,7 +445,7 @@ func meteoraDammV2AddLiquidityParser(tx *Tx, instruction Instruction, innerInstr
|
||||
}
|
||||
|
||||
func meteoraDammV2RemoveLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||
if len(instruction.Accounts) < 8 {
|
||||
if len(instruction.Accounts) < 9 {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
||||
}
|
||||
tokenAMint := tx.rawTx.accountList[instruction.Accounts[7]]
|
||||
|
||||
11
pump.go
11
pump.go
@@ -173,9 +173,17 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
|
||||
}
|
||||
userIndex := 0
|
||||
if bytes.HasPrefix(instr.Data, pumpCreateV2Discriminator[:]) {
|
||||
if len(instr.Accounts) < 6 {
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
userIndex = instr.Accounts[5]
|
||||
} else if bytes.HasPrefix(instr.Data, pumpCreateDiscriminator[:]) {
|
||||
if len(instr.Accounts) < 8 {
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
userIndex = instr.Accounts[7]
|
||||
} else {
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
userBase := getAccountBalanceAfterTx(result, userIndex)
|
||||
userQuote, _ := GetSolAfterTx(result, userIndex)
|
||||
@@ -777,6 +785,9 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
|
||||
for innerIndex, innerInstr := range inners {
|
||||
if innerInstr.ProgramIDIndex == feeEventProgramIndex && bytes.Equal(innerInstr.Data[:8], pumpGetFeesDiscriminator[:]) {
|
||||
if tradeFound {
|
||||
continue
|
||||
}
|
||||
err = agbinary.NewBorshDecoder(innerInstr.Data[8:]).Decode(&tradeFeeArg)
|
||||
if err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("pump get fees event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
|
||||
25
pump_test.go
25
pump_test.go
@@ -102,6 +102,31 @@ func TestPumpCompleteMatchesTradeEvent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPumpExactQuoteInKeepsFeeArgBeforeMatchedTrade(t *testing.T) {
|
||||
EnableAllParsers()
|
||||
|
||||
tx := mustParseRPCFixtureTx(t, "3jugr2KthX3cUHzPrMpaFKM7RtxXM6Gcxi8eFjDL7aZGLXpc6f1RaVdnAoB4ye5bRVYsP2fFs3aLaP19Utz91ewv")
|
||||
if len(tx.Swaps) != 4 {
|
||||
t.Fatalf("swaps len = %d, want 4", len(tx.Swaps))
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
swap := tx.Swaps[i]
|
||||
if swap.Program != SolProgramPump || swap.Event != "buy" {
|
||||
t.Fatalf("swap[%d] = %s/%s, want Pump/buy", i, swap.Program, swap.Event)
|
||||
}
|
||||
assertDecimalString(t, fmt.Sprintf("swap[%d].quote_amount", i), swap.QuoteAmount, "329217")
|
||||
assertDecimalString(t, fmt.Sprintf("swap[%d].fixed_amount", i), swap.FixedAmount, "333333")
|
||||
}
|
||||
|
||||
sell := tx.Swaps[3]
|
||||
if sell.Program != SolProgramPump || sell.Event != "sell" {
|
||||
t.Fatalf("swap[3] = %s/%s, want Pump/sell", sell.Program, sell.Event)
|
||||
}
|
||||
assertDecimalString(t, "swap[3].base_amount", sell.BaseAmount, "12282189230")
|
||||
assertDecimalString(t, "swap[3].quote_amount", sell.QuoteAmount, "987647")
|
||||
}
|
||||
|
||||
func TestPumpV2Discriminators(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
21
pumpamm.go
21
pumpamm.go
@@ -182,6 +182,9 @@ func pumpAmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
||||
}
|
||||
|
||||
func ammCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||
if len(instruction.Accounts) < 15 {
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
result := tx.rawTx
|
||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||
var err error
|
||||
@@ -275,6 +278,9 @@ func pumpAmmSwapAmountInfoFromArgs(args PumpSwapArgs) (swapMode SwapMode, fixedA
|
||||
}
|
||||
|
||||
func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||
if len(instruction.Accounts) < 13 {
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
if tx.Err == nil || tx.Err.UnKnown != "" {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
||||
}
|
||||
@@ -401,6 +407,9 @@ func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions Inn
|
||||
}
|
||||
|
||||
func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||
if len(instruction.Accounts) < 13 {
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
if tx.Err == nil || tx.Err.UnKnown != "" {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
||||
}
|
||||
@@ -525,6 +534,9 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||
var err error
|
||||
var prefixLen = offset[1]
|
||||
if len(instruction.Accounts) < 13 {
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||
if err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
|
||||
@@ -662,6 +674,9 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
||||
result := tx.rawTx
|
||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||
var err error
|
||||
if len(instruction.Accounts) < 13 {
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
var prefixLen = offset[1]
|
||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||
if err != nil {
|
||||
@@ -789,6 +804,9 @@ func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
||||
result := tx.rawTx
|
||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||
var err error
|
||||
if len(instruction.Accounts) < 11 {
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
var prefixLen = offset[1]
|
||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||
if err != nil {
|
||||
@@ -887,6 +905,9 @@ func withdrawParse(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
||||
result := tx.rawTx
|
||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||
var err error
|
||||
if len(instruction.Accounts) < 11 {
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
var prefixLen = offset[1]
|
||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||
if err != nil {
|
||||
|
||||
4
rawtx.go
4
rawtx.go
@@ -3,6 +3,7 @@ package pump_parser
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -876,6 +877,9 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT
|
||||
//Version: nil,
|
||||
}
|
||||
meta := y.Transaction.GetMeta()
|
||||
if meta == nil {
|
||||
return nil, errors.New("meta can not parser")
|
||||
}
|
||||
yTx := y.Transaction.Transaction
|
||||
|
||||
if meta.Err != nil && len(meta.Err.GetErr()) > 0 {
|
||||
|
||||
@@ -108,38 +108,14 @@ func raydiumClmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
||||
switch discriminator {
|
||||
case raydiumClmmIncreaseLiquidityDiscriminator:
|
||||
accountMin = 12
|
||||
market = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||
vault0 = instruction.Accounts[9]
|
||||
vault1 = instruction.Accounts[10]
|
||||
case raydiumClmmIncreaseLiquidityV2Discriminator:
|
||||
accountMin = 15
|
||||
market = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||
vault0 = instruction.Accounts[9]
|
||||
vault1 = instruction.Accounts[10]
|
||||
//token0 = tx.rawTx.accountList[instruction.Accounts[13]]
|
||||
//token1 = tx.rawTx.accountList[instruction.Accounts[14]]
|
||||
case raydiumClmmOpenPositionDiscriminator:
|
||||
accountMin = 19
|
||||
market = tx.rawTx.accountList[instruction.Accounts[5]]
|
||||
vault0 = instruction.Accounts[12]
|
||||
vault1 = instruction.Accounts[13]
|
||||
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||
case raydiumClmmOpenPositionV2Discriminator:
|
||||
accountMin = 22
|
||||
market = tx.rawTx.accountList[instruction.Accounts[5]]
|
||||
vault0 = instruction.Accounts[12]
|
||||
vault1 = instruction.Accounts[13]
|
||||
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||
//token0 = tx.rawTx.accountList[instruction.Accounts[20]]
|
||||
//token1 = tx.rawTx.accountList[instruction.Accounts[21]]
|
||||
case raydiumClmmOpenPositionWithToken22NftDiscriminator:
|
||||
accountMin = 20
|
||||
market = tx.rawTx.accountList[instruction.Accounts[4]]
|
||||
vault0 = instruction.Accounts[11]
|
||||
vault1 = instruction.Accounts[12]
|
||||
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||
//token0 = tx.rawTx.accountList[instruction.Accounts[18]]
|
||||
//token1 = tx.rawTx.accountList[instruction.Accounts[19]]
|
||||
default:
|
||||
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
|
||||
}
|
||||
@@ -148,6 +124,23 @@ func raydiumClmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
||||
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for raydiumClmm add liquidity instruction, offset, %d, %d", offset[0], offset[1])
|
||||
}
|
||||
|
||||
switch discriminator {
|
||||
case raydiumClmmIncreaseLiquidityDiscriminator, raydiumClmmIncreaseLiquidityV2Discriminator:
|
||||
market = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||
vault0 = instruction.Accounts[9]
|
||||
vault1 = instruction.Accounts[10]
|
||||
case raydiumClmmOpenPositionDiscriminator, raydiumClmmOpenPositionV2Discriminator:
|
||||
market = tx.rawTx.accountList[instruction.Accounts[5]]
|
||||
vault0 = instruction.Accounts[12]
|
||||
vault1 = instruction.Accounts[13]
|
||||
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||
case raydiumClmmOpenPositionWithToken22NftDiscriminator:
|
||||
market = tx.rawTx.accountList[instruction.Accounts[4]]
|
||||
vault0 = instruction.Accounts[11]
|
||||
vault1 = instruction.Accounts[12]
|
||||
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||
}
|
||||
|
||||
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
||||
if err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
|
||||
@@ -197,6 +190,8 @@ func raydiumClmmDecreaseLiquidityParser(tx *Tx, instruction Instruction, innerIn
|
||||
accountMin = 14
|
||||
} else if discriminator == raydiumClmmDecreaseLiquidityV2Discriminator {
|
||||
accountMin = 16
|
||||
} else {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
|
||||
}
|
||||
if len(instruction.Accounts) < accountMin {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for decrease liquidity instruction")
|
||||
@@ -299,23 +294,21 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
||||
}
|
||||
if discriminator == raydiumClmmSwapDiscriminator {
|
||||
accountMin = 9
|
||||
pool = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||
userTokenInAccount = instruction.Accounts[3]
|
||||
userTokenOutAccount = instruction.Accounts[4]
|
||||
tokenInVault = instruction.Accounts[5]
|
||||
tokenOutVault = instruction.Accounts[6]
|
||||
} else if discriminator == raydiumClmmSwapV2Discriminator {
|
||||
accountMin = 13
|
||||
pool = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||
userTokenInAccount = instruction.Accounts[3]
|
||||
userTokenOutAccount = instruction.Accounts[4]
|
||||
tokenInVault = instruction.Accounts[5]
|
||||
tokenOutVault = instruction.Accounts[6]
|
||||
} else {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
|
||||
}
|
||||
if len(instruction.Accounts) < accountMin {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction")
|
||||
}
|
||||
|
||||
pool = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||
userTokenInAccount = instruction.Accounts[3]
|
||||
userTokenOutAccount = instruction.Accounts[4]
|
||||
tokenInVault = instruction.Accounts[5]
|
||||
tokenOutVault = instruction.Accounts[6]
|
||||
|
||||
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, tokenInVault)
|
||||
if err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("failed to get tokenIn vault balance after tx: %w", err)
|
||||
|
||||
@@ -373,6 +373,9 @@ type raydiumLaunchLabSwapArgs struct {
|
||||
}
|
||||
|
||||
func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||
if len(instruction.Accounts) < 13 {
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
|
||||
var programName string
|
||||
if platformConfig.Equals(bonkPlatformConfig) {
|
||||
|
||||
@@ -15,6 +15,9 @@ func systemParser(tx *Tx, instruction Instruction, _ InnerInstructions, offset [
|
||||
}
|
||||
|
||||
decode := instruction.Data
|
||||
if len(decode) < 4 {
|
||||
return increaseOffset(offset), nil
|
||||
}
|
||||
discriminator := binary.LittleEndian.Uint32(decode[0:4])
|
||||
|
||||
switch discriminator {
|
||||
@@ -29,6 +32,9 @@ func TransferParser(result *RawTx, instruction Instruction, offset [2]uint, tx *
|
||||
if len(decodeData) < 8 {
|
||||
return increaseOffset(offset), nil
|
||||
}
|
||||
if len(instruction.Accounts) < 2 || len(result.Transaction.Message.Instructions[offset[0]].Accounts) < 1 {
|
||||
return increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
var lamports uint64 = binary.LittleEndian.Uint64(decodeData)
|
||||
|
||||
from := result.accountList[result.Transaction.Message.Instructions[offset[0]].Accounts[0]]
|
||||
|
||||
File diff suppressed because one or more lines are too long
31
tx_binary.go
31
tx_binary.go
@@ -1191,7 +1191,7 @@ func (enc *txBinaryEncoder) writeTxBinaryBody(tx *TxBinary, enumTable *txBinaryE
|
||||
func (enc *txBinaryEncoder) writePlatformEntries(entries []PlatformBinary, enumTable *txBinaryEnumTable) error {
|
||||
enc.writeUint32(uint32(len(entries)))
|
||||
for i, entry := range entries {
|
||||
enumID, err := enumTable.platforms.id(entry.Platform)
|
||||
enumID, err := enumTable.platforms.idOrFallback(entry.Platform, PlatformNone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("platform[%d]: %w", i, err)
|
||||
}
|
||||
@@ -1204,7 +1204,7 @@ func (enc *txBinaryEncoder) writePlatformEntries(entries []PlatformBinary, enumT
|
||||
func (enc *txBinaryEncoder) writeMevAgentEntries(entries []MevAgentBinary, enumTable *txBinaryEnumTable) error {
|
||||
enc.writeUint32(uint32(len(entries)))
|
||||
for i, entry := range entries {
|
||||
enumID, err := enumTable.mevAgents.id(entry.MevAgent)
|
||||
enumID, err := enumTable.mevAgents.idOrFallback(entry.MevAgent, MevAgentUnknown)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mev_agent[%d]: %w", i, err)
|
||||
}
|
||||
@@ -1592,7 +1592,7 @@ func txBinaryReadPlatformEntries(dec txBinaryBodyReader, enumTable *txBinaryEnum
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
platform, err := enumTable.platforms.value(enumID)
|
||||
platform, err := enumTable.platforms.valueOrFallback(enumID, PlatformNone)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("platform[%d]: %w", i, err)
|
||||
}
|
||||
@@ -1619,7 +1619,7 @@ func txBinaryReadMevAgentEntries(dec txBinaryBodyReader, enumTable *txBinaryEnum
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mevAgent, err := enumTable.mevAgents.value(enumID)
|
||||
mevAgent, err := enumTable.mevAgents.valueOrFallback(enumID, MevAgentUnknown)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mev_agent[%d]: %w", i, err)
|
||||
}
|
||||
@@ -2143,6 +2143,8 @@ var txBinaryEnumTables = map[uint16]*txBinaryEnumTable{
|
||||
MevAgentSpeedlanding,
|
||||
MevAgentAllenhark,
|
||||
MevAgentRaiden,
|
||||
MevAgentZan,
|
||||
MevAgentTunneling,
|
||||
},
|
||||
),
|
||||
}
|
||||
@@ -2198,9 +2200,30 @@ func (set txBinaryEnumSet) id(value string) (uint16, error) {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (set txBinaryEnumSet) idOrFallback(value string, fallback string) (uint16, error) {
|
||||
if id, ok := set.ids[value]; ok {
|
||||
return id, nil
|
||||
}
|
||||
id, ok := set.ids[fallback]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unsupported %s fallback enum value %q for versioned tx binary", set.name, fallback)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (set txBinaryEnumSet) value(id uint16) (string, error) {
|
||||
if int(id) >= len(set.values) {
|
||||
return "", fmt.Errorf("unknown %s enum id %d", set.name, id)
|
||||
}
|
||||
return set.values[id], nil
|
||||
}
|
||||
|
||||
func (set txBinaryEnumSet) valueOrFallback(id uint16, fallback string) (string, error) {
|
||||
if int(id) < len(set.values) {
|
||||
return set.values[id], nil
|
||||
}
|
||||
if _, ok := set.ids[fallback]; !ok {
|
||||
return "", fmt.Errorf("unsupported %s fallback enum value %q for versioned tx binary", set.name, fallback)
|
||||
}
|
||||
return fallback, nil
|
||||
}
|
||||
|
||||
@@ -41,6 +41,14 @@ func TestTxBinaryRoundTrip(t *testing.T) {
|
||||
MevAgent: MevAgentJito,
|
||||
MevAgentFee: decimal.RequireFromString("0.030000000"),
|
||||
},
|
||||
MevAgentZan: {
|
||||
MevAgent: MevAgentZan,
|
||||
MevAgentFee: decimal.RequireFromString("0.040000000"),
|
||||
},
|
||||
MevAgentTunneling: {
|
||||
MevAgent: MevAgentTunneling,
|
||||
MevAgentFee: decimal.RequireFromString("0.050000000"),
|
||||
},
|
||||
},
|
||||
Swaps: []Swap{
|
||||
{
|
||||
@@ -146,6 +154,12 @@ func TestTxBinaryRoundTrip(t *testing.T) {
|
||||
if !decoded.MevAgent[MevAgentJito].MevAgentFee.Equal(original.MevAgent[MevAgentJito].MevAgentFee) {
|
||||
t.Fatalf("MevAgent fee mismatch")
|
||||
}
|
||||
if !decoded.MevAgent[MevAgentZan].MevAgentFee.Equal(original.MevAgent[MevAgentZan].MevAgentFee) {
|
||||
t.Fatalf("Zan MevAgent fee mismatch")
|
||||
}
|
||||
if !decoded.MevAgent[MevAgentTunneling].MevAgentFee.Equal(original.MevAgent[MevAgentTunneling].MevAgentFee) {
|
||||
t.Fatalf("Tunneling MevAgent fee mismatch")
|
||||
}
|
||||
if len(decoded.Swaps) != 1 {
|
||||
t.Fatalf("Swaps len = %d, want 1", len(decoded.Swaps))
|
||||
}
|
||||
@@ -225,6 +239,87 @@ func TestTxBinaryRejectsUnknownProgramEnum(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxBinaryLabelEnumsFallbackToUnknown(t *testing.T) {
|
||||
original := &Tx{
|
||||
Signer: solana.WrappedSol,
|
||||
Platform: map[string]platformInfo{
|
||||
"future-platform": {
|
||||
Platform: "future-platform",
|
||||
PlatformFee: decimal.RequireFromString("0.010000000"),
|
||||
},
|
||||
},
|
||||
MevAgent: map[string]mevInfo{
|
||||
"future-mev-agent": {
|
||||
MevAgent: "future-mev-agent",
|
||||
MevAgentFee: decimal.RequireFromString("0.020000000"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded, err := EncodeTxBinary(original)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeTxBinary() error = %v", err)
|
||||
}
|
||||
decoded, err := DecodeTxBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeTxBinary() error = %v", err)
|
||||
}
|
||||
|
||||
if len(decoded.Platform) != 1 {
|
||||
t.Fatalf("Platform len = %d, want 1", len(decoded.Platform))
|
||||
}
|
||||
if _, exists := decoded.Platform["future-platform"]; exists {
|
||||
t.Fatalf("future platform was preserved, want fallback")
|
||||
}
|
||||
if !decoded.Platform[PlatformNone].PlatformFee.Equal(original.Platform["future-platform"].PlatformFee) {
|
||||
t.Fatalf("PlatformNone fee = %s, want %s", decoded.Platform[PlatformNone].PlatformFee, original.Platform["future-platform"].PlatformFee)
|
||||
}
|
||||
|
||||
if len(decoded.MevAgent) != 1 {
|
||||
t.Fatalf("MevAgent len = %d, want 1", len(decoded.MevAgent))
|
||||
}
|
||||
if _, exists := decoded.MevAgent["future-mev-agent"]; exists {
|
||||
t.Fatalf("future mev agent was preserved, want fallback")
|
||||
}
|
||||
if !decoded.MevAgent[MevAgentUnknown].MevAgentFee.Equal(original.MevAgent["future-mev-agent"].MevAgentFee) {
|
||||
t.Fatalf("MevAgentUnknown fee = %s, want %s", decoded.MevAgent[MevAgentUnknown].MevAgentFee, original.MevAgent["future-mev-agent"].MevAgentFee)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxBinaryReadLabelEnumUnknownIDsFallback(t *testing.T) {
|
||||
enumTable := txBinaryEnumTables[txBinaryEnumVersionV1]
|
||||
|
||||
platformFee := uint64(123)
|
||||
platformEnc := txBinaryEncoder{}
|
||||
platformEnc.writeUint32(1)
|
||||
platformEnc.writeUint16(uint16(len(enumTable.platforms.values) + 10))
|
||||
platformEnc.writeUint64(platformFee)
|
||||
|
||||
platformDec := txBinaryDecoder{reader: bytes.NewReader(platformEnc.bytes())}
|
||||
platforms, err := txBinaryReadPlatformEntries(&platformDec, enumTable)
|
||||
if err != nil {
|
||||
t.Fatalf("txBinaryReadPlatformEntries() error = %v", err)
|
||||
}
|
||||
if len(platforms) != 1 || platforms[0].Platform != PlatformNone || platforms[0].PlatformFee != platformFee {
|
||||
t.Fatalf("platform fallback = %+v, want %s/%d", platforms, PlatformNone, platformFee)
|
||||
}
|
||||
|
||||
mevFee := uint64(456)
|
||||
mevEnc := txBinaryEncoder{}
|
||||
mevEnc.writeUint32(1)
|
||||
mevEnc.writeUint16(uint16(len(enumTable.mevAgents.values) + 10))
|
||||
mevEnc.writeUint64(mevFee)
|
||||
|
||||
mevDec := txBinaryDecoder{reader: bytes.NewReader(mevEnc.bytes())}
|
||||
mevAgents, err := txBinaryReadMevAgentEntries(&mevDec, enumTable)
|
||||
if err != nil {
|
||||
t.Fatalf("txBinaryReadMevAgentEntries() error = %v", err)
|
||||
}
|
||||
if len(mevAgents) != 1 || mevAgents[0].MevAgent != MevAgentUnknown || mevAgents[0].MevAgentFee != mevFee {
|
||||
t.Fatalf("mev agent fallback = %+v, want %s/%d", mevAgents, MevAgentUnknown, mevFee)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxBinaryAcceptsKnownEventEnums(t *testing.T) {
|
||||
events := []string{
|
||||
TxEventAddLP,
|
||||
|
||||
Reference in New Issue
Block a user