Compare commits

...

11 Commits

Author SHA1 Message Date
cachalots
4facb86e0a go mod 2026-06-11 17:40:12 +08:00
cachalots
cc03798ff6 bloxroute tip 2026-06-11 17:22:43 +08:00
thloyi
44eecac087 tx binary with block time 2026-06-05 10:35:49 +08:00
thloyi
9f17ffce61 fix accounts len check 2026-05-28 10:23:56 +08:00
thloyi
e4eaddec4e reademe 2026-05-18 14:22:48 +08:00
thloyi
9454c3f6c7 ignore unknonw meta 2026-05-18 11:46:49 +08:00
thloyi
39bfeb085f fix pump wrapper buy and sell 2026-05-13 17:30:06 +08:00
thloyi
10885d5e08 fix pump wrapper buy and sell 2026-05-13 17:16:45 +08:00
thloyi
2406f6d087 fix pump wrapper buy and sell 2026-05-13 17:07:47 +08:00
thloyi
8b608889cb fix pump wrapper buy and sell 2026-05-13 16:54:23 +08:00
cachalots
8d4aad1932 tip 2026-05-13 16:52:55 +08:00
19 changed files with 721 additions and 57 deletions

249
README.md Normal file
View 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.

View File

@@ -25,6 +25,9 @@ func chainLinkParser(tx *Tx, instruction Instruction, inners InnerInstructions,
} }
decode := instruction.Data decode := instruction.Data
if len(decode) < 4 {
return increaseOffset(offset), nil
}
discriminator := binary.LittleEndian.Uint32(decode[0:4]) discriminator := binary.LittleEndian.Uint32(decode[0:4])
switch discriminator { 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 { if storeInstruction.Accounts[0] >= len(tx.rawTx.accountList) || tx.rawTx.accountList[storeInstruction.Accounts[0]] != chainlinkSOLUSDFeedAccount {
return increaseOffset(offset), InstructionIgnoredError return increaseOffset(offset), InstructionIgnoredError
} }
if len(storeInstruction.Data) < 8 {
return increaseOffset(offset), InstructionIgnoredError
}
if !bytes.Equal(storeInstruction.Data[0:8], chainlinkSubmitDiscriminator[:]) { if !bytes.Equal(storeInstruction.Data[0:8], chainlinkSubmitDiscriminator[:]) {
return increaseOffset(offset), InstructionIgnoredError return increaseOffset(offset), InstructionIgnoredError
} }

View File

@@ -14,7 +14,7 @@ func main() {
const rpcURL = "https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d" const rpcURL = "https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"
txHash := os.Getenv("TX_HASH") txHash := os.Getenv("TX_HASH")
if txHash == "" { if txHash == "" {
txHash = "29v7u2ewLr3Se6cWYC2xwN8jszqMWwvVgPz7MqkctTveMo1csWWYDBcUsjuJwb5ciugc5so1jc9QcmR7syJTjEns" txHash = "24wP3rk2ZfSVDB5YGyEQbhuy1jRXKZYkzXDRrDwwoKgzD6G4Kyyh4vmnir9ye98uLVKA5bBMj5Fq4cwgbDxp2Gie"
} }
if txHash == "" { if txHash == "" {

21
codex.md Normal file
View 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.

View File

@@ -116,6 +116,20 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("HZTmLyC683y74TW3HtGbNX5orxjm2sPuZBEYwwSgAM8v"): MevAgentBlocxRoute, solana.MustPublicKeyFromBase58("HZTmLyC683y74TW3HtGbNX5orxjm2sPuZBEYwwSgAM8v"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("FogxVNs6Mm2w9rnGL1vkARSwJxvLE8mujTv3LK8RnUhF"): MevAgentBlocxRoute, solana.MustPublicKeyFromBase58("FogxVNs6Mm2w9rnGL1vkARSwJxvLE8mujTv3LK8RnUhF"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("3UQUKjhMKaY2S6bjcQD6yHB7utcZt5bfarRCmctpRtUd"): MevAgentBlocxRoute, solana.MustPublicKeyFromBase58("3UQUKjhMKaY2S6bjcQD6yHB7utcZt5bfarRCmctpRtUd"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLx7XBqSg3LUPVf1bRgCnkJmgVZR8QEgDJBPqcRLHvp"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLx8KeZxinPwy6kkUgyzMLeqb2ARNsWjADG1dhSsVba"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxADBknoNj8WAGw2W6GBYeq848Xx6ajhaymV1YvrHm"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxAc88vRBwvcUQJEgcxNfBLvHPikY4csNsUmPeWea2"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxQ88oCiTsL8Xj4YWekKi1hjrgmbE3J3FFZ2xZHR3h"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxS7NoLuynNRJ4mCnEE2YbtwJFttYsEyp2ME7rp2yt"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxW6mCov7VEbrKc3S9tcBRcfSzRnLCbNp3Dfn3SJG5"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxXSGXs4mYPTC5okZXed1qzvjNwNJ48QJ82hT2V7w7"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxYi3vojbbB7hVzVDVTdBLVPhp7GJ3ZB3BwdK5sFXi"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxhLPgBXtUpX4b1bH3HatuMGMSKT9GnwtuCGiMSAqe"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxpY1mniuFW4PgkNA4JiNxoeKHFszryi6tNgyZAiAA"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxuETxd2tgWxBALNwPzAfHhsik4BzD3nrEBCiPNZQD"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxuL2gK5FW7xfahvwLrxLyW76vcCpNsKQY2CmnE6kV"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxv4Hnub7nDJWHs8s17o9bGU65Bnx6Yqp2fqtMgHmm"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq"): MevAgentNozomi, solana.MustPublicKeyFromBase58("TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4"): MevAgentNozomi, solana.MustPublicKeyFromBase58("noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE"): MevAgentNozomi, solana.MustPublicKeyFromBase58("noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE"): MevAgentNozomi,
@@ -186,6 +200,12 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas, solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas, solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasF3QPWPAKKmgA3GjfWax1kmTT1aoqSGxPzVLNUQ"): 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("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium,
solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium, solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium,
solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium, solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium,
@@ -200,6 +220,8 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane, solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"): MevAgentAstralane, solana.MustPublicKeyFromBase58("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane, solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane, solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane, solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("b1ooMDLjzz4QqecNsJ8bBXzJTzfAPDCP3CxijTS2K93"): MevAgentAstralane, solana.MustPublicKeyFromBase58("b1ooMDLjzz4QqecNsJ8bBXzJTzfAPDCP3CxijTS2K93"): MevAgentAstralane,
@@ -335,7 +357,10 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("bgDETv6tnt9mwYqAKebLXY5B5o6akiKJmAdU7Gd9G7H"): MevAgentNozomi, solana.MustPublicKeyFromBase58("bgDETv6tnt9mwYqAKebLXY5B5o6akiKJmAdU7Gd9G7H"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgH7YhymSykyvMa3nAZpzvrn73owJHU5iB75S1aiLT9"): MevAgentNozomi, solana.MustPublicKeyFromBase58("bgH7YhymSykyvMa3nAZpzvrn73owJHU5iB75S1aiLT9"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfngGVVQLiVRFbLWw3Ektiv17ef9NiRZbcgdAhh4ZEW"): MevAgentNozomi, solana.MustPublicKeyFromBase58("pfngGVVQLiVRFbLWw3Ektiv17ef9NiRZbcgdAhh4ZEW"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("mwGELGMgGGrNL1UibNCQeJHDE7qdPptWRYB6noUHmTj"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("nEFs3jph8HJt7honu3k7XtGUufMnwAvSXmXcKSPxryP"): MevAgentNozomi, solana.MustPublicKeyFromBase58("nEFs3jph8HJt7honu3k7XtGUufMnwAvSXmXcKSPxryP"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("nzUMeyyucuHSCZLw1aaX14d1si2mffT4PjVNdZcybot"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("n6Tc3GoTheA1pYF4MPV57KGmn3DHJCCuk3wHZmcUypp"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("Fa1con11xLjPddfzRwRUB16sbFZggp2JeJkCeWREyR8X"): MevagentFa1con, solana.MustPublicKeyFromBase58("Fa1con11xLjPddfzRwRUB16sbFZggp2JeJkCeWREyR8X"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con11TM1RuAQzbQzYjTy4Ekfap9Lnc9fnEbQYEd6Q"): MevagentFa1con, solana.MustPublicKeyFromBase58("Fa1con11TM1RuAQzbQzYjTy4Ekfap9Lnc9fnEbQYEd6Q"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con113Bvi76nS5AzUiRDC2fqjfzkNMUNRLgQybMYt"): MevagentFa1con, solana.MustPublicKeyFromBase58("Fa1con113Bvi76nS5AzUiRDC2fqjfzkNMUNRLgQybMYt"): MevagentFa1con,
@@ -381,6 +406,16 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("t7qUQU35sLpPydh42BcPmtEfTWW8gBe4Ry3gjwVnokJ"): MevAgentRaiden, solana.MustPublicKeyFromBase58("t7qUQU35sLpPydh42BcPmtEfTWW8gBe4Ry3gjwVnokJ"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t8pPgarSK3TnuLbbHmoE1RCQdLxfxuPqNTyFjBKahok"): MevAgentRaiden, solana.MustPublicKeyFromBase58("t8pPgarSK3TnuLbbHmoE1RCQdLxfxuPqNTyFjBKahok"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t96GGdw3MiaGR993XN8PSsRpKGXx56t5Wf6zcF1hBpY"): 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{ var entryContractAddresses = map[solana.PublicKey]string{

View File

@@ -21,6 +21,8 @@ const (
MevAgentSpeedlanding = "speedlanding" MevAgentSpeedlanding = "speedlanding"
MevAgentAllenhark = "allenhark" MevAgentAllenhark = "allenhark"
MevAgentRaiden = "raiden" MevAgentRaiden = "raiden"
MevAgentZan = "zan"
MevAgentTunneling = "tunneling"
) )
const ( const (

View File

@@ -25,7 +25,7 @@ func main() {
// laserstream-mainnet-slc.helius-rpc.com:80 // laserstream-mainnet-slc.helius-rpc.com:80
ch := make(chan example.SubscriptionMessage, 1) 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) // var tokenTxs = make(map[string]*types.Tx)
// currentBlock := uint64(0) // currentBlock := uint64(0)
for msg := range ch { for msg := range ch {

View File

@@ -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) { 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 swapOffset := offset
var args metaoraPoolSwapArgs var args metaoraPoolSwapArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil { if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {

View File

@@ -83,7 +83,13 @@ type MetaoraDammInitializePoolEvent struct {
} }
func meteoraDammV2InitializePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { 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") return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
} }
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] 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) { 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") return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
} }
tokenAMint := tx.rawTx.accountList[instruction.Accounts[7]] tokenAMint := tx.rawTx.accountList[instruction.Accounts[7]]

11
pump.go
View File

@@ -173,9 +173,17 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
} }
userIndex := 0 userIndex := 0
if bytes.HasPrefix(instr.Data, pumpCreateV2Discriminator[:]) { if bytes.HasPrefix(instr.Data, pumpCreateV2Discriminator[:]) {
if len(instr.Accounts) < 6 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
userIndex = instr.Accounts[5] userIndex = instr.Accounts[5]
} else if bytes.HasPrefix(instr.Data, pumpCreateDiscriminator[:]) { } else if bytes.HasPrefix(instr.Data, pumpCreateDiscriminator[:]) {
if len(instr.Accounts) < 8 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
userIndex = instr.Accounts[7] userIndex = instr.Accounts[7]
} else {
return nil, increaseOffset(offset), InstructionIgnoredError
} }
userBase := getAccountBalanceAfterTx(result, userIndex) userBase := getAccountBalanceAfterTx(result, userIndex)
userQuote, _ := GetSolAfterTx(result, userIndex) userQuote, _ := GetSolAfterTx(result, userIndex)
@@ -777,6 +785,9 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
for innerIndex, innerInstr := range inners { for innerIndex, innerInstr := range inners {
if innerInstr.ProgramIDIndex == feeEventProgramIndex && bytes.Equal(innerInstr.Data[:8], pumpGetFeesDiscriminator[:]) { if innerInstr.ProgramIDIndex == feeEventProgramIndex && bytes.Equal(innerInstr.Data[:8], pumpGetFeesDiscriminator[:]) {
if tradeFound {
continue
}
err = agbinary.NewBorshDecoder(innerInstr.Data[8:]).Decode(&tradeFeeArg) err = agbinary.NewBorshDecoder(innerInstr.Data[8:]).Decode(&tradeFeeArg)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("pump get fees event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) return nil, increaseOffset(offset), fmt.Errorf("pump get fees event decode error: %v, offset, %d, %d", err, offset[0], offset[1])

View File

@@ -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) { func TestPumpV2Discriminators(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View File

@@ -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) { 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 result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error 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) { 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 != "" { 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]) 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) { 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 != "" { 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]) 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 entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error var err error
var prefixLen = offset[1] var prefixLen = offset[1]
if len(instruction.Accounts) < 13 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
inners, err := getInnerInstructions(innerInstructions, prefixLen) inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen) 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 result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error var err error
if len(instruction.Accounts) < 13 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
var prefixLen = offset[1] var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen) inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil { if err != nil {
@@ -789,6 +804,9 @@ func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstru
result := tx.rawTx result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error var err error
if len(instruction.Accounts) < 11 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
var prefixLen = offset[1] var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen) inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil { if err != nil {
@@ -887,6 +905,9 @@ func withdrawParse(tx *Tx, instruction Instruction, innerInstructions InnerInstr
result := tx.rawTx result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error var err error
if len(instruction.Accounts) < 11 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
var prefixLen = offset[1] var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen) inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil { if err != nil {

View File

@@ -3,6 +3,7 @@ package pump_parser
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@@ -876,6 +877,9 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT
//Version: nil, //Version: nil,
} }
meta := y.Transaction.GetMeta() meta := y.Transaction.GetMeta()
if meta == nil {
return nil, errors.New("meta can not parser")
}
yTx := y.Transaction.Transaction yTx := y.Transaction.Transaction
if meta.Err != nil && len(meta.Err.GetErr()) > 0 { if meta.Err != nil && len(meta.Err.GetErr()) > 0 {

View File

@@ -108,38 +108,14 @@ func raydiumClmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
switch discriminator { switch discriminator {
case raydiumClmmIncreaseLiquidityDiscriminator: case raydiumClmmIncreaseLiquidityDiscriminator:
accountMin = 12 accountMin = 12
market = tx.rawTx.accountList[instruction.Accounts[2]]
vault0 = instruction.Accounts[9]
vault1 = instruction.Accounts[10]
case raydiumClmmIncreaseLiquidityV2Discriminator: case raydiumClmmIncreaseLiquidityV2Discriminator:
accountMin = 15 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: case raydiumClmmOpenPositionDiscriminator:
accountMin = 19 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: case raydiumClmmOpenPositionV2Discriminator:
accountMin = 22 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: case raydiumClmmOpenPositionWithToken22NftDiscriminator:
accountMin = 20 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: default:
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator") 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]) 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) baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) 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 accountMin = 14
} else if discriminator == raydiumClmmDecreaseLiquidityV2Discriminator { } else if discriminator == raydiumClmmDecreaseLiquidityV2Discriminator {
accountMin = 16 accountMin = 16
} else {
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
} }
if len(instruction.Accounts) < accountMin { if len(instruction.Accounts) < accountMin {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for decrease liquidity instruction") 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 { if discriminator == raydiumClmmSwapDiscriminator {
accountMin = 9 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 { } else if discriminator == raydiumClmmSwapV2Discriminator {
accountMin = 13 accountMin = 13
pool = tx.rawTx.accountList[instruction.Accounts[2]] } else {
userTokenInAccount = instruction.Accounts[3] return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
userTokenOutAccount = instruction.Accounts[4]
tokenInVault = instruction.Accounts[5]
tokenOutVault = instruction.Accounts[6]
} }
if len(instruction.Accounts) < accountMin { if len(instruction.Accounts) < accountMin {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction") 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) baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, tokenInVault)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get tokenIn vault balance after tx: %w", err) return nil, increaseOffset(offset), fmt.Errorf("failed to get tokenIn vault balance after tx: %w", err)

View File

@@ -373,6 +373,9 @@ type raydiumLaunchLabSwapArgs struct {
} }
func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { 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]] platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
var programName string var programName string
if platformConfig.Equals(bonkPlatformConfig) { if platformConfig.Equals(bonkPlatformConfig) {

View File

@@ -15,6 +15,9 @@ func systemParser(tx *Tx, instruction Instruction, _ InnerInstructions, offset [
} }
decode := instruction.Data decode := instruction.Data
if len(decode) < 4 {
return increaseOffset(offset), nil
}
discriminator := binary.LittleEndian.Uint32(decode[0:4]) discriminator := binary.LittleEndian.Uint32(decode[0:4])
switch discriminator { switch discriminator {
@@ -29,6 +32,9 @@ func TransferParser(result *RawTx, instruction Instruction, offset [2]uint, tx *
if len(decodeData) < 8 { if len(decodeData) < 8 {
return increaseOffset(offset), nil 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) var lamports uint64 = binary.LittleEndian.Uint64(decodeData)
from := result.accountList[result.Transaction.Message.Instructions[offset[0]].Accounts[0]] from := result.accountList[result.Transaction.Message.Instructions[offset[0]].Accounts[0]]

File diff suppressed because one or more lines are too long

View File

@@ -17,7 +17,8 @@ import (
) )
const ( const (
txBinarySchemaVersionCurrent uint16 = 3 txBinarySchemaVersionV3 uint16 = 3
txBinarySchemaVersionCurrent uint16 = 4
txBinaryEnumVersionV1 uint16 = 1 txBinaryEnumVersionV1 uint16 = 1
txBinarySOLScale int32 = 9 txBinarySOLScale int32 = 9
@@ -27,6 +28,10 @@ const (
var txBinaryMagic = [4]byte{'P', 'T', 'X', 'B'} var txBinaryMagic = [4]byte{'P', 'T', 'X', 'B'}
var txsBinaryMagic = [4]byte{'P', 'T', 'X', 'S'} var txsBinaryMagic = [4]byte{'P', 'T', 'X', 'S'}
func txBinarySchemaVersionSupported(version uint16) bool {
return version >= txBinarySchemaVersionV3 && version <= txBinarySchemaVersionCurrent
}
type TxBinary struct { type TxBinary struct {
SchemaVersion uint16 SchemaVersion uint16
EnumVersion uint16 EnumVersion uint16
@@ -34,6 +39,7 @@ type TxBinary struct {
Signer uint32 Signer uint32
Block uint64 Block uint64
BlockIndex uint64 BlockIndex uint64
BlockAt int64
TxHash *[64]byte TxHash *[64]byte
CuFee uint64 CuFee uint64
Swaps []SwapBinary Swaps []SwapBinary
@@ -204,6 +210,7 @@ func newTxBinaryWithAddressTable(tx *Tx, addressTable []solana.PublicKey, addres
EnumVersion: txBinaryEnumVersionV1, EnumVersion: txBinaryEnumVersionV1,
Block: tx.Block, Block: tx.Block,
BlockIndex: tx.BlockIndex, BlockIndex: tx.BlockIndex,
BlockAt: tx.BlockAt,
CuLimit: tx.CuLimit, CuLimit: tx.CuLimit,
ComputeUnitsConsumed: tx.ComputeUnitsConsumed, ComputeUnitsConsumed: tx.ComputeUnitsConsumed,
} }
@@ -411,6 +418,8 @@ func MergeTxsBinarySourcesToWriterWithOptions(sources []TxsBinaryReaderSource, w
return fmt.Errorf("source[%d].batch[%d].tx[%d]: %w", sourceIndex, batchIndex, txIndex, err) return fmt.Errorf("source[%d].batch[%d].tx[%d]: %w", sourceIndex, batchIndex, txIndex, err)
} }
tx.SchemaVersion = plan.schemaVersion
tx.EnumVersion = plan.enumVersion
bodyBytes, err := txBinaryMarshalTxBody(&tx, plan.enumTable) bodyBytes, err := txBinaryMarshalTxBody(&tx, plan.enumTable)
if err != nil { if err != nil {
reader.Close() reader.Close()
@@ -432,7 +441,7 @@ func (tx *TxBinary) MarshalBinary() ([]byte, error) {
if tx == nil { if tx == nil {
return nil, fmt.Errorf("tx binary is nil") return nil, fmt.Errorf("tx binary is nil")
} }
if tx.SchemaVersion != txBinarySchemaVersionCurrent { if !txBinarySchemaVersionSupported(tx.SchemaVersion) {
return nil, fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion) return nil, fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion)
} }
@@ -460,7 +469,7 @@ func (txs *TxsBinary) MarshalBinary() ([]byte, error) {
if txs == nil { if txs == nil {
return nil, fmt.Errorf("txs binary is nil") return nil, fmt.Errorf("txs binary is nil")
} }
if txs.SchemaVersion != txBinarySchemaVersionCurrent { if !txBinarySchemaVersionSupported(txs.SchemaVersion) {
return nil, fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion) return nil, fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion)
} }
@@ -478,8 +487,11 @@ func (txs *TxsBinary) MarshalBinary() ([]byte, error) {
} }
enc.writeUint32(uint32(len(txs.Txs))) enc.writeUint32(uint32(len(txs.Txs)))
for i := range txs.Txs { for i := range txs.Txs {
if err := enc.writeTxBinaryBody(&txs.Txs[i], enumTable); err != nil { tx := txs.Txs[i]
return nil, fmt.Errorf("tx[%d], %s: %w", i, base58.Encode(txs.Txs[i].TxHash[:]), err) tx.SchemaVersion = txs.SchemaVersion
tx.EnumVersion = txs.EnumVersion
if err := enc.writeTxBinaryBody(&tx, enumTable); err != nil {
return nil, fmt.Errorf("tx[%d], %s: %w", i, base58.Encode(tx.TxHash[:]), err)
} }
} }
return enc.bytes(), nil return enc.bytes(), nil
@@ -520,7 +532,7 @@ func (tx *TxBinary) UnmarshalBinary(data []byte) error {
if err != nil { if err != nil {
return err return err
} }
if tx.SchemaVersion != txBinarySchemaVersionCurrent { if !txBinarySchemaVersionSupported(tx.SchemaVersion) {
return fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion) return fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion)
} }
@@ -560,7 +572,7 @@ func (txs *TxsBinary) UnmarshalBinary(data []byte) error {
if err != nil { if err != nil {
return err return err
} }
if txs.SchemaVersion != txBinarySchemaVersionCurrent { if !txBinarySchemaVersionSupported(txs.SchemaVersion) {
return fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion) return fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion)
} }
@@ -613,6 +625,7 @@ func (tx *TxBinary) ToTx() (*Tx, error) {
Signer: signer, Signer: signer,
Block: tx.Block, Block: tx.Block,
BlockIndex: tx.BlockIndex, BlockIndex: tx.BlockIndex,
BlockAt: tx.BlockAt,
CuFee: decimal.NewFromUint64(tx.CuFee), CuFee: decimal.NewFromUint64(tx.CuFee),
CUPrice: decimal.NewFromUint64(tx.CUPrice).Shift(-txBinaryCUPriceScale), CUPrice: decimal.NewFromUint64(tx.CUPrice).Shift(-txBinaryCUPriceScale),
BeforeSolBalance: txBinaryFloat64ToDecimal(tx.BeforeSolBalance, txBinarySOLScale), BeforeSolBalance: txBinaryFloat64ToDecimal(tx.BeforeSolBalance, txBinarySOLScale),
@@ -1166,6 +1179,9 @@ func (enc *txBinaryEncoder) writeTxBinaryBody(tx *TxBinary, enumTable *txBinaryE
enc.writeUint32(tx.Signer) enc.writeUint32(tx.Signer)
enc.writeUint64(tx.Block) enc.writeUint64(tx.Block)
enc.writeUint64(tx.BlockIndex) enc.writeUint64(tx.BlockIndex)
if tx.SchemaVersion >= txBinarySchemaVersionCurrent {
enc.writeUint64(uint64(tx.BlockAt))
}
enc.writeBool(tx.TxHash != nil) enc.writeBool(tx.TxHash != nil)
if tx.TxHash != nil { if tx.TxHash != nil {
enc.writeBytes(tx.TxHash[:]) enc.writeBytes(tx.TxHash[:])
@@ -1191,7 +1207,7 @@ func (enc *txBinaryEncoder) writeTxBinaryBody(tx *TxBinary, enumTable *txBinaryE
func (enc *txBinaryEncoder) writePlatformEntries(entries []PlatformBinary, enumTable *txBinaryEnumTable) error { func (enc *txBinaryEncoder) writePlatformEntries(entries []PlatformBinary, enumTable *txBinaryEnumTable) error {
enc.writeUint32(uint32(len(entries))) enc.writeUint32(uint32(len(entries)))
for i, entry := range entries { for i, entry := range entries {
enumID, err := enumTable.platforms.id(entry.Platform) enumID, err := enumTable.platforms.idOrFallback(entry.Platform, PlatformNone)
if err != nil { if err != nil {
return fmt.Errorf("platform[%d]: %w", i, err) return fmt.Errorf("platform[%d]: %w", i, err)
} }
@@ -1204,7 +1220,7 @@ func (enc *txBinaryEncoder) writePlatformEntries(entries []PlatformBinary, enumT
func (enc *txBinaryEncoder) writeMevAgentEntries(entries []MevAgentBinary, enumTable *txBinaryEnumTable) error { func (enc *txBinaryEncoder) writeMevAgentEntries(entries []MevAgentBinary, enumTable *txBinaryEnumTable) error {
enc.writeUint32(uint32(len(entries))) enc.writeUint32(uint32(len(entries)))
for i, entry := range entries { for i, entry := range entries {
enumID, err := enumTable.mevAgents.id(entry.MevAgent) enumID, err := enumTable.mevAgents.idOrFallback(entry.MevAgent, MevAgentUnknown)
if err != nil { if err != nil {
return fmt.Errorf("mev_agent[%d]: %w", i, err) return fmt.Errorf("mev_agent[%d]: %w", i, err)
} }
@@ -1474,7 +1490,7 @@ func (dec *txBinaryStreamDecoder) readTxsBinaryHeader() (*txsBinaryHeader, error
if err != nil { if err != nil {
return nil, err return nil, err
} }
if schemaVersion != txBinarySchemaVersionCurrent { if !txBinarySchemaVersionSupported(schemaVersion) {
return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion) return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion)
} }
@@ -1531,7 +1547,7 @@ func (dec *txBinaryStreamDecoder) readTxsBinaryHeaderOrEOF() (*txsBinaryHeader,
if err != nil { if err != nil {
return nil, err return nil, err
} }
if schemaVersion != txBinarySchemaVersionCurrent { if !txBinarySchemaVersionSupported(schemaVersion) {
return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion) return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion)
} }
@@ -1592,7 +1608,7 @@ func txBinaryReadPlatformEntries(dec txBinaryBodyReader, enumTable *txBinaryEnum
if err != nil { if err != nil {
return nil, err return nil, err
} }
platform, err := enumTable.platforms.value(enumID) platform, err := enumTable.platforms.valueOrFallback(enumID, PlatformNone)
if err != nil { if err != nil {
return nil, fmt.Errorf("platform[%d]: %w", i, err) return nil, fmt.Errorf("platform[%d]: %w", i, err)
} }
@@ -1619,7 +1635,7 @@ func txBinaryReadMevAgentEntries(dec txBinaryBodyReader, enumTable *txBinaryEnum
if err != nil { if err != nil {
return nil, err return nil, err
} }
mevAgent, err := enumTable.mevAgents.value(enumID) mevAgent, err := enumTable.mevAgents.valueOrFallback(enumID, MevAgentUnknown)
if err != nil { if err != nil {
return nil, fmt.Errorf("mev_agent[%d]: %w", i, err) return nil, fmt.Errorf("mev_agent[%d]: %w", i, err)
} }
@@ -1799,6 +1815,13 @@ func txBinaryReadTxBody(dec txBinaryBodyReader, tx *TxBinary, enumTable *txBinar
if tx.BlockIndex, err = dec.readUint64(); err != nil { if tx.BlockIndex, err = dec.readUint64(); err != nil {
return err return err
} }
if tx.SchemaVersion >= txBinarySchemaVersionCurrent {
blockAt, err := dec.readUint64()
if err != nil {
return err
}
tx.BlockAt = int64(blockAt)
}
hasTxHash, err := dec.readBool() hasTxHash, err := dec.readBool()
if err != nil { if err != nil {
@@ -1854,7 +1877,9 @@ func txBinaryBuildMergePlan(sources []TxsBinaryReaderSource, opts TxsBinaryMerge
builder := txBinaryAddressTableBuilder{ builder := txBinaryAddressTableBuilder{
index: make(map[solana.PublicKey]struct{}), index: make(map[solana.PublicKey]struct{}),
} }
plan := &txsBinaryMergePlan{} plan := &txsBinaryMergePlan{
schemaVersion: txBinarySchemaVersionCurrent,
}
hasBatch := false hasBatch := false
for sourceIndex, source := range sources { for sourceIndex, source := range sources {
@@ -1896,15 +1921,10 @@ func txBinaryBuildMergePlan(sources []TxsBinaryReaderSource, opts TxsBinaryMerge
} }
if !hasBatch { if !hasBatch {
plan.schemaVersion = header.schemaVersion
plan.enumVersion = header.enumVersion plan.enumVersion = header.enumVersion
plan.enumTable = header.enumTable plan.enumTable = header.enumTable
hasBatch = true hasBatch = true
} else { } else {
if header.schemaVersion != plan.schemaVersion {
reader.Close()
return nil, fmt.Errorf("source[%d].batch[%d]: schema version mismatch: got %d want %d", sourceIndex, batchIndex, header.schemaVersion, plan.schemaVersion)
}
if header.enumVersion != plan.enumVersion { if header.enumVersion != plan.enumVersion {
reader.Close() reader.Close()
return nil, fmt.Errorf("source[%d].batch[%d]: enum version mismatch: got %d want %d", sourceIndex, batchIndex, header.enumVersion, plan.enumVersion) return nil, fmt.Errorf("source[%d].batch[%d]: enum version mismatch: got %d want %d", sourceIndex, batchIndex, header.enumVersion, plan.enumVersion)
@@ -2143,6 +2163,8 @@ var txBinaryEnumTables = map[uint16]*txBinaryEnumTable{
MevAgentSpeedlanding, MevAgentSpeedlanding,
MevAgentAllenhark, MevAgentAllenhark,
MevAgentRaiden, MevAgentRaiden,
MevAgentZan,
MevAgentTunneling,
}, },
), ),
} }
@@ -2198,9 +2220,30 @@ func (set txBinaryEnumSet) id(value string) (uint16, error) {
return id, nil 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) { func (set txBinaryEnumSet) value(id uint16) (string, error) {
if int(id) >= len(set.values) { if int(id) >= len(set.values) {
return "", fmt.Errorf("unknown %s enum id %d", set.name, id) return "", fmt.Errorf("unknown %s enum id %d", set.name, id)
} }
return set.values[id], nil 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
}

View File

@@ -19,6 +19,7 @@ func TestTxBinaryRoundTrip(t *testing.T) {
Signer: mustPubKey("So11111111111111111111111111111111111111112"), Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 123456789, Block: 123456789,
BlockIndex: 42, BlockIndex: 42,
BlockAt: 1710000000,
TxHash: &txHash, TxHash: &txHash,
CuFee: decimal.NewFromInt(5000), CuFee: decimal.NewFromInt(5000),
CUPrice: decimal.RequireFromString("0.123456"), CUPrice: decimal.RequireFromString("0.123456"),
@@ -41,6 +42,14 @@ func TestTxBinaryRoundTrip(t *testing.T) {
MevAgent: MevAgentJito, MevAgent: MevAgentJito,
MevAgentFee: decimal.RequireFromString("0.030000000"), MevAgentFee: decimal.RequireFromString("0.030000000"),
}, },
MevAgentZan: {
MevAgent: MevAgentZan,
MevAgentFee: decimal.RequireFromString("0.040000000"),
},
MevAgentTunneling: {
MevAgent: MevAgentTunneling,
MevAgentFee: decimal.RequireFromString("0.050000000"),
},
}, },
Swaps: []Swap{ Swaps: []Swap{
{ {
@@ -110,6 +119,9 @@ func TestTxBinaryRoundTrip(t *testing.T) {
if decoded.BlockIndex != original.BlockIndex { if decoded.BlockIndex != original.BlockIndex {
t.Fatalf("BlockIndex = %d, want %d", decoded.BlockIndex, original.BlockIndex) t.Fatalf("BlockIndex = %d, want %d", decoded.BlockIndex, original.BlockIndex)
} }
if decoded.BlockAt != original.BlockAt {
t.Fatalf("BlockAt = %d, want %d", decoded.BlockAt, original.BlockAt)
}
if decoded.TxHash == nil { if decoded.TxHash == nil {
t.Fatal("TxHash = nil, want non-nil") t.Fatal("TxHash = nil, want non-nil")
} }
@@ -146,6 +158,12 @@ func TestTxBinaryRoundTrip(t *testing.T) {
if !decoded.MevAgent[MevAgentJito].MevAgentFee.Equal(original.MevAgent[MevAgentJito].MevAgentFee) { if !decoded.MevAgent[MevAgentJito].MevAgentFee.Equal(original.MevAgent[MevAgentJito].MevAgentFee) {
t.Fatalf("MevAgent fee mismatch") 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 { if len(decoded.Swaps) != 1 {
t.Fatalf("Swaps len = %d, want 1", len(decoded.Swaps)) t.Fatalf("Swaps len = %d, want 1", len(decoded.Swaps))
} }
@@ -225,6 +243,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) { func TestTxBinaryAcceptsKnownEventEnums(t *testing.T) {
events := []string{ events := []string{
TxEventAddLP, TxEventAddLP,
@@ -415,6 +514,7 @@ func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
Signer: mustPubKey("So11111111111111111111111111111111111111112"), Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 1, Block: 1,
BlockIndex: 1, BlockIndex: 1,
BlockAt: 1710000001,
CuFee: decimal.NewFromInt(1000), CuFee: decimal.NewFromInt(1000),
CUPrice: decimal.RequireFromString("0.123456"), CUPrice: decimal.RequireFromString("0.123456"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"), BeforeSolBalance: decimal.RequireFromString("1.000000000"),
@@ -465,6 +565,7 @@ func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
tx2 := tx1 tx2 := tx1
tx2.Block = 2 tx2.Block = 2
tx2.BlockIndex = 2 tx2.BlockIndex = 2
tx2.BlockAt = 1710000002
tx2.CuFee = decimal.NewFromInt(2000) tx2.CuFee = decimal.NewFromInt(2000)
tx2.AfterSOLBalance = decimal.RequireFromString("0.700000000") tx2.AfterSOLBalance = decimal.RequireFromString("0.700000000")
tx2.Swaps = []Swap{tx1.Swaps[0]} tx2.Swaps = []Swap{tx1.Swaps[0]}
@@ -486,6 +587,9 @@ func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
if decoded[0].Signer != tx1.Signer || decoded[1].Signer != tx2.Signer { if decoded[0].Signer != tx1.Signer || decoded[1].Signer != tx2.Signer {
t.Fatalf("decoded signer mismatch") t.Fatalf("decoded signer mismatch")
} }
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt {
t.Fatalf("decoded block_at mismatch")
}
if decoded[0].Swaps[0].Pool != tx1.Swaps[0].Pool || decoded[1].Swaps[0].Pool != tx2.Swaps[0].Pool { if decoded[0].Swaps[0].Pool != tx1.Swaps[0].Pool || decoded[1].Swaps[0].Pool != tx2.Swaps[0].Pool {
t.Fatalf("decoded shared address mismatch") t.Fatalf("decoded shared address mismatch")
} }
@@ -508,6 +612,7 @@ func TestDecodeTxsBinaryReader(t *testing.T) {
Signer: mustPubKey("So11111111111111111111111111111111111111112"), Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 100, Block: 100,
BlockIndex: 7, BlockIndex: 7,
BlockAt: 1710000100,
CuFee: decimal.NewFromInt(111), CuFee: decimal.NewFromInt(111),
CUPrice: decimal.RequireFromString("0.123456"), CUPrice: decimal.RequireFromString("0.123456"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"), BeforeSolBalance: decimal.RequireFromString("1.000000000"),
@@ -554,6 +659,7 @@ func TestDecodeTxsBinaryReader(t *testing.T) {
tx2 := tx1 tx2 := tx1
tx2.Block = 101 tx2.Block = 101
tx2.BlockIndex = 8 tx2.BlockIndex = 8
tx2.BlockAt = 1710000101
tx2.CuFee = decimal.NewFromInt(222) tx2.CuFee = decimal.NewFromInt(222)
tx2.AfterSOLBalance = decimal.RequireFromString("0.300000000") tx2.AfterSOLBalance = decimal.RequireFromString("0.300000000")
tx2.Swaps = []Swap{tx1.Swaps[0]} tx2.Swaps = []Swap{tx1.Swaps[0]}
@@ -582,6 +688,9 @@ func TestDecodeTxsBinaryReader(t *testing.T) {
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block { if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
t.Fatalf("decoded block mismatch") t.Fatalf("decoded block mismatch")
} }
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt {
t.Fatalf("decoded block_at mismatch")
}
if decoded[0].Swaps[0].BaseAmount.Cmp(tx1.Swaps[0].BaseAmount) != 0 { if decoded[0].Swaps[0].BaseAmount.Cmp(tx1.Swaps[0].BaseAmount) != 0 {
t.Fatalf("decoded tx1 swap base amount = %s, want %s", decoded[0].Swaps[0].BaseAmount, tx1.Swaps[0].BaseAmount) t.Fatalf("decoded tx1 swap base amount = %s, want %s", decoded[0].Swaps[0].BaseAmount, tx1.Swaps[0].BaseAmount)
} }
@@ -629,6 +738,7 @@ func TestMergeTxsBinaryBytes(t *testing.T) {
Signer: mustPubKey("So11111111111111111111111111111111111111112"), Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 11, Block: 11,
BlockIndex: 1, BlockIndex: 1,
BlockAt: 1710000011,
CuFee: decimal.NewFromInt(10), CuFee: decimal.NewFromInt(10),
CUPrice: decimal.RequireFromString("0.000123"), CUPrice: decimal.RequireFromString("0.000123"),
BeforeSolBalance: decimal.RequireFromString("1.100000000"), BeforeSolBalance: decimal.RequireFromString("1.100000000"),
@@ -660,6 +770,7 @@ func TestMergeTxsBinaryBytes(t *testing.T) {
Signer: mustPubKey("SysvarRent111111111111111111111111111111111"), Signer: mustPubKey("SysvarRent111111111111111111111111111111111"),
Block: 12, Block: 12,
BlockIndex: 2, BlockIndex: 2,
BlockAt: 1710000012,
CuFee: decimal.NewFromInt(20), CuFee: decimal.NewFromInt(20),
CUPrice: decimal.RequireFromString("0.000456"), CUPrice: decimal.RequireFromString("0.000456"),
BeforeSolBalance: decimal.RequireFromString("2.200000000"), BeforeSolBalance: decimal.RequireFromString("2.200000000"),
@@ -723,6 +834,9 @@ func TestMergeTxsBinaryBytes(t *testing.T) {
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block { if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
t.Fatalf("decoded block mismatch") t.Fatalf("decoded block mismatch")
} }
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt {
t.Fatalf("decoded block_at mismatch")
}
} }
func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) { func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
@@ -730,6 +844,7 @@ func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
Signer: mustPubKey("So11111111111111111111111111111111111111112"), Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 21, Block: 21,
BlockIndex: 1, BlockIndex: 1,
BlockAt: 1710000021,
CuFee: decimal.NewFromInt(1), CuFee: decimal.NewFromInt(1),
CUPrice: decimal.RequireFromString("0.000001"), CUPrice: decimal.RequireFromString("0.000001"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"), BeforeSolBalance: decimal.RequireFromString("1.000000000"),
@@ -740,9 +855,11 @@ func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
tx2 := tx1 tx2 := tx1
tx2.Block = 22 tx2.Block = 22
tx2.BlockIndex = 2 tx2.BlockIndex = 2
tx2.BlockAt = 1710000022
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111") tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
tx3 := tx1 tx3 := tx1
tx3.Block = 23 tx3.Block = 23
tx3.BlockAt = 1710000023
tx3.BlockIndex = 3 tx3.BlockIndex = 3
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111") tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
@@ -785,6 +902,9 @@ func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block || decoded[2].Block != tx3.Block { if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block || decoded[2].Block != tx3.Block {
t.Fatalf("decoded block order mismatch") t.Fatalf("decoded block order mismatch")
} }
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt || decoded[2].BlockAt != tx3.BlockAt {
t.Fatalf("decoded block_at order mismatch")
}
} }
func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) { func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) {
@@ -792,6 +912,7 @@ func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) {
Signer: mustPubKey("So11111111111111111111111111111111111111112"), Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 31, Block: 31,
BlockIndex: 1, BlockIndex: 1,
BlockAt: 1710000031,
CuFee: decimal.NewFromInt(1), CuFee: decimal.NewFromInt(1),
CUPrice: decimal.RequireFromString("0.000001"), CUPrice: decimal.RequireFromString("0.000001"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"), BeforeSolBalance: decimal.RequireFromString("1.000000000"),
@@ -802,10 +923,12 @@ func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) {
tx2 := tx1 tx2 := tx1
tx2.Block = 32 tx2.Block = 32
tx2.BlockIndex = 2 tx2.BlockIndex = 2
tx2.BlockAt = 1710000032
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111") tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
tx3 := tx1 tx3 := tx1
tx3.Block = 33 tx3.Block = 33
tx3.BlockIndex = 3 tx3.BlockIndex = 3
tx3.BlockAt = 1710000033
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111") tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
batch1, err := EncodeTxsBinary([]Tx{tx1}) batch1, err := EncodeTxsBinary([]Tx{tx1})
@@ -865,15 +988,127 @@ func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) {
if decoded[0].Block != tx1.Block || decoded[1].Block != tx3.Block { if decoded[0].Block != tx1.Block || decoded[1].Block != tx3.Block {
t.Fatalf("decoded block order mismatch after skip") t.Fatalf("decoded block order mismatch after skip")
} }
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx3.BlockAt {
t.Fatalf("decoded block_at order mismatch after skip")
}
if source.opens != 2 { if source.opens != 2 {
t.Fatalf("source.opens = %d, want 2", source.opens) t.Fatalf("source.opens = %d, want 2", source.opens)
} }
} }
func TestTxBinaryDecodeSchemaV3LeavesBlockAtZero(t *testing.T) {
original := &Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 41,
BlockIndex: 1,
BlockAt: 1710000041,
}
encoded := mustEncodeTxBinaryV3(t, original)
decoded, err := DecodeTxBinary(encoded)
if err != nil {
t.Fatalf("DecodeTxBinary(v3) error = %v", err)
}
if decoded.Block != original.Block || decoded.BlockIndex != original.BlockIndex {
t.Fatalf("decoded block mismatch: got (%d,%d), want (%d,%d)", decoded.Block, decoded.BlockIndex, original.Block, original.BlockIndex)
}
if decoded.BlockAt != 0 {
t.Fatalf("BlockAt = %d, want 0 for legacy v3", decoded.BlockAt)
}
}
func TestMergeTxsBinaryBytesUpgradesSchemaV3AndPreservesV4BlockAt(t *testing.T) {
legacyTx := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 51,
BlockIndex: 1,
BlockAt: 1710000051,
}
currentTx := Tx{
Signer: mustPubKey("SysvarRent111111111111111111111111111111111"),
Block: 52,
BlockIndex: 2,
BlockAt: 1710000052,
}
merged, err := MergeTxsBinaryBytes([][]byte{
mustEncodeTxsBinaryV3(t, []Tx{legacyTx}),
mustEncodeTxsBinary(t, []Tx{currentTx}),
})
if err != nil {
t.Fatalf("MergeTxsBinaryBytes(v3,v4) error = %v", err)
}
var mergedBinary TxsBinary
if err := mergedBinary.UnmarshalBinary(merged); err != nil {
t.Fatalf("UnmarshalBinary(merged) error = %v", err)
}
if mergedBinary.SchemaVersion != txBinarySchemaVersionCurrent {
t.Fatalf("merged schema version = %d, want %d", mergedBinary.SchemaVersion, txBinarySchemaVersionCurrent)
}
decoded, err := DecodeTxsBinary(merged)
if err != nil {
t.Fatalf("DecodeTxsBinary(merged) error = %v", err)
}
if len(decoded) != 2 {
t.Fatalf("decoded len = %d, want 2", len(decoded))
}
if decoded[0].BlockAt != 0 {
t.Fatalf("legacy BlockAt = %d, want 0", decoded[0].BlockAt)
}
if decoded[1].BlockAt != currentTx.BlockAt {
t.Fatalf("current BlockAt = %d, want %d", decoded[1].BlockAt, currentTx.BlockAt)
}
}
func mustPubKey(value string) solana.PublicKey { func mustPubKey(value string) solana.PublicKey {
return solana.MustPublicKeyFromBase58(value) return solana.MustPublicKeyFromBase58(value)
} }
func mustEncodeTxBinaryV3(t *testing.T, tx *Tx) []byte {
t.Helper()
binaryTx, err := NewTxBinary(tx)
if err != nil {
t.Fatalf("NewTxBinary() error = %v", err)
}
binaryTx.SchemaVersion = txBinarySchemaVersionV3
encoded, err := binaryTx.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary(v3) error = %v", err)
}
return encoded
}
func mustEncodeTxsBinary(t *testing.T, txs []Tx) []byte {
t.Helper()
encoded, err := EncodeTxsBinary(txs)
if err != nil {
t.Fatalf("EncodeTxsBinary() error = %v", err)
}
return encoded
}
func mustEncodeTxsBinaryV3(t *testing.T, txs []Tx) []byte {
t.Helper()
binaryTxs, err := NewTxsBinary(txs)
if err != nil {
t.Fatalf("NewTxsBinary() error = %v", err)
}
binaryTxs.SchemaVersion = txBinarySchemaVersionV3
for i := range binaryTxs.Txs {
binaryTxs.Txs[i].SchemaVersion = txBinarySchemaVersionV3
}
encoded, err := binaryTxs.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary(v3) error = %v", err)
}
return encoded
}
func mustTxBinary(t *testing.T, data []byte) *TxsBinary { func mustTxBinary(t *testing.T, data []byte) *TxsBinary {
t.Helper() t.Helper()