Compare commits

..

31 Commits

Author SHA1 Message Date
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
thloyi
5cd3a97d81 pump usdc support 2026-05-08 11:21:30 +08:00
thloyi
0a4aabc67f raw tx binary 2026-04-30 17:56:35 +08:00
thloyi
d46e8b651c raw tx binary 2026-04-30 17:02:04 +08:00
thloyi
43659ea4e4 fix pump amm quoteAmountIn 2026-04-27 14:36:03 +08:00
thloyi
6414e6a25f rawtx binary 2026-04-24 18:00:44 +08:00
thloyi
273e87b8ad fix ignore failed metaora swap 2026-04-22 11:16:26 +08:00
thloyi
bb858c643e fix orcawhirpool int64 overflow 2026-04-22 11:10:46 +08:00
thloyi
a620df5837 fix pump parser 2026-04-21 14:18:42 +08:00
thloyi
36da96eeaf no two hp swap slippage 2026-04-20 16:31:18 +08:00
thloyi
a765fafddd fix pump parser 2026-04-20 16:26:55 +08:00
thloyi
738e417167 fix EncodeTxBinary 2026-04-20 15:25:08 +08:00
thloyi
51f1511c8f fix EncodeTxBinary 2026-04-20 15:09:42 +08:00
thloyi
7dfe003e5b fix event enum 2026-04-20 14:16:20 +08:00
thloyi
fe94888b14 fix slippage 2026-04-20 12:31:30 +08:00
thloyi
1dd843c393 batch encode opts 2026-04-16 17:56:17 +08:00
thloyi
d2879efcc6 batch encode 2026-04-16 16:40:40 +08:00
thloyi
e761fd6f84 swap amount input 2026-04-16 14:24:14 +08:00
thloyi
ab0e87a48a fix raydium v4 swap v2 2026-04-16 11:39:15 +08:00
bijianing97
fb8d93f426 Update dlmm fee 2026-04-11 08:34:21 +08:00
bijianing97
0cc843b370 Update dlmm fee 2026-04-11 08:27:34 +08:00
bijianing97
d9a214b4b4 Add dlmm add liquidity one side function 2026-03-25 11:34:46 +08:00
54 changed files with 11620 additions and 1355 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.

156
SLIPPAGE_MAPPING.md Normal file
View File

@@ -0,0 +1,156 @@
# Slippage Mapping
This document describes how `SlippageBps` is derived for each supported swap protocol in this repository.
## Unified Fields
Each parsed `Swap` may include these normalized fields:
- `SwapMode`
- `FixedAmount`
- `FixedAmountSide`
- `FixedMint`
- `LimitAmountType`
- `LimitAmount`
- `LimitAmountSide`
- `LimitMint`
- `ActualLimitAmount`
- `ActualLimitAmountSide`
- `SlippageBps`
## Internal Enum Mapping
These fields are stored internally as `uint8` enums and serialized as strings in JSON / debug output.
### `SwapMode`
| Raw Value | Name | Serialized Value |
| --- | --- | --- |
| `0` | `SwapModeUnknown` | `""` |
| `1` | `SwapModeExactIn` | `"exact_in"` |
| `2` | `SwapModeExactOut` | `"exact_out"` |
### `SwapAmountSide`
Used by:
- `FixedAmountSide`
- `LimitAmountSide`
- `ActualLimitAmountSide`
| Raw Value | Name | Serialized Value |
| --- | --- | --- |
| `0` | `SwapAmountSideUnknown` | `""` |
| `1` | `SwapAmountSideBase` | `"base"` |
| `2` | `SwapAmountSideQuote` | `"quote"` |
### `SwapLimitType`
Used by:
- `LimitAmountType`
| Raw Value | Name | Serialized Value |
| --- | --- | --- |
| `0` | `SwapLimitTypeUnknown` | `""` |
| `1` | `SwapLimitTypeMinOut` | `"min_out"` |
| `2` | `SwapLimitTypeMaxIn` | `"max_in"` |
## Calculation Rules
- `exact_in`
- `SlippageBps = (actual_out - min_out) / actual_out * 10000`
- `exact_out`
- `SlippageBps = (max_in - actual_in) / max_in * 10000`
Interpretation:
- Positive: execution is better than the user limit
- Zero: execution lands exactly on the user limit
- `10000`: user limit is effectively unbounded on the constrained side (for example `min_out = 0`)
- Negative raw headroom is clamped to `0` because successful-swap storage uses a non-negative bounded metric
This definition makes `SlippageBps` a bounded "remaining headroom to the user's limit" metric for successful swaps:
- `exact_in`: how much output headroom remained, measured against the realized output
- `exact_out`: how much input headroom remained, measured against the allowed max input
## Protocol Mapping
| Protocol | Method Semantics | `SwapMode` | `FixedAmount` | `LimitAmountType` | `LimitAmount` | `ActualLimitAmount` |
| --- | --- | --- | --- | --- | --- | --- |
| `Pump` | `buy` | `exact_out` | target token amount | `max_in` | max SOL in | actual SOL in |
| `Pump` | `buy_exact_sol_in` | `exact_in` | SOL in | `min_out` | min token out | actual token out |
| `Pump` | `sell` | `exact_in` | token in | `min_out` | min SOL out | actual SOL out |
| `PumpAMM` | `buy` | `exact_out` | target base out | `max_in` | max quote in | actual quote in |
| `PumpAMM` | `buy_exact_quote_in` | `exact_in` | quote in | `min_out` | min base out | actual base out |
| `PumpAMM` | `sell` | `exact_in` | base in | `min_out` | min quote out | actual quote out |
| `MeteoraDLMM` | `swap` / `swap2` / `swap_with_price_impact` | `exact_in` | `AmountIn` | `min_out` | instruction min out | event output |
| `MeteoraDLMM` | `swap_exact_out` / `swap_exact_out2` | `exact_out` | `OutAmount` | `max_in` | `MaxInAmount` | event input |
| `MeteoraPools` | `swap` | `exact_in` | `InAmount` | `min_out` | `MinimumOutAmount` | actual output side |
| `MeteoraBondingCurve` | `swap` / `swap2` | `exact_in` | `AmountIn` | `min_out` | `MinimumAmountOut` | actual output side |
| `MeteoraAmmV2` | `swap` / `swap2` exact-in or partial | `exact_in` | params input side | `min_out` | params output threshold | actual output side |
| `MeteoraAmmV2` | `swap` / `swap2` exact-out | `exact_out` | params target output | `max_in` | params max input | actual input side |
| `RaydiumLaunchLab` | `*_ExactIn` | `exact_in` | `Amount` | `min_out` | `OtherAmountThreshold` | actual output side |
| `RaydiumLaunchLab` | `*_ExactOut` | `exact_out` | `Amount` | `max_in` | `OtherAmountThreshold` | actual input side |
| `RaydiumCPMM` | `swap_base_input` | `exact_in` | `AmountIn` | `min_out` | `MinimumAmountOut` | actual output side |
| `RaydiumCPMM` | `swap_base_output` | `exact_out` | `AmountOut` | `max_in` | `MaxAmountIn` | actual input side |
| `RaydiumCLMM` | `swap` / `swap_v2` | `exact_in` or `exact_out` | `amount` | `min_out` or `max_in` | `other_amount_threshold` | opposite-side actual amount |
| `RaydiumV4` | `swap_base_in` / `swap_base_in_v2` | `exact_in` | `amount_in` | `min_out` | `minimum_amount_out` | actual output side |
| `RaydiumV4` | `swap_base_out` / `swap_base_out_v2` | `exact_out` | `amount_out` | `max_in` | `max_amount_in` | actual input side |
| `OrcaWhirlpool` | `swap` / `swap_v2` | `exact_in` or `exact_out` | `amount` | `min_out` or `max_in` | `other_amount_threshold` | opposite-side actual amount |
| `OrcaWhirlpool` | `two_hop_swap` / `two_hop_swap_v2` | route-level | route specified amount | `min_out` or `max_in` | route threshold | route final output or total input |
## Notes
- `Pump` quote side is normalized to `wSOL` in the slippage fields, even when legacy `Swap.QuoteMint` is not populated.
- `OrcaWhirlpool` two-hop instructions use route-level slippage. The normalized slippage fields are attached to the first returned swap entry.
- `MeteoraAmmV2` uses `SwapMode.ExactIn`, `SwapMode.PartialFill`, and `SwapMode.ExactOut`. `PartialFill` is treated like exact-in for slippage purposes because it still uses a minimum-output threshold.
## DAMM v2 Verification
The `MeteoraAmmV2` mapping has been checked against the program IDL for `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG`.
- `swap`
- instruction arg type: `SwapParameters`
- fields: `amountIn`, `minimumAmountOut`
- semantics: exact-in
- `swap2`:
- instruction / event arg type: `SwapParameters2`
- `amount0`: "When it's exact in, partial fill, this will be amount_in. When it's exact out, this will be amount_out"
- `amount1`: "When it's exact in, partial fill, this will be minimum_amount_out. When it's exact out, this will be maximum_amount_in"
- `swapMode`: `ExactIn`, `PartialFill`, `ExactOut`
The downloaded JSON IDL references `SwapMode` in the field docs but does not inline the enum body itself. In this repository, the raw `swapMode` values are interpreted consistently as:
- `0 = ExactIn`
- `1 = PartialFill`
- `2 = ExactOut`
That means the parser mapping is:
- `swap2` + `ExactIn` / `PartialFill`
- `FixedAmount = amount0`
- `LimitAmount = amount1`
- `LimitAmountType = min_out`
- `swap2` + `ExactOut`
- `FixedAmount = amount0`
- `LimitAmount = amount1`
- `LimitAmountType = max_in`
## Source Files
- `Swap` normalized fields: `tx.go`
- Shared slippage mapping helpers: `swap_amounts.go`
- Protocol parsers:
- `pump.go`
- `pumpamm.go`
- `metaoradlmm.go`
- `metaorapool.go`
- `meteora_bonding_curve.go`
- `meteoradamm.go`
- `raydiumlaunchlab.go`
- `raydiumcpmm.go`
- `raydiumclmm.go`
- `raydiumv4.go`
- `orcawhirpool.go`

View File

@@ -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
}

View File

@@ -0,0 +1,594 @@
package main
import (
"flag"
"fmt"
"math/big"
"os"
"sort"
pump_parser "github.com/thloyi/pump-parser"
)
type sizeStats struct {
total uint64
items map[string]uint64
}
type txInnerDataStat struct {
TxOrdinal int
BlockIndex int
IndexWithinBlock uint32
Slot uint64
Bytes uint64
InstructionCount int
}
func newSizeStats() *sizeStats {
return &sizeStats{items: make(map[string]uint64)}
}
func (s *sizeStats) add(name string, n uint64) {
s.items[name] += n
s.total += n
}
func main() {
filePath := flag.String("file", "testdata/rawtx-binary/rawtx-blocks-414696178-414696182.prbs", "path to RawTxBlocksBinary .prbs file")
flag.Parse()
raw, err := os.ReadFile(*filePath)
if err != nil {
fmt.Fprintf(os.Stderr, "read file: %v\n", err)
os.Exit(1)
}
var blocks pump_parser.RawTxBlocksBinary
if err := blocks.UnmarshalBinary(raw); err != nil {
fmt.Fprintf(os.Stderr, "decode rawtx blocks binary: %v\n", err)
os.Exit(1)
}
stats := analyzeRawTxBlocksBinary(&blocks)
if stats.total != uint64(len(raw)) {
fmt.Fprintf(os.Stderr, "size accounting mismatch: accounted=%d file=%d\n", stats.total, len(raw))
os.Exit(1)
}
printReport(*filePath, len(raw), &blocks, stats)
fmt.Println()
printInnerInstructionDataDistribution(&blocks)
fmt.Println()
printBalanceAnalysis(&blocks)
}
func analyzeRawTxBlocksBinary(blocks *pump_parser.RawTxBlocksBinary) *sizeStats {
stats := newSizeStats()
stats.add("file.magic", 4)
stats.add("file.schema_version", 2)
stats.add("address_table.count", 4)
stats.add("address_table.pubkeys", uint64(len(blocks.AddressTable))*32)
stats.add("blocks.count", 4)
stats.add("blocks.block_time", uint64(len(blocks.BlockTimes))*8)
stats.add("blocks.tx_count", uint64(len(blocks.BlockTxCounts))*4)
stats.add("txs.total_count", 4)
for i := range blocks.Txs {
addTx(stats, &blocks.Txs[i])
}
return stats
}
func addTx(stats *sizeStats, tx *pump_parser.RawTxBinary) {
stats.add("tx.index_within_block", 4)
stats.add("tx.slot", 8)
stats.add("tx.version", 1)
stats.add("tx.account_key_count", 4)
stats.add("tx.account_list.count", 4)
stats.add("tx.account_list.refs", uint64(len(tx.AccountList))*4)
addMeta(stats, &tx.Meta)
addTransaction(stats, &tx.Transaction)
}
func addMeta(stats *sizeStats, meta *pump_parser.RawTxMetaBinary) {
addErr(stats, meta.Err)
stats.add("meta.fee", 8)
addInnerInstructions(stats, meta.InnerInstructions)
addLamportBalances(stats, meta.PreBalances, meta.PostBalances)
addTokenBalances(stats, "meta.token_balances", meta.TokenBalances)
stats.add("meta.compute_units_consumed", 8)
}
func addErr(stats *sizeStats, errValue *pump_parser.TransactionParsedError) {
stats.add("meta.err.present", 1)
if errValue == nil {
return
}
stats.add("meta.err.index", 1)
stats.add("meta.err.variant", 4)
stats.add("meta.err.enum", 4)
stats.add("meta.err.custom_code", 4)
}
func addTransaction(stats *sizeStats, tx *pump_parser.RawTxTransactionBinary) {
stats.add("transaction.signature.present", 1)
if tx.HasSignature {
stats.add("transaction.signature.first", 64)
}
addHeader(stats)
addInstructions(stats, "transaction.instructions", tx.Message.Instructions)
addAddressTableLookups(stats, tx.Message.AddressTableLookups)
}
func addHeader(stats *sizeStats) {
stats.add("transaction.header.num_readonly_signed", 4)
stats.add("transaction.header.num_readonly_unsigned", 4)
stats.add("transaction.header.num_required_signatures", 4)
}
func addInnerInstructions(stats *sizeStats, values []pump_parser.InnerInstructions) {
stats.add("meta.inner_instructions.count", 4)
for _, value := range values {
stats.add("meta.inner_instructions.index", 4)
addInstructions(stats, "meta.inner_instructions.instructions", value.Instructions)
}
}
func addInstructions(stats *sizeStats, prefix string, values []pump_parser.Instruction) {
stats.add(prefix+".count", 4)
for _, value := range values {
stats.add(prefix+".program_id_index", 1)
stats.add(prefix+".accounts.count", 4)
stats.add(prefix+".accounts.refs", uint64(len(value.Accounts)))
stats.add(prefix+".data.length", 4)
stats.add(prefix+".data.bytes", uint64(len(value.Data)))
stats.add(prefix+".stack_height.present", 1)
if value.StackHeight != nil {
stats.add(prefix+".stack_height.value", 4)
}
stats.add(prefix+".log_events.count", 4)
for _, event := range value.LogEvents {
stats.add(prefix+".log_events.length", 4)
stats.add(prefix+".log_events.bytes", uint64(len(event)))
}
}
}
func addAddressTableLookups(stats *sizeStats, values []pump_parser.RawTxAddressTableLookupBinary) {
stats.add("transaction.address_table_lookups.count", 4)
for _, value := range values {
stats.add("transaction.address_table_lookups.account_key", 4)
stats.add("transaction.address_table_lookups.writable.count", 4)
stats.add("transaction.address_table_lookups.writable.indexes", uint64(len(value.WritableIndexes)))
stats.add("transaction.address_table_lookups.readonly.count", 4)
stats.add("transaction.address_table_lookups.readonly.indexes", uint64(len(value.ReadonlyIndexes)))
}
}
func addUint64Slice(stats *sizeStats, prefix string, count int) {
stats.add(prefix+".count", 4)
stats.add(prefix+".values", uint64(count)*8)
}
func addLamportBalances(stats *sizeStats, preBalances []uint64, postBalances []uint64) {
stats.add("meta.pre_balances.count_uvarint", uint64(uvarintLen(uint64(len(preBalances)))))
for _, value := range preBalances {
stats.add("meta.pre_balances.value_uvarint", uint64(uvarintLen(value)))
}
n := len(preBalances)
if len(postBalances) < n {
n = len(postBalances)
}
changed := 0
for i := 0; i < n; i++ {
if preBalances[i] != postBalances[i] {
changed++
}
}
stats.add("meta.post_balance_changes.count_uvarint", uint64(uvarintLen(uint64(changed))))
for i := 0; i < n; i++ {
if preBalances[i] == postBalances[i] {
continue
}
stats.add("meta.post_balance_changes.index_uvarint", uint64(uvarintLen(uint64(i))))
stats.add("meta.post_balance_changes.delta_uvarint", uint64(zigzagDeltaUvarintLen(preBalances[i], postBalances[i])))
}
}
func addTokenBalances(stats *sizeStats, prefix string, values []pump_parser.RawTxTokenBalanceBinary) {
stats.add(prefix+".count", 4)
for _, value := range values {
stats.add(prefix+".account_index", 1)
stats.add(prefix+".mint_ref", 1)
stats.add(prefix+".owner.present", 1)
if value.HasOwnerAccount {
stats.add(prefix+".owner_ref", 1)
}
stats.add(prefix+".program_id_ref", 1)
stats.add(prefix+".decimals", 1)
stats.add(prefix+".pre_amount.present", 1)
if value.HasPreAmount {
stats.add(prefix+".pre_amount.length", 1)
stats.add(prefix+".pre_amount.bytes", uint64(uint256ByteLen(value.PreAmount)))
}
stats.add(prefix+".post_amount.present", 1)
if value.HasPostAmount {
stats.add(prefix+".post_amount.length", 1)
stats.add(prefix+".post_amount.bytes", uint64(uint256ByteLen(value.PostAmount)))
}
}
}
func uint256ByteLen(value string) int {
if value == "" || value == "0" {
return 0
}
amount, ok := new(big.Int).SetString(value, 10)
if !ok || amount.Sign() <= 0 {
return 0
}
return len(amount.Bytes())
}
func printReport(filePath string, fileSize int, blocks *pump_parser.RawTxBlocksBinary, stats *sizeStats) {
type row struct {
name string
bytes uint64
}
rows := make([]row, 0, len(stats.items))
for name, bytes := range stats.items {
rows = append(rows, row{name: name, bytes: bytes})
}
sort.Slice(rows, func(i, j int) bool {
if rows[i].bytes == rows[j].bytes {
return rows[i].name < rows[j].name
}
return rows[i].bytes > rows[j].bytes
})
fmt.Printf("file=%s\n", filePath)
fmt.Printf("bytes=%d\n", fileSize)
fmt.Printf("schema_version=%d\n", blocks.SchemaVersion)
fmt.Printf("blocks=%d\n", len(blocks.BlockTxCounts))
fmt.Printf("txs=%d\n", len(blocks.Txs))
fmt.Printf("address_table_entries=%d\n", len(blocks.AddressTable))
fmt.Println()
fmt.Printf("%-56s %12s %8s\n", "field", "bytes", "pct")
fmt.Printf("%-56s %12s %8s\n", "-----", "-----", "---")
for _, row := range rows {
fmt.Printf("%-56s %12d %7.2f%%\n", row.name, row.bytes, float64(row.bytes)*100/float64(fileSize))
}
}
func printInnerInstructionDataDistribution(blocks *pump_parser.RawTxBlocksBinary) {
stats := collectInnerInstructionDataStats(blocks)
values := make([]uint64, 0, len(stats))
var total uint64
var nonZero int
for _, stat := range stats {
values = append(values, stat.Bytes)
total += stat.Bytes
if stat.Bytes > 0 {
nonZero++
}
}
sort.Slice(values, func(i, j int) bool { return values[i] < values[j] })
fmt.Println("inner_instruction_data_bytes_per_tx")
fmt.Printf("txs=%d nonzero_txs=%d total_bytes=%d avg=%.2f\n", len(stats), nonZero, total, avg(total, len(stats)))
if len(values) > 0 {
fmt.Printf("min=%d p50=%d p75=%d p90=%d p95=%d p99=%d max=%d\n",
values[0],
percentile(values, 0.50),
percentile(values, 0.75),
percentile(values, 0.90),
percentile(values, 0.95),
percentile(values, 0.99),
values[len(values)-1],
)
}
fmt.Println()
fmt.Printf("%-16s %8s %8s\n", "bucket", "txs", "bytes")
fmt.Printf("%-16s %8s %8s\n", "------", "---", "-----")
for _, bucket := range innerDataBuckets(stats) {
fmt.Printf("%-16s %8d %8d\n", bucket.label, bucket.count, bucket.bytes)
}
sort.Slice(stats, func(i, j int) bool {
if stats[i].Bytes == stats[j].Bytes {
return stats[i].TxOrdinal < stats[j].TxOrdinal
}
return stats[i].Bytes > stats[j].Bytes
})
fmt.Println()
fmt.Printf("%-6s %-5s %-8s %-12s %-8s %-10s\n", "rank", "block", "tx_index", "slot", "bytes", "inner_ix")
fmt.Printf("%-6s %-5s %-8s %-12s %-8s %-10s\n", "----", "-----", "--------", "----", "-----", "--------")
limit := 20
if len(stats) < limit {
limit = len(stats)
}
for i := 0; i < limit; i++ {
stat := stats[i]
fmt.Printf("%-6d %-5d %-8d %-12d %-8d %-10d\n", i+1, stat.BlockIndex, stat.IndexWithinBlock, stat.Slot, stat.Bytes, stat.InstructionCount)
}
}
func collectInnerInstructionDataStats(blocks *pump_parser.RawTxBlocksBinary) []txInnerDataStat {
out := make([]txInnerDataStat, 0, len(blocks.Txs))
txOffset := 0
for blockIndex, count := range blocks.BlockTxCounts {
for i := uint32(0); i < count; i++ {
tx := &blocks.Txs[txOffset]
var bytes uint64
var instructionCount int
for _, inner := range tx.Meta.InnerInstructions {
for _, instruction := range inner.Instructions {
bytes += uint64(len(instruction.Data))
instructionCount++
}
}
out = append(out, txInnerDataStat{
TxOrdinal: txOffset,
BlockIndex: blockIndex,
IndexWithinBlock: tx.IndexWithinBlock,
Slot: tx.Slot,
Bytes: bytes,
InstructionCount: instructionCount,
})
txOffset++
}
}
return out
}
func percentile(values []uint64, p float64) uint64 {
if len(values) == 0 {
return 0
}
idx := int(float64(len(values)-1) * p)
return values[idx]
}
func avg(total uint64, count int) float64 {
if count == 0 {
return 0
}
return float64(total) / float64(count)
}
type innerDataBucket struct {
label string
count int
bytes uint64
}
func innerDataBuckets(stats []txInnerDataStat) []innerDataBucket {
buckets := []innerDataBucket{
{label: "0"},
{label: "1-63"},
{label: "64-127"},
{label: "128-255"},
{label: "256-511"},
{label: "512-1023"},
{label: "1024-2047"},
{label: "2048-4095"},
{label: "4096+"},
}
for _, stat := range stats {
index := 0
switch {
case stat.Bytes == 0:
index = 0
case stat.Bytes < 64:
index = 1
case stat.Bytes < 128:
index = 2
case stat.Bytes < 256:
index = 3
case stat.Bytes < 512:
index = 4
case stat.Bytes < 1024:
index = 5
case stat.Bytes < 2048:
index = 6
case stat.Bytes < 4096:
index = 7
default:
index = 8
}
buckets[index].count++
buckets[index].bytes += stat.Bytes
}
return buckets
}
type balanceValueStats struct {
name string
count int
unique int
zeroCount int
topValues []balanceTopValue
fixedBytes uint64
uvarintBytes uint64
duplicateCount int
}
type balanceTopValue struct {
value uint64
count int
}
type balancePairStats struct {
txCount int
pairCount int
lengthMismatchTxs int
unchangedCount int
changedCount int
currentFixedValueBytes uint64
bothUvarintBytes uint64
preUvarintPostDelta uint64
preUvarintChangedDeltas uint64
}
func printBalanceAnalysis(blocks *pump_parser.RawTxBlocksBinary) {
preValues := make([]uint64, 0)
postValues := make([]uint64, 0)
pairs := balancePairStats{}
pairs.txCount = len(blocks.Txs)
for _, tx := range blocks.Txs {
preValues = append(preValues, tx.Meta.PreBalances...)
postValues = append(postValues, tx.Meta.PostBalances...)
preLen := len(tx.Meta.PreBalances)
postLen := len(tx.Meta.PostBalances)
if preLen != postLen {
pairs.lengthMismatchTxs++
}
n := preLen
if postLen < n {
n = postLen
}
pairs.currentFixedValueBytes += uint64(preLen+postLen) * 8
pairs.preUvarintChangedDeltas += uint64(uvarintLen(uint64(n)))
for i := 0; i < preLen; i++ {
pairs.bothUvarintBytes += uint64(uvarintLen(tx.Meta.PreBalances[i]))
pairs.preUvarintPostDelta += uint64(uvarintLen(tx.Meta.PreBalances[i]))
pairs.preUvarintChangedDeltas += uint64(uvarintLen(tx.Meta.PreBalances[i]))
}
for i := 0; i < postLen; i++ {
pairs.bothUvarintBytes += uint64(uvarintLen(tx.Meta.PostBalances[i]))
}
for i := 0; i < n; i++ {
pre := tx.Meta.PreBalances[i]
post := tx.Meta.PostBalances[i]
pairs.pairCount++
pairs.preUvarintPostDelta += uint64(zigzagDeltaUvarintLen(pre, post))
if pre == post {
pairs.unchangedCount++
continue
}
pairs.changedCount++
pairs.preUvarintChangedDeltas += uint64(uvarintLen(uint64(i)))
pairs.preUvarintChangedDeltas += uint64(zigzagDeltaUvarintLen(pre, post))
}
}
preStats := collectBalanceValueStats("pre_balances", preValues)
postStats := collectBalanceValueStats("post_balances", postValues)
combined := append(append([]uint64(nil), preValues...), postValues...)
combinedStats := collectBalanceValueStats("pre+post_balances", combined)
fmt.Println("balance_values_analysis")
printBalanceValueStats(preStats)
printBalanceValueStats(postStats)
printBalanceValueStats(combinedStats)
fmt.Println()
fmt.Println("balance_encoding_estimates")
fmt.Printf("txs=%d pairs=%d length_mismatch_txs=%d unchanged_pairs=%d changed_pairs=%d unchanged_pct=%.2f%%\n",
pairs.txCount,
pairs.pairCount,
pairs.lengthMismatchTxs,
pairs.unchangedCount,
pairs.changedCount,
float64(pairs.unchangedCount)*100/float64(maxInt(pairs.pairCount, 1)),
)
printEstimate("current_fixed_uint64_values", pairs.currentFixedValueBytes, pairs.currentFixedValueBytes)
printEstimate("both_values_uvarint", pairs.bothUvarintBytes, pairs.currentFixedValueBytes)
printEstimate("pre_uvarint_post_delta_each_index", pairs.preUvarintPostDelta, pairs.currentFixedValueBytes)
printEstimate("pre_uvarint_post_changed_delta_pairs", pairs.preUvarintChangedDeltas, pairs.currentFixedValueBytes)
}
func collectBalanceValueStats(name string, values []uint64) balanceValueStats {
freq := make(map[uint64]int)
var zeroCount int
var uvarintBytes uint64
for _, value := range values {
freq[value]++
if value == 0 {
zeroCount++
}
uvarintBytes += uint64(uvarintLen(value))
}
top := make([]balanceTopValue, 0, len(freq))
var duplicateCount int
for value, count := range freq {
top = append(top, balanceTopValue{value: value, count: count})
if count > 1 {
duplicateCount += count - 1
}
}
sort.Slice(top, func(i, j int) bool {
if top[i].count == top[j].count {
return top[i].value < top[j].value
}
return top[i].count > top[j].count
})
if len(top) > 10 {
top = top[:10]
}
return balanceValueStats{
name: name,
count: len(values),
unique: len(freq),
zeroCount: zeroCount,
topValues: top,
fixedBytes: uint64(len(values)) * 8,
uvarintBytes: uvarintBytes,
duplicateCount: duplicateCount,
}
}
func printBalanceValueStats(stats balanceValueStats) {
fmt.Printf("%s: count=%d unique=%d duplicate_values=%d zero=%d zero_pct=%.2f%% fixed_bytes=%d uvarint_bytes=%d uvarint_saved=%.2f%%\n",
stats.name,
stats.count,
stats.unique,
stats.duplicateCount,
stats.zeroCount,
float64(stats.zeroCount)*100/float64(maxInt(stats.count, 1)),
stats.fixedBytes,
stats.uvarintBytes,
savedPct(stats.fixedBytes, stats.uvarintBytes),
)
fmt.Printf("%-22s %-8s %-8s\n", "value", "count", "pct")
for _, item := range stats.topValues {
fmt.Printf("%-22d %-8d %7.2f%%\n", item.value, item.count, float64(item.count)*100/float64(maxInt(stats.count, 1)))
}
}
func printEstimate(name string, bytes uint64, baseline uint64) {
fmt.Printf("%-38s %10d saved=%7.2f%%\n", name, bytes, savedPct(baseline, bytes))
}
func savedPct(baseline uint64, value uint64) float64 {
if baseline == 0 {
return 0
}
return (float64(baseline) - float64(value)) * 100 / float64(baseline)
}
func uvarintLen(value uint64) int {
n := 1
for value >= 0x80 {
value >>= 7
n++
}
return n
}
func zigzagDeltaUvarintLen(pre uint64, post uint64) int {
if post >= pre {
return uvarintLen((post - pre) << 1)
}
return uvarintLen(((pre - post) << 1) - 1)
}
func maxInt(a int, b int) int {
if a > b {
return a
}
return b
}

View File

@@ -0,0 +1,302 @@
package main
import (
"context"
"flag"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"sort"
"syscall"
"time"
"github.com/gagliardetto/solana-go"
pump_parser "github.com/thloyi/pump-parser"
pb "go.onsig.ai/onsig/yellowstone-proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/metadata"
)
type collector struct {
endpoint string
xToken string
plaintext bool
blocks map[uint64][]pump_parser.RawTx
seen map[string]struct{}
totalUpdates uint64
txUpdates uint64
savedNonVote uint64
duplicates uint64
voteFiltered uint64
convertErrs uint64
reconnects uint64
firstSlot uint64
lastSlot uint64
}
func main() {
var (
endpoint = flag.String("endpoint", "ams.rpc.orbitflare.com:10000", "Yellowstone gRPC endpoint")
xToken = flag.String("x-token", os.Getenv("YELLOWSTONE_X_TOKEN"), "Yellowstone x-token; defaults to YELLOWSTONE_X_TOKEN")
duration = flag.Duration("duration", 5*time.Minute, "collection duration")
output = flag.String("output", "", "output .prbs file path")
plaintext = flag.Bool("plaintext", true, "use plaintext gRPC instead of TLS")
)
flag.Parse()
if *xToken == "" {
exitf("missing -x-token or YELLOWSTONE_X_TOKEN")
}
if *duration <= 0 {
exitf("-duration must be positive")
}
if *output == "" {
*output = filepath.Join("testdata", "rawtx-binary", fmt.Sprintf("rawtx-yellowstone-%s.prbs", time.Now().Format("20060102-150405")))
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
ctx, cancel := context.WithTimeout(ctx, *duration)
defer cancel()
c := &collector{
endpoint: *endpoint,
xToken: *xToken,
plaintext: *plaintext,
blocks: make(map[uint64][]pump_parser.RawTx),
seen: make(map[string]struct{}),
}
started := time.Now()
done := make(chan error, 1)
go func() {
done <- c.run(ctx)
}()
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
loop:
for {
select {
case err := <-done:
if err != nil {
exitf("%v", err)
}
break loop
case <-ctx.Done():
if err := <-done; err != nil {
exitf("%v", err)
}
break loop
case <-ticker.C:
c.printProgress(started)
}
}
encoded, decodedCount, err := encodeAndVerify(c.blocks)
if err != nil {
exitf("raw tx binary encode/decode: %v", err)
}
if decodedCount != int(c.savedNonVote) {
exitf("decoded tx count mismatch: got=%d want=%d", decodedCount, c.savedNonVote)
}
if err := os.MkdirAll(filepath.Dir(*output), 0o755); err != nil {
exitf("mkdir output dir: %v", err)
}
if err := os.WriteFile(*output, encoded, 0o644); err != nil {
exitf("write output: %v", err)
}
fmt.Printf("output=%s\n", *output)
fmt.Printf("duration=%s elapsed=%s\n", *duration, time.Since(started).Truncate(time.Second))
fmt.Printf("updates=%d tx_updates=%d converted_nonvote=%d duplicate=%d vote_filtered=%d convert_err=%d reconnects=%d\n",
c.totalUpdates, c.txUpdates, c.savedNonVote, c.duplicates, c.voteFiltered, c.convertErrs, c.reconnects)
fmt.Printf("slots=%d first_slot=%d last_slot=%d decoded=%d\n", len(c.blocks), c.firstSlot, c.lastSlot, decodedCount)
fmt.Printf("bytes=%d bytes_per_tx=%.2f\n", len(encoded), float64(len(encoded))/float64(max(int(c.savedNonVote), 1)))
}
func (c *collector) run(ctx context.Context) error {
for ctx.Err() == nil {
if err := c.recvOnce(ctx); err != nil && ctx.Err() == nil {
c.reconnects++
fmt.Fprintf(os.Stderr, "stream_err reconnect=%d err=%v\n", c.reconnects, err)
time.Sleep(time.Second)
continue
}
if ctx.Err() == nil {
c.reconnects++
time.Sleep(time.Second)
}
}
return nil
}
func (c *collector) recvOnce(ctx context.Context) error {
conn, err := grpc.NewClient(
c.endpoint,
c.transportOption(),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 10 * time.Second,
Timeout: time.Second,
PermitWithoutStream: true,
}),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(64*1024*1024)),
)
if err != nil {
return err
}
defer conn.Close()
ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{"x-token": c.xToken}))
stream, err := pb.NewGeyserClient(conn).Subscribe(ctx)
if err != nil {
return err
}
vote := false
subscription := &pb.SubscribeRequest{
Transactions: map[string]*pb.SubscribeRequestFilterTransactions{
"nonvote": {Vote: &vote},
},
}
if err := stream.Send(subscription); err != nil {
return err
}
for {
resp, err := stream.Recv()
if err != nil {
if err == io.EOF || ctx.Err() != nil {
return nil
}
return err
}
c.totalUpdates++
txn := resp.GetTransaction()
if txn == nil {
continue
}
c.txUpdates++
created := time.Now().Unix()
if resp.GetCreatedAt() != nil {
created = resp.GetCreatedAt().Seconds
}
rawTx, err := pump_parser.ConvertYellowstoneGrpcTransactionToSolanaTransaction(txn, created)
if err != nil {
c.convertErrs++
continue
}
txHash := rawTx.TxHash()
if txHash != "" {
if _, exists := c.seen[txHash]; exists {
c.duplicates++
continue
}
c.seen[txHash] = struct{}{}
}
if c.firstSlot == 0 || rawTx.Slot < c.firstSlot {
c.firstSlot = rawTx.Slot
}
if rawTx.Slot > c.lastSlot {
c.lastSlot = rawTx.Slot
}
if isVoteTx(rawTx) {
c.voteFiltered++
continue
}
c.blocks[rawTx.Slot] = append(c.blocks[rawTx.Slot], *rawTx)
c.savedNonVote++
}
}
func (c *collector) transportOption() grpc.DialOption {
if c.plaintext {
return grpc.WithTransportCredentials(insecure.NewCredentials())
}
return grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, ""))
}
func (c *collector) printProgress(started time.Time) {
fmt.Fprintf(os.Stderr,
"progress elapsed=%s updates=%d tx_updates=%d saved_nonvote=%d slots=%d duplicate=%d vote_filtered=%d convert_err=%d reconnects=%d\n",
time.Since(started).Truncate(time.Second),
c.totalUpdates,
c.txUpdates,
c.savedNonVote,
len(c.blocks),
c.duplicates,
c.voteFiltered,
c.convertErrs,
c.reconnects,
)
}
func encodeAndVerify(blocks map[uint64][]pump_parser.RawTx) ([]byte, int, error) {
slots := make([]uint64, 0, len(blocks))
for slot := range blocks {
slots = append(slots, slot)
}
sort.Slice(slots, func(i, j int) bool { return slots[i] < slots[j] })
ordered := make([][]pump_parser.RawTx, 0, len(slots))
for _, slot := range slots {
txs := blocks[slot]
blockTime := int64(0)
if len(txs) > 0 {
blockTime = txs[0].BlockTime
}
for i := range txs {
txs[i].BlockTime = blockTime
}
ordered = append(ordered, txs)
}
encoded, err := pump_parser.EncodeRawTxBlocksBinary(ordered)
if err != nil {
return nil, 0, err
}
decoded, err := pump_parser.DecodeRawTxBlocksBinary(encoded)
if err != nil {
return nil, 0, err
}
decodedCount := 0
for _, block := range decoded {
decodedCount += len(block)
}
return encoded, decodedCount, nil
}
func isVoteTx(tx *pump_parser.RawTx) bool {
if tx == nil {
return false
}
accountList := tx.GetAccountList()
for _, instr := range tx.Transaction.Message.Instructions {
if instr.ProgramIDIndex >= 0 && instr.ProgramIDIndex < len(accountList) && accountList[instr.ProgramIDIndex] == solana.VoteProgramID {
return true
}
}
return false
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func exitf(format string, args ...any) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}

View File

@@ -0,0 +1,134 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
pump_parser "github.com/thloyi/pump-parser"
)
type blockResponse struct {
Result blockResult `json:"result"`
}
type blockResult struct {
BlockTime *int64 `json:"blockTime"`
Transactions []pump_parser.RawTx `json:"transactions"`
}
func main() {
var (
filePath = flag.String("file", "", "path to getBlock payload json")
slot = flag.Uint64("slot", 0, "block slot")
swapsOnly = flag.Bool("swaps-only", false, "only include transactions with swaps > 0")
)
flag.Parse()
if *filePath == "" || *slot == 0 {
fmt.Fprintln(os.Stderr, "usage: measure_tx_binary_block -file /path/block.json -slot 413539056")
os.Exit(2)
}
raw, err := os.ReadFile(*filePath)
if err != nil {
fmt.Fprintf(os.Stderr, "read file: %v\n", err)
os.Exit(1)
}
var response blockResponse
if err := json.Unmarshal(raw, &response); err != nil {
fmt.Fprintf(os.Stderr, "unmarshal block payload: %v\n", err)
os.Exit(1)
}
var blockTime *uint64
if response.Result.BlockTime != nil {
bt := uint64(*response.Result.BlockTime)
blockTime = &bt
}
total := len(response.Result.Transactions)
converted := 0
parsed := 0
convertFailures := 0
parseFailures := 0
encodeFailures := 0
filteredOutNoSwaps := 0
var totalRawTxBytes int
var totalSingleEncoded int
minSingleEncoded := -1
maxSingleEncoded := 0
parsedTxs := make([]pump_parser.Tx, 0, total)
for i, rawTx := range response.Result.Transactions {
transactionJSON, err := json.Marshal(rawTx.Transaction)
if err == nil {
totalRawTxBytes += len(transactionJSON)
}
rawTx.BlockTime = 0
if blockTime != nil {
rawTx.BlockTime = int64(*blockTime)
}
rawTx.Slot = *slot
rawTx.IndexWithinBlock = int64(i)
converted++
tx, err := pump_parser.ParseRawTx(&rawTx)
if err != nil {
parseFailures++
continue
}
if *swapsOnly && len(tx.Swaps) == 0 {
filteredOutNoSwaps++
continue
}
parsed++
encoded, err := pump_parser.EncodeTxBinary(tx)
if err != nil {
encodeFailures++
continue
}
size := len(encoded)
totalSingleEncoded += size
if minSingleEncoded == -1 || size < minSingleEncoded {
minSingleEncoded = size
}
if size > maxSingleEncoded {
maxSingleEncoded = size
}
parsedTxs = append(parsedTxs, *tx)
}
batchEncoded, err := pump_parser.EncodeTxsBinary(parsedTxs)
if err != nil {
fmt.Fprintf(os.Stderr, "encode txs binary: %v\n", err)
os.Exit(1)
}
avgSingleEncoded := 0
if parsed > 0 {
avgSingleEncoded = totalSingleEncoded / parsed
}
fmt.Printf("block_slot=%d\n", *slot)
fmt.Printf("payload_json_bytes=%d\n", len(raw))
fmt.Printf("transactions_total=%d\n", total)
fmt.Printf("transactions_converted=%d\n", converted)
fmt.Printf("transactions_parsed=%d\n", parsed)
fmt.Printf("transactions_filtered_no_swaps=%d\n", filteredOutNoSwaps)
fmt.Printf("convert_failures=%d\n", convertFailures)
fmt.Printf("parse_failures=%d\n", parseFailures)
fmt.Printf("encode_failures=%d\n", encodeFailures)
fmt.Printf("raw_tx_total_bytes=%d\n", totalRawTxBytes)
fmt.Printf("single_txbinary_total_bytes=%d\n", totalSingleEncoded)
fmt.Printf("single_txbinary_avg_bytes=%d\n", avgSingleEncoded)
fmt.Printf("single_txbinary_min_bytes=%d\n", minSingleEncoded)
fmt.Printf("single_txbinary_max_bytes=%d\n", maxSingleEncoded)
fmt.Printf("batch_shared_table_bytes=%d\n", len(batchEncoded))
if totalSingleEncoded > 0 {
fmt.Printf("batch_vs_single_saved_bytes=%d\n", totalSingleEncoded-len(batchEncoded))
}
}

133
cmd/rpc_parse/main.go Normal file
View File

@@ -0,0 +1,133 @@
package main
import (
"context"
"fmt"
"os"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
pump_parser "github.com/thloyi/pump-parser"
)
func main() {
const rpcURL = "https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"
txHash := os.Getenv("TX_HASH")
if txHash == "" {
txHash = "24wP3rk2ZfSVDB5YGyEQbhuy1jRXKZYkzXDRrDwwoKgzD6G4Kyyh4vmnir9ye98uLVKA5bBMj5Fq4cwgbDxp2Gie"
}
if txHash == "" {
fmt.Fprintln(os.Stderr, "txHash is empty; set it in cmd/rpc_parse/main.go")
os.Exit(1)
}
sig, err := solana.SignatureFromBase58(txHash)
if err != nil {
fmt.Fprintf(os.Stderr, "invalid txHash: %v\n", err)
os.Exit(1)
}
client := rpc.New(rpcURL)
maxSupportedVersion := uint64(0)
out, err := client.GetTransaction(context.Background(), sig, &rpc.GetTransactionOpts{
Encoding: solana.EncodingBase64,
Commitment: rpc.CommitmentConfirmed,
MaxSupportedTransactionVersion: &maxSupportedVersion,
})
if err != nil {
fmt.Fprintf(os.Stderr, "rpc getTransaction error: %v\n", err)
os.Exit(1)
}
if out == nil || out.Transaction == nil || out.Meta == nil {
fmt.Fprintln(os.Stderr, "rpc getTransaction returned empty response")
os.Exit(1)
}
rawBinary := out.Transaction.GetBinary()
if len(rawBinary) == 0 {
fmt.Fprintln(os.Stderr, "rpc getTransaction returned empty transaction data")
os.Exit(1)
}
txWithMeta := rpc.TransactionWithMeta{
Slot: out.Slot,
BlockTime: out.BlockTime,
Transaction: rpc.DataBytesOrJSONFromBytes(rawBinary),
Meta: out.Meta,
Version: out.Version,
}
var blockTime *uint64
if out.BlockTime != nil {
bt := uint64(*out.BlockTime)
blockTime = &bt
}
rawTx, err := pump_parser.FromRpcTransactionWithMeta(txWithMeta, blockTime, out.Slot, 0)
if err != nil {
fmt.Fprintf(os.Stderr, "convert rpc transaction error: %v\n", err)
os.Exit(1)
}
pump_parser.EnableAllParsers()
parsed, err := pump_parser.ParseRawTx(rawTx)
if err != nil {
fmt.Fprintf(os.Stderr, "parse raw tx error: %v\n", err)
os.Exit(1)
}
if len(parsed.Swaps) == 0 {
fmt.Println("no swaps parsed from tx")
return
}
for i, swap := range parsed.Swaps {
fmt.Printf("swap[%d]\n", i)
fmt.Printf(" program: %s\n", swap.Program)
fmt.Printf(" event: %s\n", swap.Event)
fmt.Printf(" pool: %s\n", swap.Pool)
fmt.Printf(" user: %s\n", swap.User)
fmt.Printf(" base_mint: %s (decimals=%d)\n", swap.BaseMint, swap.BaseMintDecimals)
fmt.Printf(" quote_mint: %s (decimals=%d)\n", swap.QuoteMint, swap.QuoteMintDecimals)
fmt.Printf(" base_amount: %s\n", swap.BaseAmount.String())
fmt.Printf(" quote_amount: %s\n", swap.QuoteAmount.String())
if swap.SwapMode != pump_parser.SwapModeUnknown {
fmt.Printf(" swap_mode: %s\n", swap.SwapMode.String())
fmt.Printf(" fixed_amount: %s\n", swap.FixedAmount.String())
fmt.Printf(" fixed_amount_side: %s\n", swap.FixedAmountSide.String())
fmt.Printf(" fixed_mint: %s\n", swap.FixedMint)
fmt.Printf(" limit_amount_type: %s\n", swap.LimitAmountType.String())
fmt.Printf(" limit_amount: %s\n", swap.LimitAmount.String())
fmt.Printf(" limit_amount_side: %s\n", swap.LimitAmountSide.String())
fmt.Printf(" limit_mint: %s\n", swap.LimitMint)
fmt.Printf(" actual_limit_amount: %s\n", swap.ActualLimitAmount.String())
fmt.Printf(" slippage_bps: %s\n", swap.SlippageBps.String())
}
if !swap.FeeAmount.IsZero() || swap.FeeSide != "" {
fmt.Printf(" fee_amount: %s\n", swap.FeeAmount.String())
fmt.Printf(" lp_fee_amount: %s\n", swap.LpFeeAmount.String())
fmt.Printf(" fee_side: %s\n", swap.FeeSide)
fmt.Printf(" fee_mint: %s (decimals=%d)\n", swap.FeeMint, swap.FeeMintDecimals)
fmt.Printf(" fee_token_program: %s\n", swap.FeeTokenProgram)
}
fmt.Printf(" base_reserve: %s\n", swap.BaseReserve.String())
fmt.Printf(" quote_reserve: %s\n", swap.QuoteReserve.String())
fmt.Printf(" base_token_program: %s\n", swap.BaseTokenProgram)
fmt.Printf(" quote_token_program: %s\n", swap.QuoteTokenProgram)
fmt.Printf(" entry_contract: %s\n", swap.EntryContract)
fmt.Printf(" user_base_balance: %s\n", swap.UserBaseBalance.String())
fmt.Printf(" user_quote_balance: %s\n", swap.UserQuoteBalance.String())
fmt.Printf(" active_bin_id: %d\n", swap.ActiveBinId)
fmt.Printf(" start_bin_id: %d\n", swap.StartBinId)
fmt.Printf(" end_bin_id: %d\n", swap.EndBinId)
fmt.Printf(" remove_bp: %d\n", swap.RemoveBp)
fmt.Printf(" position_account: %s\n", swap.PositionAccount)
if swap.Mayhem {
fmt.Printf(" mayhem: true\n")
} else {
fmt.Printf(" mayhem: false\n")
}
fmt.Println()
}
}

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("FogxVNs6Mm2w9rnGL1vkARSwJxvLE8mujTv3LK8RnUhF"): 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("noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE"): MevAgentNozomi,
@@ -186,6 +200,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 +220,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,
@@ -335,7 +357,10 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("bgDETv6tnt9mwYqAKebLXY5B5o6akiKJmAdU7Gd9G7H"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgH7YhymSykyvMa3nAZpzvrn73owJHU5iB75S1aiLT9"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfngGVVQLiVRFbLWw3Ektiv17ef9NiRZbcgdAhh4ZEW"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("mwGELGMgGGrNL1UibNCQeJHDE7qdPptWRYB6noUHmTj"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("nEFs3jph8HJt7honu3k7XtGUufMnwAvSXmXcKSPxryP"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("nzUMeyyucuHSCZLw1aaX14d1si2mffT4PjVNdZcybot"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("n6Tc3GoTheA1pYF4MPV57KGmn3DHJCCuk3wHZmcUyp"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("Fa1con11xLjPddfzRwRUB16sbFZggp2JeJkCeWREyR8X"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con11TM1RuAQzbQzYjTy4Ekfap9Lnc9fnEbQYEd6Q"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con113Bvi76nS5AzUiRDC2fqjfzkNMUNRLgQybMYt"): MevagentFa1con,
@@ -381,6 +406,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{

17
enum.go
View File

@@ -21,6 +21,8 @@ const (
MevAgentSpeedlanding = "speedlanding"
MevAgentAllenhark = "allenhark"
MevAgentRaiden = "raiden"
MevAgentZan = "zan"
MevAgentTunneling = "tunneling"
)
const (
@@ -123,5 +125,20 @@ const (
TxEventRemoveLP = "remove"
TxEventBuy = "buy"
TxEventSell = "sell"
TxEventBuyFailed = "buy_failed"
TxEventSellFailed = "sell_failed"
TxEventBurn = "burn"
TxEventCreate = "create"
TxEventComplete = "complete"
TxEventMigrate = "migrate"
TxEventDeposit = "deposit"
TxEventWithdraw = "withdraw"
TxEventOpen = "open"
TxEventClose = "close"
TxEventClaimFee = "claim_fee"
TxEventAddLiquidity = "add_liquidity"
TxEventAddLiquidityOneSide = "add_liquidity_one_side"
TxEventRemoveLiquidity = "remove_liquidity"
TxEventRemoveLiquidityOneSide = "remove_liquidity_one_side"
)

View File

@@ -25,7 +25,7 @@ func main() {
// laserstream-mainnet-slc.helius-rpc.com:80
ch := make(chan example.SubscriptionMessage, 1)
go example.RunLoopWithReConnect(context.Background(), "127.0.0.1:10001", 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 {
@@ -51,9 +51,24 @@ func main() {
//}
// 处理交易
if len(ptx.Swaps) > 0 && (ptx.Swaps[0].Program == parser.SolProgramPump || ptx.Swaps[0].Program == parser.SolProgramPumpAMM) {
fmt.Printf("success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s \n", time.Now().Format("2006-01-02 15:04:05"), ptx.Swaps[0].Program, ptx.Swaps[0].Event, ptx.Block, ptx.GetTxHash(),
ptx.Swaps[0].BaseAmount.Div(decimal.NewFromInt(1e6)), ptx.Swaps[0].QuoteAmount.Div(decimal.NewFromInt(1e9)))
if len(ptx.Swaps) > 0 {
for _, swap := range ptx.Swaps {
if swap.SlippageBps.LessThan(decimal.Zero) || swap.SlippageBps.GreaterThan(decimal.NewFromInt(10000)) {
fmt.Printf("success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s \n", time.Now().Format("2006-01-02 15:04:05"), swap.Program, swap.Event, ptx.Block, ptx.GetTxHash(),
swap.BaseAmount.Div(decimal.NewFromInt(1e6)), swap.QuoteAmount.Div(decimal.NewFromInt(1e9)))
}
if swap.SlippageBps.Equal(decimal.Zero) && (swap.Event == "buy" || swap.Event == "sell") {
fmt.Printf("zero success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s, fix: %s, limit: %s, \n", time.Now().Format("2006-01-02 15:04:05"), swap.Program, swap.Event, ptx.Block, ptx.GetTxHash(),
swap.BaseAmount.Div(decimal.NewFromInt(1e6)), swap.QuoteAmount.Div(decimal.NewFromInt(1e9)), swap.FixedAmount.String(), swap.LimitAmount.String())
}
}
if len(ptx.Swaps) > 0 {
_, err := parser.EncodeTxBinary(ptx)
if err != nil {
fmt.Printf("success tx : %s, , block: %d, tx: %s, err: %s \n", time.Now().Format("2006-01-02 15:04:05"), ptx.Block, ptx.GetTxHash(), err.Error())
}
}
}
// currentBlock = ptx.Block
//

View File

@@ -45,9 +45,11 @@ type Client struct {
firstMessage bool
handler Handler
xToken string
}
func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client {
func NewClientWithPumpSwap(endpoint string, xtoken string, ch chan SubscriptionMessage) *Client {
var subscription pb.SubscribeRequest
//var failed = true
@@ -58,10 +60,10 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client
Vote: &vote,
}
subscription.Transactions["transactions_sub"].AccountInclude = []string{
"pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
}
//subscription.Transactions["transactions_sub"].AccountInclude = []string{
// "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
// "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
//}
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
@@ -72,6 +74,7 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client
lastReceiveTime: time.Now(),
subStatus: false,
subscription: &subscription,
xToken: xtoken,
}
c.handler = NewPumpHandler(func(tx *types.Tx) {
c.sendTx(tx)
@@ -112,12 +115,12 @@ func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Clien
return c
}
func RunLoopWithReConnect(ctx context.Context, endpoint, program string, ch chan SubscriptionMessage) {
func RunLoopWithReConnect(ctx context.Context, endpoint, token, program string, ch chan SubscriptionMessage) {
var client *Client
if program == types.SolProgramRaydiumLaunchLab {
client = NewClientWithLaunchLab(endpoint, ch)
} else {
client = NewClientWithPumpSwap(endpoint, ch)
client = NewClientWithPumpSwap(endpoint, token, ch)
}
for {
select {
@@ -206,12 +209,13 @@ func (c *Client) grpcSubscribe(ctx context.Context, conn *grpc.ClientConn) error
log.Printf("Subscription request: %s", string(subscriptionJson))
// Set up the subscription request
//if *token != "" {
// md := metadata.New(map[string]string{"x-token": *token})
// ctx = metadata.NewOutgoingContext(ctx, md)
//}
md := metadata.New(map[string]string{"x-token": "5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"})
if c.xToken != "" {
fmt.Println("xtoken", c.xToken)
md := metadata.New(map[string]string{"x-token": c.xToken})
ctx = metadata.NewOutgoingContext(ctx, md)
}
//md := metadata.New(map[string]string{"x-token": "5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"})
//ctx = metadata.NewOutgoingContext(ctx, md)
stream, err := client.Subscribe(ctx)
if err != nil {

View File

@@ -2,29 +2,18 @@ package main
import (
"context"
"errors"
"fmt"
"log"
"log/slog"
"strings"
"time"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
"github.com/jackc/pgtype"
"github.com/shopspring/decimal"
solana_parser "github.com/thloyi/pump-parser"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var ()
func main() {
var slot uint64 = 403021435
var data = NewBlockData(decimal.NewFromFloat(100.0))
var slot uint64 = 414696178
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
var rewards = false
var version uint64 = 0
@@ -42,7 +31,7 @@ func main() {
}
solana_parser.EnableAllParsers()
var txs []*solana_parser.Tx
var txs []solana_parser.Tx
for i, tx := range blocks.Transactions {
var blockTime uint64
if blocks.BlockTime != nil {
@@ -61,766 +50,11 @@ func main() {
fmt.Println("parse tx error:", i, rawTx.TxHash(), err)
break
}
txs = append(txs, parsedTx)
txs = append(txs, *parsedTx)
}
for _, result := range txs {
swapsLen := len(result.Swaps)
for i := 0; i < swapsLen; i++ {
action := result.Swaps[i]
var actions []solana_parser.Swap = make([]solana_parser.Swap, 0, 2)
actions = append(actions, action)
if i+1 < swapsLen {
nextAction := result.Swaps[i+1]
if action.Event == "buy" && nextAction.Event == "complete" &&
action.Program == solana_parser.SolProgramPump &&
nextAction.Program == solana_parser.SolProgramPump &&
action.BaseMint == nextAction.BaseMint {
actions = append(actions, nextAction)
i++
}
if action.Event == "migrate" && nextAction.Event == "create" &&
action.Program == solana_parser.SolProgramPump &&
nextAction.Program == solana_parser.SolProgramPumpAMM &&
action.BaseMint == nextAction.BaseMint {
actions = append(actions, nextAction)
i++
}
}
if err = HandleAction(context.Background(), result, actions, data); err != nil {
//h.logger.Errorf("handle action error: %s - %v", result.RawTx.Transaction.Signatures[0].String(), err)
fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err)
}
}
}
fmt.Println("slot", slot, "tx count: ", len(data.Txs))
}
var (
meteoraDammV2Program = solana.MustPublicKeyFromBase58("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG")
raydiumCPmmProgramID = solana.MustPublicKeyFromBase58("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C")
)
func HandleAction(ctx context.Context, tx *solana_parser.Tx, swaps []solana_parser.Swap, data *BlockData) error {
swapLen := len(swaps)
if len(swaps) == 0 {
return nil
}
if swaps[0].BaseMint != solana_parser.WSOL && swaps[0].QuoteMint != solana_parser.WSOL && !swaps[0].QuoteMint.IsZero() {
return nil
}
if len(swaps) == 0 {
return nil
}
event := swaps[0].Event
swap := swaps[0]
action := SwapGetter{swap}
switch event {
case "buy", "sell":
data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price))
if swap.Program == solana_parser.SolProgramPump {
if swapLen == 2 && swaps[1].Event == "complete" {
t := pgtype.Timestamptz{}
t.Set(time.Unix(tx.BlockAt, 0))
data.AppendAction(Action{
Maker: swaps[1].User.String(),
Token: swaps[1].BaseMint.String(),
Pair: swaps[1].Pool.String(),
Action: "pump-migrate",
Block: tx.Block,
BlockAt: t,
TxHash: tx.GetTxHash(),
})
}
}
return data.SetPair(action, tx.Block, "")
case "create":
pair, err := action.GetPair(tx.Block, "")
_, err = solana_parser.EncodeTxsBinary(txs)
if err != nil {
return err
}
data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price))
data.Pairs[pair.Address] = *pair
case "add_liquidity", "remove_liquidity", "deposit", "withdraw", "add", "remove":
liquidityTx, err := action.GetLiquidityTx(tx, uint64(swap.TxIndex))
if liquidityTx == nil {
return err
}
data.AppendTx(*liquidityTx)
return data.SetPair(action, tx.Block, "")
fmt.Println("EncodeTxsBinary err", err)
}
if event != "migrate" {
return nil
}
if swap.Program == solana_parser.SolProgramPump {
t := pgtype.Timestamptz{}
t.Set(time.Unix(tx.BlockAt, 0))
if swapLen == 2 && swaps[1].Event == "create" && swaps[1].Program == solana_parser.SolProgramPumpAMM && swaps[1].BaseMint == swap.BaseMint {
tokenMint := swap.BaseMint.String()
data.AppendAction(Action{
Maker: swap.User.String(),
Token: tokenMint,
Pair: swaps[1].Pool.String(),
Action: "on-pumpswap",
Block: tx.Block,
BlockAt: t,
TxHash: tx.GetTxHash(),
})
data.NewRaydium = append(data.NewRaydium, tokenMint)
}
} else if swap.Program == solana_parser.SolProgramRaydiumLaunchLab || swap.Program == solana_parser.SolProgramRaydiumLaunchLabBonk {
t := pgtype.Timestamptz{}
t.Set(time.Unix(tx.BlockAt, 0))
var actionType string
if action.MigrateTopProgram == raydiumCPmmProgramID {
actionType = "on-raydium-cpmm"
} else {
actionType = "on-raydium-amm"
}
data.AppendAction(Action{
Maker: action.User.String(),
Token: action.BaseMint.String(),
Pair: action.MigrateToPool.String(),
Action: actionType,
Block: tx.Block,
BlockAt: t,
TxHash: tx.GetTxHash(),
})
} else if swap.Program == solana_parser.SolProgramMeteoraBondingCurve {
t := pgtype.Timestamptz{}
t.Set(time.Unix(tx.BlockAt, 0))
var actionType string
if swap.MigrateTopProgram == meteoraDammV2Program {
actionType = "on-meteora-amm-v2"
} else {
actionType = "on-meteora-amm-v1"
}
data.AppendAction(Action{
Maker: action.User.String(),
Token: action.BaseMint.String(),
Pair: action.MigrateToPool.String(),
Action: actionType,
Block: uint64(tx.Block),
BlockAt: t,
TxHash: tx.GetTxHash(),
})
}
return nil
}
type Pair struct {
Id string `gorm:"column:id;primaryKey;default:uuid_generate_v4()"`
Address string
Name string
Token0 string
Token1 string
LpToken string
ChainId int64
Reserve0 decimal.Decimal
Reserve1 decimal.Decimal
Block uint64
BlockAt *pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at,omitempty"`
CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"created_at,omitempty"`
SortId uint64
Program string
IsCreate bool `gorm:"-"`
//TokenObj *Token `gorm:"-" json:"token_obj,omitempty"`
UpdateSlot uint64 `gorm:"-"`
InDB bool `gorm:"-"`
}
type Tx struct {
Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"`
PairAddress string `json:"pair_address"`
Maker string `json:"maker"`
Token0Address string `json:"token0_address"`
Token1Address string `json:"token1_address"`
Token0Amount decimal.Decimal `json:"token0Amount" gorm:"column:token0_amount;type:numeric"`
Token1Amount decimal.Decimal `json:"token1Amount" gorm:"column:token1_amount;type:numeric"`
PriceUsd decimal.Decimal `json:"price_usd" gorm:"column:price_usd;type:numeric"`
AmountUsd decimal.Decimal `json:"amount_usd" gorm:"column:amount_usd;type:numeric"`
Block uint64 `json:"block"`
BlockIndex uint64 `json:"index"`
Event string `json:"event"`
TxHash string `json:"tx_hash"`
TxIndex uint64 `json:"topic_index"`
Program string `json:"program"`
BlockAt pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at"`
CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"`
TotalSupply string `gorm:"total_supply"`
AfterReserve0 string `gorm:"after_reserve0"`
AfterReserve1 string `gorm:"after_reserve1"`
PositionChange int64 `gorm:"position_change"`
Platform string `gorm:"column:tx_platform;type:platform;default:'none'" json:"tx_platform"`
PlatformFee decimal.Decimal `gorm:"-" json:"-"` // TODO: save to db
CUPrice decimal.Decimal `gorm:"column:tx_cu_price;type:numeric" json:"tx_cu_price"`
MevAgent string `gorm:"column:tx_mev_agent;type:mev_agent;default:'none'" json:"tx_mev_agent"`
MevAgentFee decimal.Decimal `gorm:"column:tx_mev_agent_fee;type:numeric" json:"tx_mev_agent_fee"`
AfterSOLBalance decimal.Decimal `gorm:"column:after_sol_balance;type:numeric" json:"after_sol_balance"`
EntryContract string `gorm:"column:tx_entry_contract;type:entry_contract;default:'none'" json:"tx_entry_contract"`
}
type Action struct {
Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"`
Maker string `json:"maker"`
Token string `json:"token"`
Pair string `json:"pair"`
Action string `json:"action"`
Block uint64 `json:"block"`
BlockAt pgtype.Timestamptz `json:"block_at"`
TxHash string `json:"tx_hash"`
CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"`
}
type BlockData struct {
Pairs map[string]Pair
Txs []Tx
Actions []Action
Price decimal.Decimal
NewRaydium []string
}
func NewBlockData(price decimal.Decimal) *BlockData {
return &BlockData{
Pairs: make(map[string]Pair),
Txs: make([]Tx, 0),
Actions: make([]Action, 0),
Price: price,
NewRaydium: make([]string, 0),
}
}
func (bd *BlockData) AppendTx(tx Tx) {
bd.Txs = append(bd.Txs, tx)
}
func (bd *BlockData) AppendAction(action Action) {
bd.Actions = append(bd.Actions, action)
}
func (bd *BlockData) SetPair(action SwapGetter, block uint64, _ string) error {
pair, err := action.GetPair(block, "")
if err != nil {
return err
}
bd.Pairs[pair.Address] = *pair
return nil
}
type SwapGetter struct {
solana_parser.Swap
}
const (
PositionChangeNone = int64(iota)
PositionChangeNewBuy
PositionChangeBuyMore
PositionChangeSellPart
PositionChangeSellAll
)
func (spg SwapGetter) GetLiquidityTx(tx *solana_parser.Tx, index uint64) (*Tx, error) {
if spg.BaseMint != solana.WrappedSol && spg.QuoteMint != solana.WrappedSol {
return nil, nil
}
var (
token0 string
amount0 decimal.Decimal
amount1 decimal.Decimal
pool0 decimal.Decimal
pool1 decimal.Decimal
event string
)
if spg.BaseMint == solana.WrappedSol {
amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
token0 = spg.QuoteMint.String()
pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
} else {
amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
token0 = spg.BaseMint.String()
pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
}
if spg.Event == "deposit" || spg.Event == "add" || spg.Event == "add_liquidity" || spg.Event == "add_liquidity_one_side" {
event = "add"
} else if spg.Event == "withdraw" || spg.Event == "remove" || spg.Event == "remove_liquidity" || spg.Event == "remove_liquidity_one_side" {
event = "remove"
}
if event == "" {
return nil, nil
}
mevName, mevFee := tx.CheckMevAgent()
platformName, platformFee := tx.CheckPlatform(spg.Swap)
pairString := ""
if spg.Program == solana_parser.SolProgramPump {
pairString = spg.BaseMint.String()
} else {
pairString = spg.Pool.String()
}
t := pgtype.Timestamptz{}
_ = t.Set(time.Unix(tx.BlockAt, 0))
return &Tx{
PairAddress: pairString,
Maker: spg.User.String(),
Token0Address: token0,
Token1Address: "So11111111111111111111111111111111111111112",
Token0Amount: amount0,
Token1Amount: amount1,
Block: tx.Block,
BlockIndex: tx.BlockIndex,
Event: event,
TxHash: tx.GetTxHash(),
TxIndex: index,
BlockAt: t,
Program: spg.Program,
AfterReserve0: pool0.String(),
AfterReserve1: pool1.String(),
Platform: platformName,
PlatformFee: platformFee,
CUPrice: tx.CUPrice,
MevAgent: mevName,
MevAgentFee: mevFee,
AfterSOLBalance: spg.AfterSOLBalance,
EntryContract: spg.CheckEntryContract(),
}, nil
}
func (spg SwapGetter) GetTx(tx *solana_parser.Tx, index uint64, price decimal.Decimal) Tx {
var (
token0 string
amount0 decimal.Decimal
amount1 decimal.Decimal
pool0 decimal.Decimal
pool1 decimal.Decimal
event string
)
if spg.BaseMint == solana.WrappedSol {
amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
token0 = spg.QuoteMint.String()
pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
if spg.Event == "buy" {
event = "sell"
} else if spg.Event == "sell" {
event = "buy"
}
} else {
amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
token0 = spg.BaseMint.String()
pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
event = spg.Event
}
priceUsd := decimal.Zero
if amount0.GreaterThan(priceUsd) {
priceUsd = amount1.Div(amount0).Mul(price)
}
pc := PositionChangeNone
if event == "buy" {
pc = PositionChangeNewBuy
if spg.BaseMint == solana.WrappedSol {
if spg.UserQuoteBalance.GreaterThan(spg.QuoteAmount) {
pc = PositionChangeBuyMore
}
} else {
if spg.UserBaseBalance.GreaterThan(spg.BaseAmount) {
pc = PositionChangeBuyMore
}
}
} else if event == "sell" {
pc = PositionChangeSellPart
if spg.BaseMint == solana.WrappedSol {
if spg.UserQuoteBalance.Div(decimal.New(1, int32(spg.QuoteMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) {
pc = PositionChangeSellAll
}
} else {
if spg.UserBaseBalance.Div(decimal.New(1, int32(spg.BaseMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) {
pc = PositionChangeSellAll
}
}
}
mevName, mevFee := tx.CheckMevAgent()
platformName, platformFee := tx.CheckPlatform(spg.Swap)
if mevName == "" {
mevName = "none"
}
if mevName == "unknown" {
mevName = "none"
mevFee = decimal.Zero
}
pairString := ""
if spg.Program == solana_parser.SolProgramPump {
pairString = spg.BaseMint.String()
} else {
pairString = spg.Pool.String()
}
t := pgtype.Timestamptz{}
_ = t.Set(time.Unix(tx.BlockAt, 0))
return Tx{
PairAddress: pairString,
Maker: spg.User.String(),
Token0Address: token0,
Token1Address: "So11111111111111111111111111111111111111112",
Token0Amount: amount0,
Token1Amount: amount1,
PriceUsd: priceUsd,
AmountUsd: amount1.Mul(price),
Block: tx.Block,
BlockIndex: tx.BlockIndex,
Event: event,
TxHash: tx.GetTxHash(),
TxIndex: index,
BlockAt: t,
Program: spg.Program,
AfterReserve0: pool0.String(),
AfterReserve1: pool1.String(),
PositionChange: pc,
Platform: platformName,
PlatformFee: platformFee,
CUPrice: tx.CUPrice,
MevAgent: mevName,
MevAgentFee: mevFee,
AfterSOLBalance: spg.AfterSOLBalance,
EntryContract: spg.CheckEntryContract(),
}
}
func (spg SwapGetter) GetPair(slot uint64, _ string) (*Pair, error) {
//pump amm
if spg.Program == solana_parser.SolProgramPump {
tokenMint := spg.BaseMint.String()
return &Pair{
Address: tokenMint,
Token0: tokenMint,
Token1: "So11111111111111111111111111111111111111112",
ChainId: 900,
Reserve0: spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))),
Reserve1: spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))),
IsCreate: spg.Event == "create",
Program: spg.Program,
UpdateSlot: slot,
}, nil
} else {
var (
token0 string
amount0 decimal.Decimal
amount1 decimal.Decimal
)
if spg.BaseMint.IsZero() || spg.QuoteMint.IsZero() {
return nil, errors.New("base mint or quote mint is empty")
}
if spg.BaseMint == solana.WrappedSol {
amount0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
amount1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
//decimal0 = spg.QuoteMintDecimals
token0 = spg.QuoteMint.String()
} else {
amount0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
amount1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
//decimal0 = a.BaseDecimals
token0 = spg.BaseMint.String()
}
return &Pair{
Address: spg.Pool.String(),
LpToken: spg.LpMint.String(),
Token0: token0,
Token1: "So11111111111111111111111111111111111111112",
ChainId: 900,
Reserve0: amount0,
Reserve1: amount1,
IsCreate: false,
Program: spg.Program,
UpdateSlot: slot,
}, nil
}
}
func getBlockTxsFromDb(db *gorm.DB, block uint64) ([]Tx, error) {
var txs []Tx
result := db.Table("tx").Where("block = ?", block).Find(&txs)
return txs, result.Error
}
func getBlockActionsFromDb(db *gorm.DB, block uint64) ([]Action, error) {
var txs []Action
result := db.Table("action").Where("block = ?", block).Find(&txs)
return txs, result.Error
}
type dbLog struct {
logger *slog.Logger
}
func (l *dbLog) Printf(format string, args ...interface{}) {
l.logger.Info(fmt.Sprintf(format, args...))
}
func newDbLog() *dbLog {
return &dbLog{logger: slog.Default()}
}
func NewGorm(dsn string) *gorm.DB {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.New(newDbLog(), logger.Config{
Colorful: false,
LogLevel: logger.Warn,
SlowThreshold: time.Second * 10,
IgnoreRecordNotFoundError: true,
}),
})
if err != nil {
panic(err)
}
return db
}
func compareTxs(dbTxs []Tx, dataTxs []Tx) (diff int, missing int) {
dataByHash := make(map[string][]Tx, len(dataTxs))
for _, tx := range dataTxs {
dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)] = append(dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)], tx)
}
for _, dbTx := range dbTxs {
candidates := dataByHash[fmt.Sprintf("%s-%d", dbTx.TxHash, dbTx.TxIndex)]
if len(candidates) == 0 {
missing++
log.Printf("missing tx: %s", txCompareString(dbTx))
continue
}
matched := false
for _, dataTx := range candidates {
if txEqualWithoutHash(dbTx, dataTx) {
matched = true
break
}
}
if !matched {
diff++
log.Printf("tx diff hash=%s, program=%s, event:%s: %s, ", dbTx.TxHash, dbTx.Program, dbTx.Event, txCompareDiffString(dbTx, candidates[0]))
}
}
log.Printf("compare txs done: db=%d parsed=%d missing=%d diff=%d", len(dbTxs), len(dataTxs), missing, diff)
return diff, missing
}
func withinOnePercentDecimal(a decimal.Decimal, b decimal.Decimal) bool {
if a.IsZero() {
return b.IsZero()
}
diff := a.Sub(b).Abs()
threshold := a.Abs().Mul(decimal.NewFromFloat(0.03))
return diff.LessThanOrEqual(threshold)
}
func withinOnePercentStringDecimal(a string, b string) bool {
ad, errA := decimal.NewFromString(a)
bd, errB := decimal.NewFromString(b)
if errA != nil || errB != nil {
return a == b
}
return withinOnePercentDecimal(ad, bd)
}
func txEqualWithoutHash(a Tx, b Tx) bool {
//mevMatch := a.MevAgent == b.MevAgent || (a.MevAgent == "none" && b.MevAgent == "unknown") || (a.MevAgent == "unknown" && b.MevAgent == "none")
//mevNone := a.MevAgent == "none" || a.MevAgent == "unknown"
return a.PairAddress == b.PairAddress &&
a.Token1Address == b.Token1Address &&
(a.Token0Address == "" || a.Token0Address == b.Token0Address) &&
//a.Maker == b.Maker &&
(a.Token0Address == "" || withinOnePercentDecimal(a.Token0Amount, b.Token0Amount)) &&
withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) &&
a.Block == b.Block &&
a.BlockIndex == b.BlockIndex &&
a.Event == b.Event &&
a.TxIndex == b.TxIndex &&
a.Program == b.Program &&
(a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0)) &&
(a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1)) &&
// a.PositionChange == b.PositionChange &&
(a.Platform == b.Platform || (a.Platform == "photon" && b.Platform == "fake") || (a.Platform == "trojan" && b.Platform == "fake")) &&
a.CUPrice.String() == b.CUPrice.String() // &&
//mevMatch &&
//(mevNone || a.MevAgentFee.String() == b.MevAgentFee.String()) &&
//(a.Program == solana_parser.SolProgramRaydiumV4 || a.AfterSOLBalance.String() == b.AfterSOLBalance.String())
//&&
// a.EntryContract == b.EntryContract
}
func txCompareDiffString(a Tx, b Tx) string {
var diffs []string
if a.PairAddress != b.PairAddress {
diffs = append(diffs, fmt.Sprintf("PairAddress db=%s data=%s", a.PairAddress, b.PairAddress))
}
//if a.Maker != b.Maker {
// diffs = append(diffs, fmt.Sprintf("Maker db=%s, data=%s", a.Maker, b.Maker))
//}
if a.Token1Address != b.Token1Address {
diffs = append(diffs, fmt.Sprintf("Token1Address db=%s data=%s", a.Token1Address, b.Token1Address))
}
if a.Token0Address != b.Token0Address {
diffs = append(diffs, fmt.Sprintf("Token0Address db=%s data=%s", a.Token0Address, b.Token0Address))
}
if !withinOnePercentDecimal(a.Token0Amount, b.Token0Amount) {
diffs = append(diffs, fmt.Sprintf("Token0Amount db=%s data=%s", a.Token0Amount.String(), b.Token0Amount.String()))
}
if !withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) {
diffs = append(diffs, fmt.Sprintf("Token1Amount db=%s data=%s", a.Token1Amount.String(), b.Token1Amount.String()))
}
if a.Block != b.Block {
diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block))
}
if a.BlockIndex != b.BlockIndex {
diffs = append(diffs, fmt.Sprintf("BlockIndex db=%d data=%d", a.BlockIndex, b.BlockIndex))
}
if a.Event != b.Event {
diffs = append(diffs, fmt.Sprintf("Event db=%s data=%s", a.Event, b.Event))
}
if a.TxIndex != b.TxIndex {
diffs = append(diffs, fmt.Sprintf("TxIndex db=%d data=%d", a.TxIndex, b.TxIndex))
}
if a.Program != b.Program {
diffs = append(diffs, fmt.Sprintf("Program db=%s data=%s", a.Program, b.Program))
}
if !withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0) {
diffs = append(diffs, fmt.Sprintf("AfterReserve0 db=%s data=%s", a.AfterReserve0, b.AfterReserve0))
}
if !withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1) {
diffs = append(diffs, fmt.Sprintf("AfterReserve1 db=%s data=%s", a.AfterReserve1, b.AfterReserve1))
}
//if a.PositionChange != b.PositionChange {
// diffs = append(diffs, fmt.Sprintf("PositionChange db=%d data=%d", a.PositionChange, b.PositionChange))
//}
if a.Platform != b.Platform {
diffs = append(diffs, fmt.Sprintf("Platform db=%s data=%s", a.Platform, b.Platform))
}
if a.CUPrice.String() != b.CUPrice.String() {
diffs = append(diffs, fmt.Sprintf("CUPrice db=%s data=%s", a.CUPrice.String(), b.CUPrice.String()))
}
//if a.MevAgent != b.MevAgent {
// diffs = append(diffs, fmt.Sprintf("MevAgent db=%s data=%s", a.MevAgent, b.MevAgent))
//}
//if a.MevAgentFee.String() != b.MevAgentFee.String() {
// diffs = append(diffs, fmt.Sprintf("MevAgentFee db=%s data=%s", a.MevAgentFee.String(), b.MevAgentFee.String()))
//}
//if a.AfterSOLBalance.String() != b.AfterSOLBalance.String() {
// diffs = append(diffs, fmt.Sprintf("AfterSOLBalance db=%s data=%s", a.AfterSOLBalance.String(), b.AfterSOLBalance.String()))
//}
//if a.EntryContract != b.EntryContract {
// diffs = append(diffs, fmt.Sprintf("EntryContract db=%s data=%s", a.EntryContract, b.EntryContract))
//}
return strings.Join(diffs, "; ")
}
func compareActions(dbActions []Action, dataActions []Action) (diff, missing int) {
dataByHash := make(map[string][]Action, len(dataActions))
for _, action := range dataActions {
dataByHash[action.TxHash] = append(dataByHash[action.TxHash], action)
}
for _, dbAction := range dbActions {
candidates := dataByHash[dbAction.TxHash]
if len(candidates) == 0 {
missing++
log.Printf("missing action: %s", actionCompareString(dbAction))
continue
}
matched := false
for _, dataAction := range candidates {
if actionEqualWithoutHash(dbAction, dataAction) {
matched = true
break
}
}
if !matched {
diff++
log.Printf("action diff hash=%s: %s", dbAction.TxHash, actionCompareDiffString(dbAction, candidates[0]))
}
}
log.Printf("compare actions done: db=%d parsed=%d missing=%d diff=%d", len(dbActions), len(dataActions), missing, diff)
return diff, missing
}
func actionEqualWithoutHash(a Action, b Action) bool {
return a.Maker == b.Maker &&
a.Token == b.Token &&
a.Pair == b.Pair &&
a.Action == b.Action &&
a.Block == b.Block
}
func actionCompareDiffString(a Action, b Action) string {
var diffs []string
if a.Maker != b.Maker {
diffs = append(diffs, fmt.Sprintf("Maker db=%s data=%s", a.Maker, b.Maker))
}
if a.Token != b.Token {
diffs = append(diffs, fmt.Sprintf("Token db=%s data=%s", a.Token, b.Token))
}
if a.Pair != b.Pair {
diffs = append(diffs, fmt.Sprintf("Pair db=%s data=%s", a.Pair, b.Pair))
}
if a.Action != b.Action {
diffs = append(diffs, fmt.Sprintf("Action db=%s data=%s", a.Action, b.Action))
}
if a.Block != b.Block {
diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block))
}
return strings.Join(diffs, "; ")
}
func actionCompareString(action Action) string {
return fmt.Sprintf("Maker=%s Token=%s Pair=%s Action=%s Block=%d TxHash=%s", action.Maker, action.Token, action.Pair, action.Action, action.Block, action.TxHash)
}
func txCompareString(tx Tx) string {
return fmt.Sprintf(
"tx.Program=%s TxHash=%s PairAddress=%s Token1Address=%s Token0Amount=%s Token1Amount=%s Block=%d BlockIndex=%d Event=%s TxIndex=%d AfterReserve0=%s AfterReserve1=%s PositionChange=%d Platform=%s CUPrice=%s MevAgent=%s MevAgentFee=%s AfterSOLBalance=%s EntryContract=%s",
tx.Program,
tx.TxHash,
tx.PairAddress,
tx.Token1Address,
tx.Token0Amount.String(),
tx.Token1Amount.String(),
tx.Block,
tx.BlockIndex,
tx.Event,
tx.TxIndex,
tx.AfterReserve0,
tx.AfterReserve1,
tx.PositionChange,
tx.Platform,
tx.CUPrice.String(),
tx.MevAgent,
tx.MevAgentFee.String(),
tx.AfterSOLBalance.String(),
tx.EntryContract,
)
}

View File

@@ -26,7 +26,7 @@ func main() {
var data = NewBlockData(decimal.NewFromFloat(100.0))
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
var version uint64 = 0
txSig, _ := solana.SignatureFromBase58("2LCw5yZy6sGTWKpJNxpFxR11M66cXPsrGmJXnQmWW9QVv6SDWRmu1aevc6yE9NeUz78mFb4T8TEx9w5781NHnz2T")
txSig, _ := solana.SignatureFromBase58("4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri")
tx, err := client.GetTransaction(context.Background(), txSig, &rpc.GetTransactionOpts{
Commitment: rpc.CommitmentFinalized,
Encoding: solana.EncodingBase64,
@@ -78,6 +78,10 @@ func main() {
i++
}
}
fmt.Printf("swap: %d, program: %s, event: %s, base: %s quote: %s, base amount: %s, quote amount: %s, \n", i,
action.Program, action.Event, action.BaseMint.String(), action.QuoteMint.String(),
action.BaseAmount.String(),
action.QuoteAmount.String())
if err = HandleAction(context.Background(), result, actions, data); err != nil {
//h.logger.Errorf("handle action error: %s - %v", result.RawTx.Transaction.Signatures[0].String(), err)
fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err)

18
meta.go
View File

@@ -20,12 +20,16 @@ var mayhemFeeAccounts = []solana.PublicKey{
var pumpGetFeesDiscriminator = calculateDiscriminator("global:get_fees")
var pumpBuyDiscriminator = calculateDiscriminator("global:buy")
var pumpBuyV2Discriminator = calculateDiscriminator("global:buy_exact_sol_in")
var pumpBuyExactSolInDiscriminator = calculateDiscriminator("global:buy_exact_sol_in")
var pumpBuyV2Discriminator = calculateDiscriminator("global:buy_v2")
var pumpBuyExactQuoteInV2Discriminator = calculateDiscriminator("global:buy_exact_quote_in_v2")
var pumpSellDiscriminator = calculateDiscriminator("global:sell")
var pumpSellV2Discriminator = calculateDiscriminator("global:sell_v2")
var pumpCreateDiscriminator = calculateDiscriminator("global:create")
var pumpCreateV2Discriminator = calculateDiscriminator("global:create_v2")
var pumpAdminSetCreatorDiscriminator = calculateDiscriminator("global:admin_set_creator")
var pumpMigrateDiscriminator = calculateDiscriminator("global:migrate")
var pumpMigrateV2Discriminator = calculateDiscriminator("global:migrate_v2")
var pumpEventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29}
var pumpTradeEventDiscriminator = [16]byte{228, 69, 165, 46, 81, 203, 154, 29, 189, 219, 127, 211, 78, 230, 97, 238}
@@ -68,7 +72,11 @@ var (
)
var (
meteoraInitializeLbPairDiscriminator = calculateDiscriminator("global:initialize_lb_pair2")
meteoraInitializeCustomizablePermissionlessLbPairDiscriminator = calculateDiscriminator("global:initialize_customizable_permissionless_lb_pair")
meteoraInitializeCustomizablePermissionlessLbPair2Discriminator = calculateDiscriminator("global:initialize_customizable_permissionless_lb_pair2")
meteoraInitializeLbPairDiscriminator = calculateDiscriminator("global:initialize_lb_pair")
meteoraInitializeLbPair2Discriminator = calculateDiscriminator("global:initialize_lb_pair2")
meteoraInitializePermissionLbPairDiscriminator = calculateDiscriminator("global:initialize_permission_lb_pair")
meteoraInitializeLbPairEventDiscriminator = calculateDiscriminator("event:LbPairCreate")
meteoraDlmmSwapDiscriminator = calculateDiscriminator("global:swap")
meteoraDlmmSwap2Discriminator = calculateDiscriminator("global:swap2")
@@ -89,9 +97,14 @@ var (
meteoraDlmmAddLiquidityByStrategyDiscriminator = calculateDiscriminator("global:add_liquidity_by_strategy")
meteoraDlmmAddLiquidityByStrategy2Discriminator = calculateDiscriminator("global:add_liquidity_by_strategy2")
meteoraDlmmAddLiquidityByWeightDiscriminator = calculateDiscriminator("global:add_liquidity_by_weight")
meteoraDlmmAddLiquidityOneSideDiscriminator = calculateDiscriminator("global:add_liquidity_one_side")
meteoraDlmmAddLiquidityOneSidePreciseDiscriminator = calculateDiscriminator("global:add_liquidity_one_side_precise")
meteoraDlmmAddLiquidityOneSidePrecise2Discriminator = calculateDiscriminator("global:add_liquidity_one_side_precise2")
meteoraDlmmAddLiquidityByStrategyOneSideDiscriminator = calculateDiscriminator("global:add_liquidity_by_strategy_one_side")
meteoraDlmmClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee")
meteoraDlmmClaimFee2Discriminator = calculateDiscriminator("global:claim_fee2")
meteoraDlmmRebalanceLiquidityDiscriminator = calculateDiscriminator("global:rebalance_liquidity")
meteoraDlmmRemoveAllLiquidityDiscriminator = calculateDiscriminator("global:remove_all_liquidity")
meteoraDlmmRemoveLiquidityDiscriminator = calculateDiscriminator("global:remove_liquidity")
meteoraDlmmRemoveLiquidity2Discriminator = calculateDiscriminator("global:remove_liquidity2")
meteoraDlmmRemoveLiquidityByRangeDiscriminator = calculateDiscriminator("global:remove_liquidity_by_range")
@@ -123,6 +136,7 @@ var (
metaoraPoolRemoveBalanceLiquidityDiscriminator = calculateDiscriminator("global:remove_balance_liquidity")
metaoraPoolClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee")
metaoraPoolBootstrapLiquidityDiscriminator = calculateDiscriminator("global:bootstrap_liquidity")
metaoraPoolSwapEventDiscriminator = calculateDiscriminator("event:Swap")
)
var (

View File

@@ -66,6 +66,13 @@ type dlmmPositionCloseEvent struct {
Owner solana.PublicKey
}
type dlmmLbPairCreateEvent struct {
LbPair solana.PublicKey
BinStep uint16
TokenX solana.PublicKey
TokenY solana.PublicKey
}
type dlmmClaimFeeInnerEvent struct {
LbPair solana.PublicKey
Position solana.PublicKey
@@ -178,6 +185,53 @@ type dlmmAddLiquidityByWeightArgs struct {
LiquidityParameter dlmmLiquidityParameterByWeight
}
type dlmmLiquidityOneSideParameter struct {
Amount uint64
ActiveID int32
MaxActiveBinSlippage int32
BinLiquidityDist []dlmmBinLiquidityDistributionByWeight
}
type dlmmLiquidityParameterByStrategyOneSide struct {
Amount uint64
ActiveID int32
MaxActiveBinSlippage int32
StrategyParameters dlmmStrategyParameters
}
type dlmmAddLiquidityOneSideArgs struct {
LiquidityParameter dlmmLiquidityOneSideParameter
}
type dlmmAddLiquidityByStrategyOneSideArgs struct {
LiquidityParameter dlmmLiquidityParameterByStrategyOneSide
}
type dlmmCompressedBinDepositAmount struct {
BinID int32
Amount uint32
}
type dlmmAddLiquiditySingleSidePreciseParameter struct {
Bins []dlmmCompressedBinDepositAmount
DecompressMultiplier uint64
}
type dlmmAddLiquiditySingleSidePreciseParameter2 struct {
Bins []dlmmCompressedBinDepositAmount
DecompressMultiplier uint64
MaxAmount uint64
}
type dlmmAddLiquidityOneSidePreciseArgs struct {
Parameter dlmmAddLiquiditySingleSidePreciseParameter
}
type dlmmAddLiquidityOneSidePrecise2Args struct {
LiquidityParameter dlmmAddLiquiditySingleSidePreciseParameter2
RemainingAccountsInfo dlmmRemainingAccountsInfo
}
type dlmmRemoveLiquidityArgs struct {
BinLiquidityRemoval []dlmmBinLiquidityReduction
}
@@ -264,6 +318,16 @@ type dlmmLiquidityAccounts struct {
tokenYProgramIdx int
}
type dlmmOneSideLiquidityAccounts struct {
positionIdx int
poolIdx int
userTokenIdx int
reserveIdx int
tokenMintIdx int
userIdx int
tokenProgramIdx int
}
var meteoraDlmmEventAuthority = func() solana.PublicKey {
key, _, err := solana.FindProgramAddress([][]byte{[]byte("__event_authority")}, meteoraDlmmProgram)
if err != nil {
@@ -283,7 +347,11 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI
discriminator := *(*[8]byte)(decode[:8])
switch discriminator {
case meteoraInitializeLbPairDiscriminator:
case meteoraInitializeCustomizablePermissionlessLbPairDiscriminator,
meteoraInitializeCustomizablePermissionlessLbPair2Discriminator,
meteoraInitializeLbPairDiscriminator,
meteoraInitializeLbPair2Discriminator,
meteoraInitializePermissionLbPairDiscriminator:
return metaoradlmmInitializeParser(tx, instruction, innerInstructions, offset)
case meteoraDlmmInitializePositionDiscriminator, meteoraDlmmInitializePosition2Discriminator,
meteoraDlmmInitializePositionByOperatorDiscriminator, meteoraDlmmInitializePositionPdaDiscriminator:
@@ -294,13 +362,15 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI
return metaoradlmmSwap2Parser(tx, instruction, innerInstructions, offset)
case meteoraDlmmAddLiquidityDiscriminator, meteoraDlmmAddLiquidity2Discriminator,
meteoraDlmmAddLiquidityByStrategyDiscriminator, meteoraDlmmAddLiquidityByStrategy2Discriminator,
meteoraDlmmAddLiquidityByWeightDiscriminator:
meteoraDlmmAddLiquidityByWeightDiscriminator, meteoraDlmmAddLiquidityOneSideDiscriminator,
meteoraDlmmAddLiquidityOneSidePreciseDiscriminator, meteoraDlmmAddLiquidityOneSidePrecise2Discriminator,
meteoraDlmmAddLiquidityByStrategyOneSideDiscriminator:
return metaoradlmmAddLiquidityParser(tx, instruction, innerInstructions, offset)
case meteoraDlmmClaimFeeDiscriminator, meteoraDlmmClaimFee2Discriminator:
return metaoradlmmClaimFeeParser(tx, instruction, innerInstructions, offset)
case meteoraDlmmRebalanceLiquidityDiscriminator:
return metaoradlmmRebalanceLiquidityParser(tx, instruction, innerInstructions, offset)
case meteoraDlmmRemoveLiquidityDiscriminator, meteoraDlmmRemoveLiquidity2Discriminator,
case meteoraDlmmRemoveAllLiquidityDiscriminator, meteoraDlmmRemoveLiquidityDiscriminator, meteoraDlmmRemoveLiquidity2Discriminator,
meteoraDlmmRemoveLiquidityByRangeDiscriminator, meteoraDlmmRemoveLiquidityByRange2Discriminator:
return metaoradlmmRemoveLiquidityParser(tx, instruction, innerInstructions, offset)
case meteoraDlmmClosePositionDiscriminator, meteoraDlmmClosePosition2Discriminator, meteoraDlmmClosePositionIfEmptyDiscriminator:
@@ -310,53 +380,131 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI
}
}
type dlmmInitializeAccounts struct {
pool solana.PublicKey
token0 solana.PublicKey
token1 solana.PublicKey
baseTokenProgram solana.PublicKey
quoteTokenProgram solana.PublicKey
user solana.PublicKey
}
func resolveDlmmInitializeAccounts(result *RawTx, data []byte, accounts []int) (dlmmInitializeAccounts, error) {
if len(data) < 8 {
return dlmmInitializeAccounts{}, fmt.Errorf("instruction data too short")
}
accountList := result.getAccountList()
resolveAt := func(position int) (solana.PublicKey, error) {
if position < 0 || position >= len(accounts) {
return solana.PublicKey{}, fmt.Errorf("accounts too short, missing position %d", position)
}
accountIndex := accounts[position]
if accountIndex < 0 || accountIndex >= len(accountList) {
return solana.PublicKey{}, fmt.Errorf("account index out of range at position %d", position)
}
return accountList[accountIndex], nil
}
resolveCommon := func(poolPos, token0Pos, token1Pos, userPos, baseTokenProgramPos, quoteTokenProgramPos int) (dlmmInitializeAccounts, error) {
pool, err := resolveAt(poolPos)
if err != nil {
return dlmmInitializeAccounts{}, err
}
token0, err := resolveAt(token0Pos)
if err != nil {
return dlmmInitializeAccounts{}, err
}
token1, err := resolveAt(token1Pos)
if err != nil {
return dlmmInitializeAccounts{}, err
}
baseTokenProgram, err := resolveAt(baseTokenProgramPos)
if err != nil {
return dlmmInitializeAccounts{}, err
}
quoteTokenProgram, err := resolveAt(quoteTokenProgramPos)
if err != nil {
return dlmmInitializeAccounts{}, err
}
user, err := resolveAt(userPos)
if err != nil {
return dlmmInitializeAccounts{}, err
}
return dlmmInitializeAccounts{
pool: pool,
token0: token0,
token1: token1,
baseTokenProgram: baseTokenProgram,
quoteTokenProgram: quoteTokenProgram,
user: user,
}, nil
}
discriminator := *(*[8]byte)(data[:8])
switch discriminator {
case meteoraInitializeLbPairDiscriminator,
meteoraInitializeCustomizablePermissionlessLbPairDiscriminator:
return resolveCommon(0, 2, 3, 8, 9, 9)
case meteoraInitializeLbPair2Discriminator,
meteoraInitializeCustomizablePermissionlessLbPair2Discriminator:
return resolveCommon(0, 2, 3, 8, 11, 12)
case meteoraInitializePermissionLbPairDiscriminator:
return resolveCommon(1, 3, 4, 8, 11, 12)
default:
return dlmmInitializeAccounts{}, fmt.Errorf("unsupported initialize discriminator")
}
}
func metaoradlmmInitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
market := tx.rawTx.accountList[instruction.Accounts[0]]
token0 := tx.rawTx.accountList[instruction.Accounts[2]]
token1 := tx.rawTx.accountList[instruction.Accounts[3]]
accounts, err := resolveDlmmInitializeAccounts(tx.rawTx, instruction.Data, instruction.Accounts)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm initialize accounts parse error: %v, offset, %d, %d", err, offset[0], offset[1])
}
entryContract := tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var baseDecimals uint8
var quoteDecimals uint8
findMintDecimals := func(mint solana.PublicKey) uint8 {
for _, acc := range tx.rawTx.Meta.PostTokenBalances {
if acc.MintAccount.Equals(token0) {
baseDecimals = uint8(acc.UITokenAmount.Decimals)
}
if acc.MintAccount.Equals(token1) {
quoteDecimals = uint8(acc.UITokenAmount.Decimals)
if acc.MintAccount.Equals(mint) {
return uint8(acc.UITokenAmount.Decimals)
}
}
return 0
}
swap := Swap{
Program: SolProgramMeteoraDLMM,
Event: "create",
Pool: market,
BaseMint: token0,
QuoteMint: token1,
BaseTokenProgram: tx.rawTx.accountList[instruction.Accounts[11]],
QuoteTokenProgram: tx.rawTx.accountList[instruction.Accounts[12]],
Pool: accounts.pool,
BaseMint: accounts.token0,
QuoteMint: accounts.token1,
BaseTokenProgram: accounts.baseTokenProgram,
QuoteTokenProgram: accounts.quoteTokenProgram,
Creator: tx.rawTx.accountList[0],
BaseMintDecimals: baseDecimals,
QuoteMintDecimals: quoteDecimals,
User: tx.rawTx.accountList[instruction.Accounts[8]],
BaseMintDecimals: findMintDecimals(accounts.token0),
QuoteMintDecimals: findMintDecimals(accounts.token1),
User: accounts.user,
EntryContract: entryContract,
}
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
createEvent, nextOffset, found, err := dlmmLbPairCreateEventFromInnerInstructions(innerInstructions, instruction, offset)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("pump create get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
return nil, nextOffset, err
}
var programIndex = instruction.ProgramIDIndex
for innerIndex, innerInstr := range inners {
if innerInstr.ProgramIDIndex == programIndex && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraInitializeLbPairEventDiscriminator[:]) {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
if found {
offset = nextOffset
if !createEvent.LbPair.IsZero() {
swap.Pool = createEvent.LbPair
}
break
if !createEvent.TokenX.IsZero() {
swap.BaseMint = createEvent.TokenX
}
if !createEvent.TokenY.IsZero() {
swap.QuoteMint = createEvent.TokenY
}
swap.BaseMintDecimals = findMintDecimals(swap.BaseMint)
swap.QuoteMintDecimals = findMintDecimals(swap.QuoteMint)
}
return []Swap{swap}, offset, nil
}
@@ -518,22 +666,33 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
}
discriminator := *(*[8]byte)(decode[:8])
var swapMode SwapMode
var fixedAmount decimal.Decimal
var limitAmount decimal.Decimal
switch discriminator {
case meteoraDlmmSwapDiscriminator, meteoraDlmmSwap2Discriminator:
var args meteoraDlmmSwapArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm swap decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
swapMode = SwapModeExactIn
fixedAmount = decimal.NewFromUint64(args.AmountIn)
limitAmount = decimal.NewFromUint64(args.MinAmountOut)
case meteoraDlmmSwapExactOutDiscriminator, meteoraDlmmSwapExactOut2Discriminator:
var args meteoraDlmmSwapExactOutArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm swap_exact_out decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
swapMode = SwapModeExactOut
fixedAmount = decimal.NewFromUint64(args.OutAmount)
limitAmount = decimal.NewFromUint64(args.MaxInAmount)
case meteoraDlmmSwapWithPriceImpactDiscriminator, meteoraDlmmSwapWithPriceImpact2Discriminator:
var args meteoraDlmmSwapWithPriceImpactArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm swap_with_price_impact decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
swapMode = SwapModeExactIn
fixedAmount = decimal.NewFromUint64(args.AmountIn)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
@@ -670,6 +829,18 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
userQuote = userQuote.Add(decimal.NewFromUint64(solAmount))
}
}
feeAmount, feeSide, feeMint, feeTokenProgram, feeDecimals := dlmmSwapFeeInfo(
baseIsX,
swapForY,
swapEvent.Fee,
baseMint,
quoteMint,
baseTokenProgram,
quoteTokenProgram,
baseDecimals,
quoteDecimals,
)
lpFeeAmount := dlmmSwapLpFeeAmount(swapEvent.Fee, swapEvent.ProtocolFee, swapEvent.HostFee)
swap := Swap{
Program: SolProgramMeteoraDLMM,
@@ -685,6 +856,13 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
User: eventUser,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
FeeAmount: feeAmount,
FeeBps: dlmmSwapFeeBpsString(swapEvent.FeeBps),
LpFeeAmount: lpFeeAmount,
FeeSide: feeSide,
FeeMint: feeMint,
FeeTokenProgram: feeTokenProgram,
FeeMintDecimals: feeDecimals,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
UserBaseBalance: userBase,
@@ -694,6 +872,7 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
StartBinId: swapEvent.StartBinId,
EndBinId: swapEvent.EndBinId,
}
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
return []Swap{swap}, offset, nil
}
@@ -702,6 +881,39 @@ func metaoradlmmSwap2Parser(tx *Tx, instruction Instruction, innerInstructions I
return metaoradlmmSwapParser(tx, instruction, innerInstructions, offset)
}
func dlmmSwapFeeInfo(
baseIsX bool,
swapForY bool,
fee uint64,
baseMint solana.PublicKey,
quoteMint solana.PublicKey,
baseTokenProgram solana.PublicKey,
quoteTokenProgram solana.PublicKey,
baseDecimals uint8,
quoteDecimals uint8,
) (decimal.Decimal, string, solana.PublicKey, solana.PublicKey, uint8) {
feeAmount := decimal.NewFromUint64(fee)
if baseIsX == swapForY {
return feeAmount, "base", baseMint, baseTokenProgram, baseDecimals
}
return feeAmount, "quote", quoteMint, quoteTokenProgram, quoteDecimals
}
func dlmmSwapLpFeeAmount(fee, protocolFee, hostFee uint64) decimal.Decimal {
total := decimal.NewFromUint64(fee)
protocol := decimal.NewFromUint64(protocolFee)
host := decimal.NewFromUint64(hostFee)
lpFee := total.Sub(protocol).Sub(host)
if lpFee.IsNegative() {
return decimal.Zero
}
return lpFee
}
func dlmmSwapFeeBpsString(feeBps agbinary.Uint128) string {
return feeBps.DecimalString()
}
func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
result := tx.rawTx
@@ -729,7 +941,7 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
weightDist []dlmmBinLiquidityDistributionByWeight
startBinId int32
endBinId int32
hasRange bool
oneSide bool
)
switch discriminator {
@@ -742,7 +954,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
amountY = args.LiquidityParameter.AmountY
binDist = args.LiquidityParameter.BinLiquidityDist
startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist)
hasRange = len(binDist) > 0
case meteoraDlmmAddLiquidity2Discriminator:
var args dlmmAddLiquidity2Args
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
@@ -752,7 +963,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
amountY = args.LiquidityParameter.AmountY
binDist = args.LiquidityParameter.BinLiquidityDist
startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist)
hasRange = len(binDist) > 0
case meteoraDlmmAddLiquidityByStrategyDiscriminator:
var args dlmmAddLiquidityByStrategyArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
@@ -762,7 +972,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
amountY = args.LiquidityParameter.AmountY
startBinId = args.LiquidityParameter.StrategyParameters.MinBinId
endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId
hasRange = true
case meteoraDlmmAddLiquidityByStrategy2Discriminator:
var args dlmmAddLiquidityByStrategy2Args
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
@@ -772,7 +981,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
amountY = args.LiquidityParameter.AmountY
startBinId = args.LiquidityParameter.StrategyParameters.MinBinId
endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId
hasRange = true
case meteoraDlmmAddLiquidityByWeightDiscriminator:
var args dlmmAddLiquidityByWeightArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
@@ -782,16 +990,40 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
amountY = args.LiquidityParameter.AmountY
weightDist = args.LiquidityParameter.BinLiquidityDist
startBinId, endBinId = dlmmMinMaxBinIdFromWeightDistribution(weightDist)
hasRange = len(weightDist) > 0
case meteoraDlmmAddLiquidityOneSideDiscriminator:
var args dlmmAddLiquidityOneSideArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity one side decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
weightDist = args.LiquidityParameter.BinLiquidityDist
startBinId, endBinId = dlmmMinMaxBinIdFromWeightDistribution(weightDist)
oneSide = true
case meteoraDlmmAddLiquidityOneSidePreciseDiscriminator:
var args dlmmAddLiquidityOneSidePreciseArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity one side precise decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
startBinId, endBinId = dlmmMinMaxBinIDFromCompressedDeposits(args.Parameter.Bins)
oneSide = true
case meteoraDlmmAddLiquidityOneSidePrecise2Discriminator:
var args dlmmAddLiquidityOneSidePrecise2Args
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity one side precise2 decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
startBinId, endBinId = dlmmMinMaxBinIDFromCompressedDeposits(args.LiquidityParameter.Bins)
oneSide = true
case meteoraDlmmAddLiquidityByStrategyOneSideDiscriminator:
var args dlmmAddLiquidityByStrategyOneSideArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity by strategy one side decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
startBinId = args.LiquidityParameter.StrategyParameters.MinBinId
endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId
oneSide = true
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
accounts, err := resolveDlmmLiquidityAccounts(result, instruction.Accounts)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity accounts parse error: %v, offset, %d, %d", err, offset[0], offset[1])
}
addEvent, nextOffset, err := dlmmAddLiquidityEventFromInnerInstructions(innerInstructions, instruction, offset)
if err != nil {
return nil, nextOffset, err
@@ -800,14 +1032,17 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
amountX = addEvent.Amounts[0]
amountY = addEvent.Amounts[1]
binChanges := []DlmmBinLiquidityChange(nil)
if len(binDist) > 0 {
binChanges = dlmmBinChangesFromDistribution(amountX, amountY, binDist)
} else if len(weightDist) > 0 {
// Weight-only params do not preserve per-side amounts for each bin, so keep the affected range only.
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, 0)
} else if hasRange {
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, 0)
if oneSide {
swaps, err := dlmmBuildOneSideAddSwap(tx, instruction, addEvent, startBinId, endBinId, entryContract)
if err != nil {
return nil, offset, err
}
return swaps, offset, nil
}
accounts, err := resolveDlmmLiquidityAccounts(result, instruction.Accounts)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity accounts parse error: %v, offset, %d, %d", err, offset[0], offset[1])
}
pool := result.accountList[accounts.poolIdx]
@@ -840,7 +1075,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
baseAmount = amountYDec
quoteAmount = amountXDec
}
eventUser := result.accountList[accounts.userIdx]
if !addEvent.From.IsZero() {
eventUser = addEvent.From
@@ -901,7 +1135,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
ActiveBinId: addEvent.ActiveBinId,
StartBinId: startBinId,
EndBinId: endBinId,
BinChanges: binChanges,
PositionAccount: result.accountList[accounts.positionIdx],
}
@@ -929,19 +1162,19 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
discriminator := *(*[8]byte)(decode[:8])
var (
binChanges []DlmmBinLiquidityChange
startBinId int32
endBinId int32
removeBp int32
)
switch discriminator {
case meteoraDlmmRemoveAllLiquidityDiscriminator:
removeBp = 10000
case meteoraDlmmRemoveLiquidityDiscriminator:
var args dlmmRemoveLiquidityArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm remove liquidity decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
binChanges = dlmmBinChangesFromReduction(args.BinLiquidityRemoval)
startBinId, endBinId = dlmmMinMaxBinIdFromReduction(args.BinLiquidityRemoval)
removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval)
case meteoraDlmmRemoveLiquidity2Discriminator:
@@ -949,7 +1182,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm remove liquidity2 decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
binChanges = dlmmBinChangesFromReduction(args.BinLiquidityRemoval)
startBinId, endBinId = dlmmMinMaxBinIdFromReduction(args.BinLiquidityRemoval)
removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval)
case meteoraDlmmRemoveLiquidityByRangeDiscriminator:
@@ -960,7 +1192,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
startBinId = args.FromBinId
endBinId = args.ToBinId
removeBp = int32(args.BpsToRemove)
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, args.BpsToRemove)
case meteoraDlmmRemoveLiquidityByRange2Discriminator:
var args dlmmRemoveLiquidityByRange2Args
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
@@ -969,7 +1200,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
startBinId = args.FromBinId
endBinId = args.ToBinId
removeBp = int32(args.BpsToRemove)
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, args.BpsToRemove)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
@@ -1015,7 +1245,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
baseAmount = amountYDec
quoteAmount = amountXDec
}
eventUser := result.accountList[accounts.userIdx]
if !removeEvent.From.IsZero() {
eventUser = removeEvent.From
@@ -1077,7 +1306,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
StartBinId: startBinId,
EndBinId: endBinId,
RemoveBp: removeBp,
BinChanges: binChanges,
PositionAccount: result.accountList[accounts.positionIdx],
}
@@ -1321,7 +1549,6 @@ func metaoradlmmRebalanceLiquidityParser(tx *Tx, instruction Instruction, innerI
ActiveBinId: event.ActiveBinId,
StartBinId: event.OldMinBinId,
EndBinId: event.OldMaxBinId,
BinChanges: dlmmBinChangesFromRange(event.OldMinBinId, event.OldMaxBinId, 0),
PositionAccount: result.accountList[accounts.positionIdx],
})
}
@@ -1347,7 +1574,6 @@ func metaoradlmmRebalanceLiquidityParser(tx *Tx, instruction Instruction, innerI
ActiveBinId: event.ActiveBinId,
StartBinId: event.NewMinBinId,
EndBinId: event.NewMaxBinId,
BinChanges: dlmmBinChangesFromRange(event.NewMinBinId, event.NewMaxBinId, 0),
PositionAccount: result.accountList[accounts.positionIdx],
})
}
@@ -1529,6 +1755,51 @@ func dlmmPositionCloseEventFromInnerInstructions(innerInstructions InnerInstruct
return dlmmPositionCloseEvent{}, increaseOffset(offset), false, nil
}
func dlmmLbPairCreateEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmLbPairCreateEvent, [2]uint, bool, error) {
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil {
return dlmmLbPairCreateEvent{}, increaseOffset(offset), false, fmt.Errorf("meteora dlmm create get inner instructions error: %v, offset, %d, %d", err, offset[0], prefixLen)
}
for innerIndex, innerInstr := range inners {
if innerInstr.ProgramIDIndex != instruction.ProgramIDIndex {
continue
}
event, ok := dlmmDecodeLbPairCreateEvent(innerInstr.Data)
if !ok {
continue
}
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
}
return event, offset, true, nil
}
return dlmmLbPairCreateEvent{}, increaseOffset(offset), false, nil
}
func dlmmDecodeLbPairCreateEvent(data []byte) (dlmmLbPairCreateEvent, bool) {
switch {
case len(data) >= 8 && bytes.Equal(data[:8], meteoraInitializeLbPairEventDiscriminator[:]):
var event dlmmLbPairCreateEvent
if err := agbinary.NewBorshDecoder(data[8:]).Decode(&event); err != nil {
return dlmmLbPairCreateEvent{}, false
}
return event, true
case len(data) >= 16 &&
bytes.Equal(data[:8], eventDiscriminator[:]) &&
bytes.Equal(data[8:16], meteoraInitializeLbPairEventDiscriminator[:]):
var event dlmmLbPairCreateEvent
if err := agbinary.NewBorshDecoder(data[16:]).Decode(&event); err != nil {
return dlmmLbPairCreateEvent{}, false
}
return event, true
default:
return dlmmLbPairCreateEvent{}, false
}
}
func dlmmDecodeAddLiquidityEvent(data []byte) (dlmmAddLiquidityEvent, bool) {
switch {
case len(data) >= 8 && bytes.Equal(data[:8], meteoraDlmmAddLiquidityEventDiscriminator[:]):
@@ -1735,16 +2006,12 @@ func resolveDlmmSwapAccounts(result *RawTx, accounts []int) (dlmmSwapAccounts, e
if eventAuthorityPos < len(accounts) && accountList[accounts[eventAuthorityPos]].Equals(solana.MemoProgramID) {
eventAuthorityPos++
}
programPos := eventAuthorityPos + 1
if programPos >= len(accounts) {
if eventAuthorityPos >= len(accounts) {
continue
}
if !accountList[accounts[eventAuthorityPos]].Equals(meteoraDlmmEventAuthority) {
continue
}
if !accountList[accounts[programPos]].Equals(meteoraDlmmProgram) {
continue
}
if hostFeePresent && oraclePos+1 < len(accounts) && dlmmIsSigner(result, accounts[oraclePos+1]) {
continue
@@ -1822,6 +2089,154 @@ func resolveDlmmLiquidityAccounts(result *RawTx, accounts []int) (dlmmLiquidityA
}, nil
}
func resolveDlmmOneSideLiquidityAccounts(result *RawTx, accounts []int) (dlmmOneSideLiquidityAccounts, error) {
if len(accounts) < 10 {
return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("accounts too short, expected at least 10")
}
accountList := result.accountList
eventAuthorityPos := -1
for i, idx := range accounts {
if idx < 0 || idx >= len(accountList) {
continue
}
if accountList[idx].Equals(meteoraDlmmEventAuthority) {
eventAuthorityPos = i
break
}
}
if eventAuthorityPos == -1 {
return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("event authority not found")
}
if eventAuthorityPos+1 >= len(accounts) || !accountList[accounts[eventAuthorityPos+1]].Equals(meteoraDlmmProgram) {
return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("program id not found after event authority")
}
tokenProgramPos := eventAuthorityPos - 1
userPos := eventAuthorityPos - 2
if tokenProgramPos < 0 || userPos < 0 {
return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("one side liquidity account positions invalid")
}
if len(accounts) < 6 {
return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("accounts too short for one side liquidity parsing")
}
return dlmmOneSideLiquidityAccounts{
positionIdx: accounts[0],
poolIdx: accounts[1],
userTokenIdx: accounts[3],
reserveIdx: accounts[4],
tokenMintIdx: accounts[5],
userIdx: accounts[userPos],
tokenProgramIdx: accounts[tokenProgramPos],
}, nil
}
func dlmmBuildOneSideAddSwap(
tx *Tx,
instruction Instruction,
addEvent dlmmAddLiquidityEvent,
startBinId int32,
endBinId int32,
entryContract solana.PublicKey,
) ([]Swap, error) {
result := tx.rawTx
accounts, err := resolveDlmmOneSideLiquidityAccounts(result, instruction.Accounts)
if err != nil {
return nil, err
}
knownMint := result.accountList[accounts.tokenMintIdx]
knownTokenProgram := result.accountList[accounts.tokenProgramIdx]
knownDecimals, ok := dlmmTokenDecimals(result, accounts.reserveIdx)
if !ok {
knownDecimals, _ = dlmmTokenDecimals(result, accounts.userTokenIdx)
}
knownReserveBalance := getAccountBalanceAfterTx(result, accounts.reserveIdx)
knownUserBalance := getAccountBalanceAfterTx(result, accounts.userTokenIdx)
if knownMint.Equals(wSolMint) {
if solAmount, err := GetSolAfterTx(result, accounts.userIdx); err == nil {
knownUserBalance = knownUserBalance.Add(decimal.NewFromUint64(solAmount))
}
}
eventUser := result.accountList[accounts.userIdx]
if !addEvent.From.IsZero() {
eventUser = addEvent.From
}
positionAccount := result.accountList[accounts.positionIdx]
if !addEvent.Position.IsZero() {
positionAccount = addEvent.Position
}
swap := Swap{
Program: SolProgramMeteoraDLMM,
Event: "add",
Pool: result.accountList[accounts.poolIdx],
User: eventUser,
EntryContract: entryContract,
ActiveBinId: addEvent.ActiveBinId,
StartBinId: startBinId,
EndBinId: endBinId,
PositionAccount: positionAccount,
}
knownIsX := dlmmInferOneSideLiquidityAxis(result, accounts, addEvent)
if knownIsX {
swap.BaseMint = knownMint
swap.BaseTokenProgram = knownTokenProgram
swap.BaseMintDecimals = knownDecimals
swap.BaseAmount = decimal.NewFromUint64(addEvent.Amounts[0])
swap.BaseReserve = knownReserveBalance
swap.UserBaseBalance = knownUserBalance
if _, exists := tx.Token[knownMint]; !exists && !knownMint.Equals(wSolMint) {
tx.Token[knownMint] = TokenMeta{
Mint: knownMint,
Decimals: knownDecimals,
TokenProgram: knownTokenProgram,
}
}
} else {
swap.QuoteMint = knownMint
swap.QuoteTokenProgram = knownTokenProgram
swap.QuoteMintDecimals = knownDecimals
swap.QuoteAmount = decimal.NewFromUint64(addEvent.Amounts[1])
swap.QuoteReserve = knownReserveBalance
swap.UserQuoteBalance = knownUserBalance
if _, exists := tx.Token[knownMint]; !exists && !knownMint.Equals(wSolMint) {
tx.Token[knownMint] = TokenMeta{
Mint: knownMint,
Decimals: knownDecimals,
TokenProgram: knownTokenProgram,
}
}
}
return []Swap{swap}, nil
}
func dlmmInferOneSideLiquidityAxis(result *RawTx, accounts dlmmOneSideLiquidityAccounts, addEvent dlmmAddLiquidityEvent) bool {
knownAmount, ok := dlmmTokenDelta(result, accounts.reserveIdx)
if !ok || knownAmount.IsZero() {
knownAmount, _ = dlmmTokenDelta(result, accounts.userTokenIdx)
}
amountX := decimal.NewFromUint64(addEvent.Amounts[0])
amountY := decimal.NewFromUint64(addEvent.Amounts[1])
switch {
case !knownAmount.IsZero() && knownAmount.Equal(amountX) && !knownAmount.Equal(amountY):
return true
case !knownAmount.IsZero() && knownAmount.Equal(amountY) && !knownAmount.Equal(amountX):
return false
case addEvent.Amounts[0] > 0 && addEvent.Amounts[1] == 0:
return true
case addEvent.Amounts[1] > 0 && addEvent.Amounts[0] == 0:
return false
default:
return true
}
}
func resolveDlmmClaimFeeAccounts(result *RawTx, data []byte, accounts []int) (dlmmLiquidityAccounts, error) {
if len(data) < 8 {
return dlmmLiquidityAccounts{}, fmt.Errorf("instruction data too short")
@@ -1993,56 +2408,67 @@ func dlmmTokenBalanceMeta(result *RawTx, accountIndex int) (TokenBalance, bool)
return TokenBalance{}, false
}
func dlmmBinChangesFromDistribution(amountX, amountY uint64, dist []dlmmBinLiquidityDistribution) []DlmmBinLiquidityChange {
if len(dist) == 0 {
func dlmmAllocateByWeights(total uint64, weights []uint64) []decimal.Decimal {
if len(weights) == 0 {
return nil
}
totalX := decimal.NewFromUint64(amountX)
totalY := decimal.NewFromUint64(amountY)
denom := decimal.NewFromInt(10000)
changes := make([]DlmmBinLiquidityChange, 0, len(dist))
for _, item := range dist {
x := totalX.Mul(decimal.NewFromInt(int64(item.DistributionX))).Div(denom).Truncate(0)
y := totalY.Mul(decimal.NewFromInt(int64(item.DistributionY))).Div(denom).Truncate(0)
changes = append(changes, DlmmBinLiquidityChange{
BinId: item.BinId,
AmountX: x,
AmountY: y,
})
}
return changes
}
func dlmmBinChangesFromReduction(reduction []dlmmBinLiquidityReduction) []DlmmBinLiquidityChange {
if len(reduction) == 0 {
return nil
sumWeights := uint64(0)
for _, weight := range weights {
sumWeights += weight
}
changes := make([]DlmmBinLiquidityChange, 0, len(reduction))
for _, item := range reduction {
changes = append(changes, DlmmBinLiquidityChange{
BinId: item.BinId,
BpsToRemove: item.BpsToRemove,
})
if sumWeights == 0 {
sumWeights = uint64(len(weights))
weights = append([]uint64(nil), weights...)
for i := range weights {
weights[i] = 1
}
return changes
}
func dlmmBinChangesFromRange(startBinId, endBinId int32, bpsToRemove uint16) []DlmmBinLiquidityChange {
if startBinId > endBinId {
startBinId, endBinId = endBinId, startBinId
allocations := make([]decimal.Decimal, len(weights))
remaining := total
for i, weight := range weights {
amount := uint64(0)
if i == len(weights)-1 {
amount = remaining
} else if sumWeights > 0 {
amount = total * weight / sumWeights
if amount > remaining {
amount = remaining
}
count := int(endBinId-startBinId) + 1
if count <= 0 {
return nil
remaining -= amount
}
changes := make([]DlmmBinLiquidityChange, 0, count)
for binId := startBinId; binId <= endBinId; binId++ {
changes = append(changes, DlmmBinLiquidityChange{
BinId: binId,
BpsToRemove: bpsToRemove,
})
allocations[i] = decimal.NewFromUint64(amount)
}
return changes
return allocations
}
func dlmmApplySignedAllocation(values []decimal.Decimal, negative bool) []decimal.Decimal {
if !negative {
return values
}
out := make([]decimal.Decimal, len(values))
for i, value := range values {
out[i] = value.Neg()
}
return out
}
func dlmmMinMaxBinIDFromCompressedDeposits(bins []dlmmCompressedBinDepositAmount) (startBinID, endBinID int32) {
if len(bins) == 0 {
return 0, 0
}
startBinID = bins[0].BinID
endBinID = bins[0].BinID
for _, bin := range bins[1:] {
if bin.BinID < startBinID {
startBinID = bin.BinID
}
if bin.BinID > endBinID {
endBinID = bin.BinID
}
}
return startBinID, endBinID
}
func dlmmCommonRemoveBp(reduction []dlmmBinLiquidityReduction) int32 {

468
metaoradlmm_test.go Normal file
View File

@@ -0,0 +1,468 @@
package pump_parser
import (
"bytes"
"testing"
agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func testPublicKey(seed byte) solana.PublicKey {
buf := make([]byte, solana.PublicKeyLength)
for i := range buf {
buf[i] = seed
}
return solana.PublicKeyFromBytes(buf)
}
func seqInts(n int) []int {
out := make([]int, n)
for i := range out {
out[i] = i
}
return out
}
func mustBorshEncode(t *testing.T, value any) []byte {
t.Helper()
var buf bytes.Buffer
if err := agbinary.NewBorshEncoder(&buf).Encode(value); err != nil {
t.Fatalf("borsh encode failed: %v", err)
}
return buf.Bytes()
}
func TestMeteoraDlmmInitializeParserCompatibility(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
discriminator [8]byte
accountCount int
wantPoolPos int
wantBaseMintPos int
wantQuoteMintPos int
wantUserPos int
wantBaseProgramPos int
wantQuoteProgramPos int
}{
{
name: "initialize_lb_pair",
discriminator: meteoraInitializeLbPairDiscriminator,
accountCount: 14,
wantPoolPos: 0,
wantBaseMintPos: 2,
wantQuoteMintPos: 3,
wantUserPos: 8,
wantBaseProgramPos: 9,
wantQuoteProgramPos: 9,
},
{
name: "initialize_lb_pair2",
discriminator: meteoraInitializeLbPair2Discriminator,
accountCount: 16,
wantPoolPos: 0,
wantBaseMintPos: 2,
wantQuoteMintPos: 3,
wantUserPos: 8,
wantBaseProgramPos: 11,
wantQuoteProgramPos: 12,
},
{
name: "initialize_customizable_permissionless_lb_pair",
discriminator: meteoraInitializeCustomizablePermissionlessLbPairDiscriminator,
accountCount: 14,
wantPoolPos: 0,
wantBaseMintPos: 2,
wantQuoteMintPos: 3,
wantUserPos: 8,
wantBaseProgramPos: 9,
wantQuoteProgramPos: 9,
},
{
name: "initialize_customizable_permissionless_lb_pair2",
discriminator: meteoraInitializeCustomizablePermissionlessLbPair2Discriminator,
accountCount: 17,
wantPoolPos: 0,
wantBaseMintPos: 2,
wantQuoteMintPos: 3,
wantUserPos: 8,
wantBaseProgramPos: 11,
wantQuoteProgramPos: 12,
},
{
name: "initialize_permission_lb_pair",
discriminator: meteoraInitializePermissionLbPairDiscriminator,
accountCount: 17,
wantPoolPos: 1,
wantBaseMintPos: 3,
wantQuoteMintPos: 4,
wantUserPos: 8,
wantBaseProgramPos: 11,
wantQuoteProgramPos: 12,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
accountList := make([]solana.PublicKey, 32)
for i := range accountList {
accountList[i] = testPublicKey(byte(i + 1))
}
programIndex := 30
accountList[programIndex] = meteoraDlmmProgram
instruction := Instruction{
Accounts: seqInts(tc.accountCount),
Data: solana.Base58(tc.discriminator[:]),
ProgramIDIndex: programIndex,
}
rawTx := &RawTx{
accountList: accountList,
Meta: Meta{
PostTokenBalances: []TokenBalance{
{
MintAccount: accountList[tc.wantBaseMintPos],
UITokenAmount: UITokenAmount{
Decimals: 6,
},
},
{
MintAccount: accountList[tc.wantQuoteMintPos],
UITokenAmount: UITokenAmount{
Decimals: 9,
},
},
},
},
Transaction: Transaction{
Message: Message{
Instructions: []Instruction{instruction},
},
},
}
tx := &Tx{rawTx: rawTx}
swaps, _, err := metaoradlmmParser(tx, instruction, InnerInstructions{}, [2]uint{0, 0})
if err != nil {
t.Fatalf("metaoradlmmParser() error = %v", err)
}
if len(swaps) != 1 {
t.Fatalf("metaoradlmmParser() swaps len = %d, want 1", len(swaps))
}
swap := swaps[0]
if !swap.Pool.Equals(accountList[tc.wantPoolPos]) {
t.Fatalf("swap.Pool = %s, want %s", swap.Pool, accountList[tc.wantPoolPos])
}
if !swap.BaseMint.Equals(accountList[tc.wantBaseMintPos]) {
t.Fatalf("swap.BaseMint = %s, want %s", swap.BaseMint, accountList[tc.wantBaseMintPos])
}
if !swap.QuoteMint.Equals(accountList[tc.wantQuoteMintPos]) {
t.Fatalf("swap.QuoteMint = %s, want %s", swap.QuoteMint, accountList[tc.wantQuoteMintPos])
}
if !swap.User.Equals(accountList[tc.wantUserPos]) {
t.Fatalf("swap.User = %s, want %s", swap.User, accountList[tc.wantUserPos])
}
if !swap.BaseTokenProgram.Equals(accountList[tc.wantBaseProgramPos]) {
t.Fatalf("swap.BaseTokenProgram = %s, want %s", swap.BaseTokenProgram, accountList[tc.wantBaseProgramPos])
}
if !swap.QuoteTokenProgram.Equals(accountList[tc.wantQuoteProgramPos]) {
t.Fatalf("swap.QuoteTokenProgram = %s, want %s", swap.QuoteTokenProgram, accountList[tc.wantQuoteProgramPos])
}
if swap.BaseMintDecimals != 6 {
t.Fatalf("swap.BaseMintDecimals = %d, want 6", swap.BaseMintDecimals)
}
if swap.QuoteMintDecimals != 9 {
t.Fatalf("swap.QuoteMintDecimals = %d, want 9", swap.QuoteMintDecimals)
}
if !swap.EntryContract.Equals(meteoraDlmmProgram) {
t.Fatalf("swap.EntryContract = %s, want %s", swap.EntryContract, meteoraDlmmProgram)
}
})
}
}
func TestDlmmDecodeLbPairCreateEvent(t *testing.T) {
t.Parallel()
event := dlmmLbPairCreateEvent{
LbPair: testPublicKey(90),
BinStep: 42,
TokenX: testPublicKey(91),
TokenY: testPublicKey(92),
}
body := mustBorshEncode(t, event)
barePayload := append(append([]byte{}, meteoraInitializeLbPairEventDiscriminator[:]...), body...)
decodedBare, ok := dlmmDecodeLbPairCreateEvent(barePayload)
if !ok {
t.Fatalf("dlmmDecodeLbPairCreateEvent() failed for bare payload")
}
if decodedBare != event {
t.Fatalf("decoded bare event = %+v, want %+v", decodedBare, event)
}
anchorPayload := append(append(append([]byte{}, eventDiscriminator[:]...), meteoraInitializeLbPairEventDiscriminator[:]...), body...)
decodedAnchor, ok := dlmmDecodeLbPairCreateEvent(anchorPayload)
if !ok {
t.Fatalf("dlmmDecodeLbPairCreateEvent() failed for anchor payload")
}
if decodedAnchor != event {
t.Fatalf("decoded anchor event = %+v, want %+v", decodedAnchor, event)
}
}
func TestResolveDlmmSwapAccountsAllowsRemainingAccountsAfterEventAuthority(t *testing.T) {
t.Parallel()
accountList := make([]solana.PublicKey, 40)
for i := range accountList {
accountList[i] = testPublicKey(byte(i + 1))
}
accountList[0] = testPublicKey(200)
accountList[26] = meteoraDlmmProgram
accountList[27] = solana.MemoProgramID
accountList[29] = solana.TokenProgramID
accountList[33] = meteoraDlmmEventAuthority
rawTx := &RawTx{
accountList: accountList,
Transaction: Transaction{
Message: Message{
AccountKeys: accountList[:11],
Header: Header{
NumRequiredSignatures: 1,
},
},
},
}
accounts := []int{13, 26, 16, 14, 11, 4, 35, 28, 15, 26, 0, 29, 29, 27, 33, 29, 3, 7, 2}
resolved, err := resolveDlmmSwapAccounts(rawTx, accounts)
if err != nil {
t.Fatalf("resolveDlmmSwapAccounts() error = %v", err)
}
if resolved.poolIdx != 13 {
t.Fatalf("poolIdx = %d, want 13", resolved.poolIdx)
}
if resolved.reserveXIdx != 16 || resolved.reserveYIdx != 14 {
t.Fatalf("reserve indexes = %d/%d, want 16/14", resolved.reserveXIdx, resolved.reserveYIdx)
}
if resolved.userIdx != 0 {
t.Fatalf("userIdx = %d, want 0", resolved.userIdx)
}
if resolved.tokenXProgramIdx != 29 || resolved.tokenYProgramIdx != 29 {
t.Fatalf("token program indexes = %d/%d, want 29/29", resolved.tokenXProgramIdx, resolved.tokenYProgramIdx)
}
}
func TestMeteoraDlmmInitializeParserUsesLbPairCreateEvent(t *testing.T) {
t.Parallel()
accountList := make([]solana.PublicKey, 32)
for i := range accountList {
accountList[i] = testPublicKey(byte(i + 1))
}
programIndex := 30
accountList[programIndex] = meteoraDlmmProgram
instruction := Instruction{
Accounts: seqInts(16),
Data: solana.Base58(meteoraInitializeLbPair2Discriminator[:]),
ProgramIDIndex: programIndex,
}
event := dlmmLbPairCreateEvent{
LbPair: testPublicKey(111),
BinStep: 25,
TokenX: testPublicKey(112),
TokenY: testPublicKey(113),
}
innerEventData := append(
append(append([]byte{}, eventDiscriminator[:]...), meteoraInitializeLbPairEventDiscriminator[:]...),
mustBorshEncode(t, event)...,
)
rawTx := &RawTx{
accountList: accountList,
Meta: Meta{
PostTokenBalances: []TokenBalance{
{
MintAccount: accountList[2],
UITokenAmount: UITokenAmount{
Decimals: 6,
},
},
{
MintAccount: accountList[3],
UITokenAmount: UITokenAmount{
Decimals: 9,
},
},
},
InnerInstructions: []InnerInstructions{
{
Index: 0,
Instructions: []Instruction{
{
ProgramIDIndex: programIndex,
Data: solana.Base58(innerEventData),
},
},
},
},
},
Transaction: Transaction{
Message: Message{
Instructions: []Instruction{instruction},
},
},
}
tx := &Tx{rawTx: rawTx}
swaps, nextOffset, err := metaoradlmmParser(tx, instruction, rawTx.Meta.InnerInstructions[0], [2]uint{0, 0})
if err != nil {
t.Fatalf("metaoradlmmParser() error = %v", err)
}
if len(swaps) != 1 {
t.Fatalf("metaoradlmmParser() swaps len = %d, want 1", len(swaps))
}
swap := swaps[0]
if !swap.Pool.Equals(event.LbPair) {
t.Fatalf("swap.Pool = %s, want event %s", swap.Pool, event.LbPair)
}
if !swap.BaseMint.Equals(event.TokenX) {
t.Fatalf("swap.BaseMint = %s, want event %s", swap.BaseMint, event.TokenX)
}
if !swap.QuoteMint.Equals(event.TokenY) {
t.Fatalf("swap.QuoteMint = %s, want event %s", swap.QuoteMint, event.TokenY)
}
if nextOffset != ([2]uint{1, 0}) {
t.Fatalf("nextOffset = %#v, want [2]uint{1, 0}", nextOffset)
}
}
func TestDlmmSwapFeeInfo(t *testing.T) {
t.Parallel()
baseMint := testPublicKey(1)
quoteMint := testPublicKey(2)
baseProgram := testPublicKey(3)
quoteProgram := testPublicKey(4)
testCases := []struct {
name string
baseIsX bool
swapForY bool
wantFeeSide string
wantFeeMint solana.PublicKey
wantFeeProg solana.PublicKey
wantDecimals uint8
}{
{
name: "x is base and input is x",
baseIsX: true,
swapForY: true,
wantFeeSide: "base",
wantFeeMint: baseMint,
wantFeeProg: baseProgram,
wantDecimals: 6,
},
{
name: "x is base and input is y",
baseIsX: true,
swapForY: false,
wantFeeSide: "quote",
wantFeeMint: quoteMint,
wantFeeProg: quoteProgram,
wantDecimals: 9,
},
{
name: "y is base and input is x",
baseIsX: false,
swapForY: true,
wantFeeSide: "quote",
wantFeeMint: quoteMint,
wantFeeProg: quoteProgram,
wantDecimals: 9,
},
{
name: "y is base and input is y",
baseIsX: false,
swapForY: false,
wantFeeSide: "base",
wantFeeMint: baseMint,
wantFeeProg: baseProgram,
wantDecimals: 6,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
feeAmount, feeSide, feeMint, feeProgram, feeDecimals := dlmmSwapFeeInfo(
tc.baseIsX,
tc.swapForY,
123,
baseMint,
quoteMint,
baseProgram,
quoteProgram,
6,
9,
)
if !feeAmount.Equal(decimal.NewFromInt(123)) {
t.Fatalf("feeAmount = %s, want 123", feeAmount)
}
if feeSide != tc.wantFeeSide {
t.Fatalf("feeSide = %s, want %s", feeSide, tc.wantFeeSide)
}
if !feeMint.Equals(tc.wantFeeMint) {
t.Fatalf("feeMint = %s, want %s", feeMint, tc.wantFeeMint)
}
if !feeProgram.Equals(tc.wantFeeProg) {
t.Fatalf("feeProgram = %s, want %s", feeProgram, tc.wantFeeProg)
}
if feeDecimals != tc.wantDecimals {
t.Fatalf("feeDecimals = %d, want %d", feeDecimals, tc.wantDecimals)
}
})
}
}
func TestDlmmSwapLpFeeAmount(t *testing.T) {
t.Parallel()
lpFee := dlmmSwapLpFeeAmount(100, 15, 5)
if !lpFee.Equal(decimal.NewFromInt(80)) {
t.Fatalf("lpFee = %s, want 80", lpFee)
}
lpFee = dlmmSwapLpFeeAmount(10, 8, 5)
if !lpFee.IsZero() {
t.Fatalf("lpFee should floor at zero, got %s", lpFee)
}
}
func TestDlmmSwapFeeBpsString(t *testing.T) {
t.Parallel()
feeBps := agbinary.Uint128{Lo: 12345}
if got := dlmmSwapFeeBpsString(feeBps); got != "12345" {
t.Fatalf("dlmmSwapFeeBpsString() = %s, want 12345", got)
}
}

View File

@@ -2,8 +2,10 @@ package pump_parser
import (
"bytes"
"encoding/base64"
"encoding/binary"
"fmt"
"strings"
agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
@@ -15,6 +17,19 @@ type metaoraPoolInitializePoolData struct {
TokenBAmount uint64 `json:"tokenBAmount"`
}
type metaoraPoolSwapArgs struct {
InAmount uint64
MinimumOutAmount uint64
}
type metaoraPoolSwapEvent struct {
InAmount uint64
OutAmount uint64
TradeFee uint64
ProtocolFee uint64
HostFee uint64
}
var (
meteoraVaultProgram = solana.MustPublicKeyFromBase58("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi")
meteoraVaultDepositDiscriminator = []byte{0xf2, 0x23, 0xc6, 0x89, 0x52, 0xe1, 0xf2, 0xb6}
@@ -726,6 +741,14 @@ 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 {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode meteora pools swap args: %w", err)
}
pool := tx.rawTx.accountList[instruction.Accounts[0]]
payer := tx.rawTx.accountList[instruction.Accounts[12]]
@@ -846,6 +869,9 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
}
}
if !baseFound || !quoteFound {
if args.InAmount == 0 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
return nil, increaseOffset(offset), fmt.Errorf("failed to find meteora pool event in inner instructions")
}
@@ -874,5 +900,195 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
EntryContract: entryContract,
},
}
fixedSide := fixedSwapAmountSide(event, SwapModeExactIn)
limitSide := oppositeSwapAmountSide(fixedSide)
if fixedSide == SwapAmountSideUnknown || limitSide == SwapAmountSideUnknown {
swaps[0].SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(args.InAmount),
decimal.NewFromUint64(args.MinimumOutAmount),
)
return swaps, offset, nil
}
actualLimitAmount := swapAmountForSide(baseAmount, quoteAmount, limitSide)
if swapEvent, ok := metaoraPoolSwapEventFromInstruction(instruction); ok {
actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount)
} else if swapEvent, ok := metaoraPoolSwapEventForOffset(tx, swapOffset); ok {
actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount)
}
swaps[0].SetSwapAmountInfoDetailed(
SwapModeExactIn,
decimal.NewFromUint64(args.InAmount),
fixedSide,
swapMintForSide(baseMint, quoteMint, fixedSide),
SwapLimitTypeMinOut,
decimal.NewFromUint64(args.MinimumOutAmount),
limitSide,
swapMintForSide(baseMint, quoteMint, limitSide),
actualLimitAmount,
)
return swaps, offset, nil
}
func metaoraPoolSwapEventFromInstruction(instruction Instruction) (metaoraPoolSwapEvent, bool) {
for _, event := range instruction.LogEvents {
if swapEvent, ok := metaoraPoolDecodeSwapEventData(event); ok {
return swapEvent, true
}
}
return metaoraPoolSwapEvent{}, false
}
func metaoraPoolSwapEventForOffset(tx *Tx, offset [2]uint) (metaoraPoolSwapEvent, bool) {
if tx == nil || tx.rawTx == nil {
return metaoraPoolSwapEvent{}, false
}
occurrence := metaoraPoolSwapInstructionOccurrence(tx.rawTx, offset)
if occurrence == 0 {
return metaoraPoolSwapEvent{}, false
}
return metaoraPoolSwapEventFromLogs(tx.rawTx.Meta.LogMessages, occurrence)
}
func metaoraPoolSwapInstructionOccurrence(rawTx *RawTx, offset [2]uint) int {
if rawTx == nil {
return 0
}
accountList := rawTx.getAccountList()
innerByOuter := make(map[int]InnerInstructions, len(rawTx.Meta.InnerInstructions))
for _, inner := range rawTx.Meta.InnerInstructions {
innerByOuter[inner.Index] = inner
}
occurrence := 0
for i, instruction := range rawTx.Transaction.Message.Instructions {
if uint(i) == offset[0] && offset[1] == 0 {
if metaoraPoolIsSwapInstruction(accountList, instruction) {
return occurrence + 1
}
return 0
}
if metaoraPoolIsSwapInstruction(accountList, instruction) {
occurrence++
}
inner := innerByOuter[i]
for j, instruction := range inner.Instructions {
innerOffset := uint(j + 1)
if uint(i) == offset[0] && offset[1] == innerOffset {
if metaoraPoolIsSwapInstruction(accountList, instruction) {
return occurrence + 1
}
return 0
}
if metaoraPoolIsSwapInstruction(accountList, instruction) {
occurrence++
}
}
}
return 0
}
func metaoraPoolIsSwapInstruction(accountList []solana.PublicKey, instruction Instruction) bool {
if instruction.ProgramIDIndex < 0 || instruction.ProgramIDIndex >= len(accountList) {
return false
}
if !accountList[instruction.ProgramIDIndex].Equals(metaoraPoolProgramID) {
return false
}
return len(instruction.Data) >= 8 && bytes.Equal(instruction.Data[:8], metaoraPoolSwapDiscriminator[:])
}
func metaoraPoolSwapEventFromLogs(logMessages []string, occurrence int) (metaoraPoolSwapEvent, bool) {
if occurrence <= 0 {
return metaoraPoolSwapEvent{}, false
}
type frame struct {
program string
sawSwap bool
}
targetProgram := metaoraPoolProgramID.String()
var stack []frame
seen := 0
for _, logMessage := range logMessages {
if program, ok := metaoraPoolLogInvokeProgram(logMessage); ok {
stack = append(stack, frame{program: program})
continue
}
if len(stack) > 0 && stack[len(stack)-1].program == targetProgram {
if logMessage == "Program log: Instruction: Swap" {
stack[len(stack)-1].sawSwap = true
continue
}
if stack[len(stack)-1].sawSwap && strings.HasPrefix(logMessage, "Program data: ") {
event, ok := metaoraPoolDecodeSwapEventLog(strings.TrimSpace(strings.TrimPrefix(logMessage, "Program data: ")))
if ok {
seen++
if seen == occurrence {
return event, true
}
}
}
}
if program, ok := metaoraPoolLogFinishedProgram(logMessage); ok {
for i := len(stack) - 1; i >= 0; i-- {
if stack[i].program == program {
stack = stack[:i]
break
}
}
}
}
return metaoraPoolSwapEvent{}, false
}
func metaoraPoolLogInvokeProgram(logMessage string) (string, bool) {
if !strings.HasPrefix(logMessage, "Program ") || !strings.Contains(logMessage, " invoke [") {
return "", false
}
rest := strings.TrimPrefix(logMessage, "Program ")
program, _, ok := strings.Cut(rest, " ")
return program, ok
}
func metaoraPoolLogFinishedProgram(logMessage string) (string, bool) {
if !strings.HasPrefix(logMessage, "Program ") {
return "", false
}
if !strings.HasSuffix(logMessage, " success") && !strings.Contains(logMessage, " failed:") {
return "", false
}
rest := strings.TrimPrefix(logMessage, "Program ")
program, _, ok := strings.Cut(rest, " ")
return program, ok
}
func metaoraPoolDecodeSwapEventLog(encoded string) (metaoraPoolSwapEvent, bool) {
data, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return metaoraPoolSwapEvent{}, false
}
return metaoraPoolDecodeSwapEventData(data)
}
func metaoraPoolDecodeSwapEventData(data []byte) (metaoraPoolSwapEvent, bool) {
if len(data) < 48 {
return metaoraPoolSwapEvent{}, false
}
if !bytes.Equal(data[:8], metaoraPoolSwapEventDiscriminator[:]) {
return metaoraPoolSwapEvent{}, false
}
body := data[8:]
return metaoraPoolSwapEvent{
InAmount: binary.LittleEndian.Uint64(body[0:8]),
OutAmount: binary.LittleEndian.Uint64(body[8:16]),
TradeFee: binary.LittleEndian.Uint64(body[16:24]),
ProtocolFee: binary.LittleEndian.Uint64(body[24:32]),
HostFee: binary.LittleEndian.Uint64(body[32:40]),
}, true
}

152
metaorapool_test.go Normal file
View File

@@ -0,0 +1,152 @@
package pump_parser
import (
"encoding/base64"
"encoding/binary"
"testing"
"github.com/gagliardetto/solana-go"
)
func TestMetaoraPoolSwapEventFromLogsUsesMatchingInvocation(t *testing.T) {
firstEvent := metaoraPoolSwapEventLogForTest(10, 9, 1, 0, 0)
secondEvent := metaoraPoolSwapEventLogForTest(4013522650, 135, 8043041, 2010760, 0)
logs := []string{
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
"Program log: Instruction: Swap",
"Program data: " + firstEvent,
"Program " + metaoraPoolProgramID.String() + " success",
"Program " + solana.TokenProgramID.String() + " invoke [1]",
"Program data: " + secondEvent,
"Program " + solana.TokenProgramID.String() + " success",
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
"Program log: Instruction: Swap",
"Program data: " + secondEvent,
"Program " + metaoraPoolProgramID.String() + " success",
}
event, ok := metaoraPoolSwapEventFromLogs(logs, 2)
if !ok {
t.Fatal("expected second swap event")
}
if event.InAmount != 4013522650 {
t.Fatalf("in amount = %d, want 4013522650", event.InAmount)
}
if event.OutAmount != 135 {
t.Fatalf("out amount = %d, want 135", event.OutAmount)
}
if event.TradeFee != 8043041 {
t.Fatalf("trade fee = %d, want 8043041", event.TradeFee)
}
if event.ProtocolFee != 2010760 {
t.Fatalf("protocol fee = %d, want 2010760", event.ProtocolFee)
}
}
func TestMetaoraPoolSwapInstructionOccurrenceIncludesInnerInstructions(t *testing.T) {
rawTx := &RawTx{
Transaction: Transaction{
Message: Message{
AccountKeys: solana.PublicKeySlice{
metaoraPoolProgramID,
solana.MustPublicKeyFromBase58("BASDaPs2cdVTsvgPRfESDLZgek8tKRTfqbR2ksdgptsn"),
},
Instructions: []Instruction{
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
{ProgramIDIndex: 1, Data: []byte{1}},
},
},
},
Meta: Meta{
InnerInstructions: []InnerInstructions{
{
Index: 1,
Instructions: []Instruction{
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
},
},
},
},
}
if got := metaoraPoolSwapInstructionOccurrence(rawTx, [2]uint{1, 1}); got != 2 {
t.Fatalf("occurrence = %d, want 2", got)
}
}
func TestAttachLogEventsToInstructions(t *testing.T) {
swapEvent := metaoraPoolSwapEventLogForTest(4013522650, 135, 8043041, 2010760, 0)
rawTx := &RawTx{
Transaction: Transaction{
Message: Message{
AccountKeys: solana.PublicKeySlice{
metaoraPoolProgramID,
solana.TokenProgramID,
},
Instructions: []Instruction{
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
},
},
},
Meta: Meta{
InnerInstructions: []InnerInstructions{
{
Index: 0,
Instructions: []Instruction{
{ProgramIDIndex: 1, Data: []byte{3}, StackHeight: intPtrForTest(2)},
},
},
},
LogMessages: []string{
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
"Program log: Instruction: Swap",
"Program " + solana.TokenProgramID.String() + " invoke [2]",
"Program " + solana.TokenProgramID.String() + " success",
"Program data: " + swapEvent,
"Program " + metaoraPoolProgramID.String() + " success",
},
},
}
applyRawTxConvertLogOptions(rawTx, RawTxConvertOptions{ParseLogEvents: true, IgnoreLogMessages: true})
if len(rawTx.Meta.LogMessages) != 0 {
t.Fatalf("log messages length = %d, want 0", len(rawTx.Meta.LogMessages))
}
if len(rawTx.Transaction.Message.Instructions[0].LogEvents) != 1 {
t.Fatalf("outer log events length = %d, want 1", len(rawTx.Transaction.Message.Instructions[0].LogEvents))
}
if len(rawTx.Meta.InnerInstructions[0].Instructions[0].LogEvents) != 0 {
t.Fatalf("inner log events length = %d, want 0", len(rawTx.Meta.InnerInstructions[0].Instructions[0].LogEvents))
}
event, ok := metaoraPoolSwapEventFromInstruction(rawTx.Transaction.Message.Instructions[0])
if !ok {
t.Fatal("expected swap event from outer instruction")
}
if event.OutAmount != 135 {
t.Fatalf("out amount = %d, want 135", event.OutAmount)
}
}
func metaoraPoolSwapInstructionDataForTest() []byte {
data := make([]byte, 8+16)
copy(data, metaoraPoolSwapDiscriminator[:])
return data
}
func metaoraPoolSwapEventLogForTest(inAmount, outAmount, tradeFee, protocolFee, hostFee uint64) string {
data := make([]byte, 8+40)
copy(data, metaoraPoolSwapEventDiscriminator[:])
binary.LittleEndian.PutUint64(data[8:16], inAmount)
binary.LittleEndian.PutUint64(data[16:24], outAmount)
binary.LittleEndian.PutUint64(data[24:32], tradeFee)
binary.LittleEndian.PutUint64(data[32:40], protocolFee)
binary.LittleEndian.PutUint64(data[40:48], hostFee)
return base64.StdEncoding.EncodeToString(data)
}
func intPtrForTest(value int) *int {
return &value
}

View File

@@ -385,5 +385,12 @@ func metaBcSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerIn
EntryContract: entryContract,
},
}
if swapEvent.Params != nil {
swaps[0].SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(swapEvent.Params.AmountIn),
decimal.NewFromUint64(swapEvent.Params.MinimumAmountOut),
)
}
return swaps, offset, 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) {
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]
@@ -188,6 +194,36 @@ type meteoraDammSwapEvent struct {
ReserveBAmount uint64
}
func meteoraDammSwapAmountInfo(event string, params *struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
_ = event
if params == nil {
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
}
// Meteora DAMM v2 IDL defines:
// - swap: SwapParameters{ amountIn, minimumAmountOut }
// - swap2: SwapParameters2{ amount0, amount1, swapMode }
// - ExactIn / PartialFill: amount0=amount_in, amount1=minimum_amount_out
// - ExactOut: amount0=amount_out, amount1=maximum_amount_in
//
// `SetSwapAmountInfo` derives sides from the normalized buy/sell event, so
// the instruction parameters should stay in raw IDL order here.
switch params.SwapMode {
case 0, 1: // ExactIn / PartialFill
swapMode = SwapModeExactIn
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
case 2: // ExactOut
swapMode = SwapModeExactOut
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
default:
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
}
}
func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 9 {
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
@@ -276,8 +312,7 @@ func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerI
return nil, offset, fmt.Errorf("invalid trade direction")
}
return []Swap{
{
swap := Swap{
Program: SolProgramMeteoraAmmV2,
Event: event,
Pool: swapEvent.Pool,
@@ -296,8 +331,11 @@ func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerI
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
if swapMode, fixedAmount, limitAmount, ok := meteoraDammSwapAmountInfo(event, swapEvent.Params); ok {
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
}
return []Swap{swap}, offset, nil
}
@@ -407,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]]

View File

@@ -1,12 +1,33 @@
package pump_parser
import (
"encoding/binary"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func decodeOrcaWhirlpoolSwapArgs(data []byte) (amount uint64, otherAmountThreshold uint64, amountSpecifiedIsInput bool, err error) {
if len(data) < 42 {
return 0, 0, false, fmt.Errorf("orca whirlpool swap instruction data too short")
}
amount = binary.LittleEndian.Uint64(data[8:16])
otherAmountThreshold = binary.LittleEndian.Uint64(data[16:24])
amountSpecifiedIsInput = data[40] != 0
return amount, otherAmountThreshold, amountSpecifiedIsInput, nil
}
func decodeOrcaWhirlpoolTwoHopSwapArgs(data []byte) (amount uint64, otherAmountThreshold uint64, amountSpecifiedIsInput bool, err error) {
if len(data) < 27 {
return 0, 0, false, fmt.Errorf("orca whirlpool two-hop swap instruction data too short")
}
amount = binary.LittleEndian.Uint64(data[8:16])
otherAmountThreshold = binary.LittleEndian.Uint64(data[16:24])
amountSpecifiedIsInput = data[24] != 0
return amount, otherAmountThreshold, amountSpecifiedIsInput, nil
}
func orcaWhirPoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(orcaProgramID) {
return nil, increaseOffset(offset), fmt.Errorf("orcawhirpoolprogram instruction not found, offset, %d, %d", offset[0], offset[1])
@@ -242,10 +263,10 @@ func orcaWhirPoolLiquidityParser(tx *Tx, instruction Instruction, innerInstructi
//return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
baseAmount = decimal.NewFromInt(int64(amount))
baseAmount = decimal.NewFromUint64(amount)
baseFound = true
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
quoteAmount = decimal.NewFromInt(int64(amount))
quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true
}
if baseFound && quoteFound {
@@ -260,7 +281,7 @@ func orcaWhirPoolLiquidityParser(tx *Tx, instruction Instruction, innerInstructi
return nil, increaseOffset(offset), InstructionIgnoredError
}
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
instructionName += "_on_side"
instructionName += "_one_side"
}
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
@@ -349,10 +370,10 @@ func orcaWhirPoolLiquidityV2Parser(tx *Tx, instruction Instruction, innerInstruc
continue
}
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
baseAmount = decimal.NewFromInt(int64(amount))
baseAmount = decimal.NewFromUint64(amount)
baseFound = true
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
quoteAmount = decimal.NewFromInt(int64(amount))
quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true
}
if baseFound && quoteFound {
@@ -367,7 +388,7 @@ func orcaWhirPoolLiquidityV2Parser(tx *Tx, instruction Instruction, innerInstruc
return nil, offset, InstructionIgnoredError
}
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
instructionName += "_on_side"
instructionName += "_one_side"
}
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
@@ -454,10 +475,10 @@ func orcaWhirPoolCollectFeeParser(tx *Tx, instruction Instruction, innerInstruct
//return nil, increaseOffset(offset), fmt.Errorf("orca whirpool parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
baseAmount = decimal.NewFromInt(int64(amount))
baseAmount = decimal.NewFromUint64(amount)
baseFound = true
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
quoteAmount = decimal.NewFromInt(int64(amount))
quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true
}
if (baseFound && quoteFound) || i >= 6 {
@@ -472,7 +493,7 @@ func orcaWhirPoolCollectFeeParser(tx *Tx, instruction Instruction, innerInstruct
return nil, offset, InstructionIgnoredError
}
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
instructionName += "_on_side"
instructionName += "_one_side"
}
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
@@ -556,10 +577,10 @@ func orcaWhirPoolCollectFeeV2Parser(tx *Tx, instruction Instruction, innerInstru
//return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
baseAmount = decimal.NewFromInt(int64(amount))
baseAmount = decimal.NewFromUint64(amount)
baseFound = true
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
quoteAmount = decimal.NewFromInt(int64(amount))
quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true
}
if (baseFound && quoteFound) || i >= 6 {
@@ -574,7 +595,7 @@ func orcaWhirPoolCollectFeeV2Parser(tx *Tx, instruction Instruction, innerInstru
return nil, offset, InstructionIgnoredError
}
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
instructionName += "_on_side"
instructionName += "_one_side"
}
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
@@ -658,10 +679,10 @@ func orcaWhirPoolCollectProtocolFeeV2Parser(tx *Tx, instruction Instruction, inn
// return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
baseAmount = decimal.NewFromInt(int64(amount))
baseAmount = decimal.NewFromUint64(amount)
baseFound = true
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
quoteAmount = decimal.NewFromInt(int64(amount))
quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true
}
if (baseFound && quoteFound) || i >= 6 {
@@ -676,7 +697,7 @@ func orcaWhirPoolCollectProtocolFeeV2Parser(tx *Tx, instruction Instruction, inn
return nil, offset, InstructionIgnoredError
}
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
instructionName += "_on_side"
instructionName += "_one_side"
}
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
@@ -709,6 +730,14 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amount, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolSwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
swapMode := SwapModeExactOut
if amountSpecifiedIsInput {
swapMode = SwapModeExactIn
}
user := tx.rawTx.accountList[instruction.Accounts[1]]
pool := tx.rawTx.accountList[instruction.Accounts[2]]
@@ -755,7 +784,7 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
baseAmount = decimal.NewFromInt(int64(amount))
baseAmount = decimal.NewFromUint64(amount)
if from.Equals(vault0Account) && to.Equals(token0Account) {
event = "buy"
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
@@ -763,7 +792,7 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
}
baseFound = true
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
quoteAmount = decimal.NewFromInt(int64(amount))
quoteAmount = decimal.NewFromUint64(amount)
if from.Equals(vault1Account) && to.Equals(token1Account) {
event = "sell"
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
@@ -781,8 +810,7 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
return nil, offset, fmt.Errorf("orca whirpool swap failed to find both base and quote token transfer in inner instructions")
}
return []Swap{
{
swap := Swap{
Program: SolProgramOrcaWhirPool,
Event: event,
Pool: pool,
@@ -800,8 +828,10 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
UserQuoteBalance: userQuote,
User: user,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
}
func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -810,6 +840,14 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amount, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolSwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
swapMode := SwapModeExactOut
if amountSpecifiedIsInput {
swapMode = SwapModeExactIn
}
user := tx.rawTx.accountList[instruction.Accounts[3]]
pool := tx.rawTx.accountList[instruction.Accounts[4]]
@@ -856,7 +894,7 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swapv2 parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
baseAmount = decimal.NewFromInt(int64(amount))
baseAmount = decimal.NewFromUint64(amount)
if from.Equals(vault0Account) && to.Equals(token0Account) {
event = "buy"
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
@@ -864,7 +902,7 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
}
baseFound = true
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
quoteAmount = decimal.NewFromInt(int64(amount))
quoteAmount = decimal.NewFromUint64(amount)
if from.Equals(vault1Account) && to.Equals(token1Account) {
event = "sell"
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
@@ -883,8 +921,7 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
}
offset[1] += uint(skipOffset + 1)
return []Swap{
{
swap := Swap{
Program: SolProgramOrcaWhirPool,
Event: event,
Pool: pool,
@@ -902,8 +939,10 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
UserQuoteBalance: userQuote,
User: user,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
}
func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -912,6 +951,14 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amountSpecified, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolTwoHopSwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
swapMode := SwapModeExactOut
if amountSpecifiedIsInput {
swapMode = SwapModeExactIn
}
user := tx.rawTx.accountList[instruction.Accounts[1]]
pool1 := tx.rawTx.accountList[instruction.Accounts[2]]
@@ -964,7 +1011,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
baseAmount = decimal.NewFromInt(int64(amount))
baseAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
event = "buy"
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
@@ -972,7 +1019,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
}
baseFound = true
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
quoteAmount = decimal.NewFromInt(int64(amount))
quoteAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool1UserQuote]) {
event = "sell"
} else if from.Equals(tx.rawTx.accountList[pool1UserQuote]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
@@ -1040,7 +1087,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
baseAmount = decimal.NewFromInt(int64(amount))
baseAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool2UserBase]) {
event = "buy"
} else if from.Equals(tx.rawTx.accountList[pool2UserBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
@@ -1048,7 +1095,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
}
baseFound = true
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
quoteAmount = decimal.NewFromInt(int64(amount))
quoteAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
event = "sell"
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
@@ -1082,6 +1129,30 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
EntryContract: entryContract,
}
}
fixedSide := fixedSwapAmountSide(swaps[0].Event, swapMode)
fixedMint := swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, fixedSide)
limitSide := oppositeSwapAmountSide(fixedSide)
limitMint := swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, limitSide)
actualLimitAmount := swapAmountForSide(swaps[1].BaseAmount, swaps[1].QuoteAmount, limitSide)
if swapMode == SwapModeExactOut {
fixedSide = fixedSwapAmountSide(swaps[1].Event, swapMode)
fixedMint = swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, fixedSide)
limitSide = oppositeSwapAmountSide(fixedSwapAmountSide(swaps[0].Event, swapMode))
limitMint = swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, limitSide)
actualLimitAmount = swapAmountForSide(swaps[0].BaseAmount, swaps[0].QuoteAmount, limitSide)
}
swaps[0].SetSwapAmountInfoDetailed(
swapMode,
decimal.NewFromUint64(amountSpecified),
fixedSide,
fixedMint,
limitSwapAmountType(swapMode),
decimal.NewFromUint64(otherAmountThreshold),
limitSide,
limitMint,
actualLimitAmount,
)
swaps[0].SlippageBps = decimal.Zero
return swaps, offset, nil
}
@@ -1091,6 +1162,14 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amountSpecified, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolTwoHopSwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
swapMode := SwapModeExactOut
if amountSpecifiedIsInput {
swapMode = SwapModeExactIn
}
user := tx.rawTx.accountList[instruction.Accounts[14]]
pool1 := tx.rawTx.accountList[instruction.Accounts[0]]
@@ -1142,7 +1221,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
baseAmount = decimal.NewFromInt(int64(amount))
baseAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
event = "buy"
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
@@ -1150,7 +1229,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
}
baseFound = true
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
quoteAmount = decimal.NewFromInt(int64(amount))
quoteAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
event = "sell"
} else if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
@@ -1216,7 +1295,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
baseAmount = decimal.NewFromInt(int64(amount))
baseAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
event = "buy"
} else if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
@@ -1224,7 +1303,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
}
baseFound = true
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
quoteAmount = decimal.NewFromInt(int64(amount))
quoteAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
event = "sell"
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
@@ -1258,5 +1337,29 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
EntryContract: entryContract,
}
}
fixedSide := fixedSwapAmountSide(swaps[0].Event, swapMode)
fixedMint := swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, fixedSide)
limitSide := oppositeSwapAmountSide(fixedSide)
limitMint := swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, limitSide)
actualLimitAmount := swapAmountForSide(swaps[1].BaseAmount, swaps[1].QuoteAmount, limitSide)
if swapMode == SwapModeExactOut {
fixedSide = fixedSwapAmountSide(swaps[1].Event, swapMode)
fixedMint = swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, fixedSide)
limitSide = oppositeSwapAmountSide(fixedSwapAmountSide(swaps[0].Event, swapMode))
limitMint = swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, limitSide)
actualLimitAmount = swapAmountForSide(swaps[0].BaseAmount, swaps[0].QuoteAmount, limitSide)
}
swaps[0].SetSwapAmountInfoDetailed(
swapMode,
decimal.NewFromUint64(amountSpecified),
fixedSide,
fixedMint,
limitSwapAmountType(swapMode),
decimal.NewFromUint64(otherAmountThreshold),
limitSide,
limitMint,
actualLimitAmount,
)
swaps[0].SlippageBps = decimal.Zero
return swaps, offset, nil
}

23
orcawhirpool_test.go Normal file
View File

@@ -0,0 +1,23 @@
package pump_parser
import "testing"
func TestOrcaWhirlpoolRemoveLiquidityPreservesLargeUint64TransferAmounts(t *testing.T) {
EnableAllParsers()
tx := mustParseRPCFixtureTx(t, "4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri")
if len(tx.Swaps) == 0 {
t.Fatal("expected parsed swaps")
}
swap := tx.Swaps[0]
if swap.Program != SolProgramOrcaWhirPool {
t.Fatalf("program = %s, want %s", swap.Program, SolProgramOrcaWhirPool)
}
if swap.Event != TxEventRemoveLiquidity {
t.Fatalf("event = %s, want %s", swap.Event, TxEventRemoveLiquidity)
}
assertDecimalString(t, "base_amount", swap.BaseAmount, "101086439062")
assertDecimalString(t, "quote_amount", swap.QuoteAmount, "9863327902766042414")
}

722
pump.go
View File

@@ -33,7 +33,7 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct
discriminator := *(*[8]byte)(decode[:8])
switch discriminator {
case pumpBuyV2Discriminator, pumpBuyDiscriminator, pumpSellDiscriminator:
case pumpBuyExactSolInDiscriminator, pumpBuyDiscriminator, pumpBuyV2Discriminator, pumpBuyExactQuoteInV2Discriminator, pumpSellDiscriminator, pumpSellV2Discriminator:
if tx.Err != nil {
return failedTxBuyOrSellParser(tx, instruction, innerInstructions, offset)
}
@@ -43,7 +43,7 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct
return nil, increaseOffset(offset), InstructionIgnoredError
}
return CreateParser(tx, instruction, innerInstructions, offset)
case pumpMigrateDiscriminator:
case pumpMigrateDiscriminator, pumpMigrateV2Discriminator:
if tx.Err != nil {
return nil, increaseOffset(offset), InstructionIgnoredError
}
@@ -89,6 +89,56 @@ type PumpCreateEvent struct {
TokenProgram solana.PublicKey
IsMayhemMode bool
IsCashbackEnabled bool
QuoteMint solana.PublicKey
VirtualQuoteReserves uint64
}
type pumpCreateEventLegacy struct {
Name string
Symbol string
Uri string
Mint solana.PublicKey
BondingCurve solana.PublicKey
User solana.PublicKey
Creator solana.PublicKey
Timestamp int64
VirtualTokenReserves uint64
VirtualSolReserves uint64
RealTokenReserves uint64
TokenTotalSupply uint64
TokenProgram solana.PublicKey
IsMayhemMode bool
IsCashbackEnabled bool
}
func decodePumpCreateEvent(data []byte) (PumpCreateEvent, error) {
var event PumpCreateEvent
if err := agbinary.NewBorshDecoder(data).Decode(&event); err == nil {
return event, nil
}
var legacy pumpCreateEventLegacy
if err := agbinary.NewBorshDecoder(data).Decode(&legacy); err != nil {
return PumpCreateEvent{}, err
}
return PumpCreateEvent{
Name: legacy.Name,
Symbol: legacy.Symbol,
Uri: legacy.Uri,
Mint: legacy.Mint,
BondingCurve: legacy.BondingCurve,
User: legacy.User,
Creator: legacy.Creator,
Timestamp: legacy.Timestamp,
VirtualTokenReserves: legacy.VirtualTokenReserves,
VirtualSolReserves: legacy.VirtualSolReserves,
RealTokenReserves: legacy.RealTokenReserves,
TokenTotalSupply: legacy.TokenTotalSupply,
TokenProgram: legacy.TokenProgram,
IsMayhemMode: legacy.IsMayhemMode,
IsCashbackEnabled: legacy.IsCashbackEnabled,
}, nil
}
func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -106,7 +156,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
}
for innerIndex, innerInstr := range inners {
if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], pumpCreateEventDiscriminator[:]) {
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&createEvent)
createEvent, err = decodePumpCreateEvent(innerInstr.Data[16:])
if offset[1] == 0 {
offset[0] += 1
} else {
@@ -123,12 +173,25 @@ 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)
quoteMint, quoteTokenProgram, quoteDecimals := pumpCreateQuoteAccounts(result, instr, createEvent)
userQuoteBalance := decimal.NewFromUint64(userQuote)
if !quoteMint.IsZero() && !quoteMint.Equals(wSolMint) {
userQuoteBalance = GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint)
}
totalSupply := decimal.NewFromUint64(createEvent.TokenTotalSupply).Div(decimal.New(1, 6))
tx.Token[createEvent.Mint] = TokenMeta{
@@ -146,12 +209,12 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
Event: "create",
Pool: createEvent.BondingCurve,
BaseMint: createEvent.Mint,
QuoteMint: solana.PublicKey{},
QuoteMint: quoteMint,
BaseTokenProgram: createEvent.TokenProgram,
QuoteTokenProgram: solana.PublicKey{},
QuoteTokenProgram: quoteTokenProgram,
Creator: createEvent.Creator,
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
QuoteMintDecimals: quoteDecimals,
User: createEvent.User,
BaseAmount: decimal.Zero,
QuoteAmount: decimal.Zero,
@@ -160,7 +223,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
Mayhem: createEvent.IsMayhemMode,
Cashback: createEvent.IsCashbackEnabled,
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
UserQuoteBalance: userQuoteBalance,
EntryContract: entryContract,
},
}, offset, nil
@@ -197,6 +260,141 @@ type PumpTradeEvent struct {
MayhemMode bool
CashbackFeeBasisPoints uint64
Cashback uint64
BuybackFeeBasisPoints uint64
BuybackFee uint64
Shareholders []PumpShareholder
QuoteMint solana.PublicKey
QuoteAmount uint64
VirtualQuoteReserves uint64
RealQuoteReserves uint64
}
type PumpShareholder struct {
Address solana.PublicKey
ShareBps uint16
}
type pumpTradeEventLegacy struct {
Mint solana.PublicKey
SolAmount uint64
TokenAmount uint64
IsBuy bool
User solana.PublicKey
Timestamp int64
VirtualSolReserves uint64
VirtualTokenReserves uint64
RealSolReserves uint64
RealTokenReserves uint64
FeeRecipient solana.PublicKey
FeeBasisPoints uint64
Fee uint64
Creator solana.PublicKey
CreatorFeeBasisPoints uint64
CreatorFee uint64
TrackVolume bool
TotalUnclaimedTokens uint64
TotalClaimedTokens uint64
CurrentSolVolume uint64
LastUpdateTimestamp int64
IxName string
MayhemMode bool
CashbackFeeBasisPoints uint64
Cashback uint64
}
type pumpTradeEventLegacyV0 struct {
Mint solana.PublicKey
SolAmount uint64
TokenAmount uint64
IsBuy bool
User solana.PublicKey
Timestamp int64
VirtualSolReserves uint64
VirtualTokenReserves uint64
RealSolReserves uint64
RealTokenReserves uint64
FeeRecipient solana.PublicKey
FeeBasisPoints uint64
Fee uint64
Creator solana.PublicKey
CreatorFeeBasisPoints uint64
CreatorFee uint64
TrackVolume bool
TotalUnclaimedTokens uint64
TotalClaimedTokens uint64
CurrentSolVolume uint64
LastUpdateTimestamp int64
IxName string
}
func decodePumpTradeEvent(data []byte) (PumpTradeEvent, error) {
var event PumpTradeEvent
if err := agbinary.NewBorshDecoder(data).Decode(&event); err == nil {
return event, nil
}
var legacy pumpTradeEventLegacy
if err := agbinary.NewBorshDecoder(data).Decode(&legacy); err == nil {
return PumpTradeEvent{
Mint: legacy.Mint,
SolAmount: legacy.SolAmount,
TokenAmount: legacy.TokenAmount,
IsBuy: legacy.IsBuy,
User: legacy.User,
Timestamp: legacy.Timestamp,
VirtualSolReserves: legacy.VirtualSolReserves,
VirtualTokenReserves: legacy.VirtualTokenReserves,
RealSolReserves: legacy.RealSolReserves,
RealTokenReserves: legacy.RealTokenReserves,
FeeRecipient: legacy.FeeRecipient,
FeeBasisPoints: legacy.FeeBasisPoints,
Fee: legacy.Fee,
Creator: legacy.Creator,
CreatorFeeBasisPoints: legacy.CreatorFeeBasisPoints,
CreatorFee: legacy.CreatorFee,
TrackVolume: legacy.TrackVolume,
TotalUnclaimedTokens: legacy.TotalUnclaimedTokens,
TotalClaimedTokens: legacy.TotalClaimedTokens,
CurrentSolVolume: legacy.CurrentSolVolume,
LastUpdateTimestamp: legacy.LastUpdateTimestamp,
IxName: legacy.IxName,
MayhemMode: legacy.MayhemMode,
CashbackFeeBasisPoints: legacy.CashbackFeeBasisPoints,
Cashback: legacy.Cashback,
}, nil
}
var legacyV0 pumpTradeEventLegacyV0
if err := agbinary.NewBorshDecoder(data).Decode(&legacyV0); err != nil {
return PumpTradeEvent{}, err
}
return PumpTradeEvent{
Mint: legacyV0.Mint,
SolAmount: legacyV0.SolAmount,
TokenAmount: legacyV0.TokenAmount,
IsBuy: legacyV0.IsBuy,
User: legacyV0.User,
Timestamp: legacyV0.Timestamp,
VirtualSolReserves: legacyV0.VirtualSolReserves,
VirtualTokenReserves: legacyV0.VirtualTokenReserves,
RealSolReserves: legacyV0.RealSolReserves,
RealTokenReserves: legacyV0.RealTokenReserves,
FeeRecipient: legacyV0.FeeRecipient,
FeeBasisPoints: legacyV0.FeeBasisPoints,
Fee: legacyV0.Fee,
Creator: legacyV0.Creator,
CreatorFeeBasisPoints: legacyV0.CreatorFeeBasisPoints,
CreatorFee: legacyV0.CreatorFee,
TrackVolume: legacyV0.TrackVolume,
TotalUnclaimedTokens: legacyV0.TotalUnclaimedTokens,
TotalClaimedTokens: legacyV0.TotalClaimedTokens,
CurrentSolVolume: legacyV0.CurrentSolVolume,
LastUpdateTimestamp: legacyV0.LastUpdateTimestamp,
IxName: legacyV0.IxName,
}, nil
}
type PumpTradeFeeArg struct {
@@ -218,6 +416,212 @@ type PumpTradeArgs struct {
Amount2 uint64
}
func pumpTradeAmountInfoFromArgs(args PumpTradeArgs) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
switch {
case bytes.Equal(args.Discriminator[:], pumpBuyExactSolInDiscriminator[:]),
bytes.Equal(args.Discriminator[:], pumpBuyExactQuoteInV2Discriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]),
bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]):
return SwapModeExactOut, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]),
bytes.Equal(args.Discriminator[:], pumpSellV2Discriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
default:
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
}
}
type pumpTradeAccountLayout struct {
IsV2 bool
FeeRecipient int
BaseMint int
QuoteMint int
BaseTokenProgram int
QuoteTokenProgram int
Pool int
BasePoolToken int
QuotePoolToken int
User int
BaseUserToken int
QuoteUserToken int
}
func pumpTradeLayout(instr Instruction) (pumpTradeAccountLayout, bool) {
if len(instr.Data) < 8 {
return pumpTradeAccountLayout{}, false
}
discriminator := instr.Data[:8]
switch {
case bytes.Equal(discriminator, pumpBuyDiscriminator[:]), bytes.Equal(discriminator, pumpBuyExactSolInDiscriminator[:]):
if len(instr.Accounts) <= 8 {
return pumpTradeAccountLayout{}, false
}
return pumpTradeAccountLayout{
FeeRecipient: 1,
BaseMint: 2,
QuoteMint: -1,
BaseTokenProgram: 8,
QuoteTokenProgram: -1,
Pool: 3,
BasePoolToken: 4,
QuotePoolToken: -1,
User: 6,
BaseUserToken: 5,
QuoteUserToken: -1,
}, true
case bytes.Equal(discriminator, pumpSellDiscriminator[:]):
if len(instr.Accounts) <= 9 {
return pumpTradeAccountLayout{}, false
}
return pumpTradeAccountLayout{
FeeRecipient: 1,
BaseMint: 2,
QuoteMint: -1,
BaseTokenProgram: 9,
QuoteTokenProgram: -1,
Pool: 3,
BasePoolToken: 4,
QuotePoolToken: -1,
User: 6,
BaseUserToken: 5,
QuoteUserToken: -1,
}, true
case bytes.Equal(discriminator, pumpBuyV2Discriminator[:]),
bytes.Equal(discriminator, pumpBuyExactQuoteInV2Discriminator[:]),
bytes.Equal(discriminator, pumpSellV2Discriminator[:]):
if len(instr.Accounts) <= 15 {
return pumpTradeAccountLayout{}, false
}
return pumpTradeAccountLayout{
IsV2: true,
FeeRecipient: 6,
BaseMint: 1,
QuoteMint: 2,
BaseTokenProgram: 3,
QuoteTokenProgram: 4,
Pool: 10,
BasePoolToken: 11,
QuotePoolToken: 12,
User: 13,
BaseUserToken: 14,
QuoteUserToken: 15,
}, true
default:
return pumpTradeAccountLayout{}, false
}
}
func pumpInstructionIsSell(data []byte) bool {
return len(data) >= 8 && (bytes.Equal(data[:8], pumpSellDiscriminator[:]) || bytes.Equal(data[:8], pumpSellV2Discriminator[:]))
}
func pumpInstructionIsExactQuoteIn(data []byte) bool {
return len(data) >= 8 && (bytes.Equal(data[:8], pumpBuyExactSolInDiscriminator[:]) || bytes.Equal(data[:8], pumpBuyExactQuoteInV2Discriminator[:]))
}
func pumpAccount(result *RawTx, instr Instruction, accountIndex int) solana.PublicKey {
if accountIndex < 0 || accountIndex >= len(instr.Accounts) {
return solana.PublicKey{}
}
listIndex := instr.Accounts[accountIndex]
if listIndex < 0 || listIndex >= len(result.accountList) {
return solana.PublicKey{}
}
return result.accountList[listIndex]
}
func pumpCreateQuoteAccounts(result *RawTx, instr Instruction, createEvent PumpCreateEvent) (solana.PublicKey, solana.PublicKey, uint8) {
quoteMint := createEvent.QuoteMint
quoteTokenProgram := solana.PublicKey{}
optionalStart := -1
if len(instr.Data) >= 8 && bytes.Equal(instr.Data[:8], pumpCreateV2Discriminator[:]) {
optionalStart = 16
}
if optionalStart >= 0 && len(instr.Accounts) > optionalStart {
accountQuoteMint := pumpAccount(result, instr, optionalStart)
if quoteMint.IsZero() && !accountQuoteMint.IsZero() && !accountQuoteMint.Equals(wSolMint) {
quoteMint = accountQuoteMint
}
if len(instr.Accounts) > optionalStart+2 && !quoteMint.IsZero() {
quoteTokenProgram = pumpAccount(result, instr, optionalStart+2)
}
}
if quoteMint.Equals(wSolMint) {
quoteTokenProgram = solana.TokenProgramID
}
if quoteTokenProgram.IsZero() && !quoteMint.IsZero() {
quoteTokenProgram = solana.TokenProgramID
}
return quoteMint, quoteTokenProgram, pumpQuoteDecimals(result, quoteMint)
}
func pumpMintDecimalsFromBalances(result *RawTx, mint solana.PublicKey, fallback uint8) uint8 {
if mint.IsZero() {
return fallback
}
for _, balance := range result.Meta.PostTokenBalances {
balance.ParseAccount()
if balance.MintAccount.Equals(mint) {
return uint8(balance.UITokenAmount.Decimals)
}
}
for _, balance := range result.Meta.PreTokenBalances {
balance.ParseAccount()
if balance.MintAccount.Equals(mint) {
return uint8(balance.UITokenAmount.Decimals)
}
}
return fallback
}
func pumpQuoteDecimals(result *RawTx, quoteMint solana.PublicKey) uint8 {
fallback := uint8(9)
if quoteMint.Equals(usdcMint) || quoteMint.Equals(usd1Mint) {
fallback = 6
}
return pumpMintDecimalsFromBalances(result, quoteMint, fallback)
}
func pumpQuoteAmount(tradeEvent PumpTradeEvent) uint64 {
if tradeEvent.QuoteAmount != 0 {
return tradeEvent.QuoteAmount
}
return tradeEvent.SolAmount
}
func pumpQuoteReserve(tradeEvent PumpTradeEvent) uint64 {
if tradeEvent.RealQuoteReserves != 0 {
return tradeEvent.RealQuoteReserves
}
return tradeEvent.RealSolReserves
}
func pumpCompleteMatchesTradeEvent(completeEvent CompleteEvent, tradeEvent PumpTradeEvent, bondingCurve solana.PublicKey) bool {
if completeEvent.Mint != tradeEvent.Mint {
return false
}
if completeEvent.User != tradeEvent.User {
return false
}
if completeEvent.BondingCurve != bondingCurve {
return false
}
return true
}
func normalizePumpQuoteSideMint(s *Swap) {
if s.FixedAmountSide == SwapAmountSideQuote && s.FixedMint.IsZero() {
s.FixedMint = wSolMint
}
if s.LimitAmountSide == SwapAmountSideQuote && s.LimitMint.IsZero() {
s.LimitMint = wSolMint
}
if s.ActualLimitAmountSide == SwapAmountSideQuote && s.LimitMint.IsZero() {
s.LimitMint = wSolMint
}
}
func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if tx.Err == nil || tx.Err.UnKnown != "" {
return nil, increaseOffset(offset), fmt.Errorf("tx pump failed but error is nil, offset, %d, %d", offset[0], offset[1])
@@ -241,10 +645,16 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions
result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
user := result.accountList[instruction.Accounts[6]]
ataUserIdx := instruction.Accounts[5]
userIndex := instruction.Accounts[6]
mint := result.accountList[instruction.Accounts[2]]
layout, ok := pumpTradeLayout(instruction)
if !ok {
return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction account layout, offset, %d, %d", offset[0], offset[1])
}
user := pumpAccount(result, instruction, layout.User)
ataUserIdx := instruction.Accounts[layout.BaseUserToken]
userIndex := instruction.Accounts[layout.User]
mint := pumpAccount(result, instruction, layout.BaseMint)
quoteMint := pumpAccount(result, instruction, layout.QuoteMint)
quoteTokenProgram := pumpAccount(result, instruction, layout.QuoteTokenProgram)
var args PumpTradeArgs
err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args)
if err != nil {
@@ -252,30 +662,27 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions
}
var event string
var (
solAmount, tokenAmount uint64
quoteAmount, tokenAmount uint64
)
if bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) {
if bytes.Equal(args.Discriminator[:], pumpBuyExactSolInDiscriminator[:]) ||
bytes.Equal(args.Discriminator[:], pumpBuyExactQuoteInV2Discriminator[:]) {
event = "buy_failed"
solAmount = args.Amount1
quoteAmount = args.Amount1
tokenAmount = args.Amount2
} else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) {
} else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) ||
bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) {
event = "buy_failed"
solAmount = args.Amount2
quoteAmount = args.Amount2
tokenAmount = args.Amount1
} else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) {
} else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) ||
bytes.Equal(args.Discriminator[:], pumpSellV2Discriminator[:]) {
event = "sell_failed"
solAmount = args.Amount2
quoteAmount = args.Amount2
tokenAmount = args.Amount1
} else {
return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction discriminator, offset, %d, %d", offset[0], offset[1])
}
var baseTokenProgram solana.PublicKey
if event == "buy_failed" {
baseTokenProgram = result.accountList[instruction.Accounts[8]]
} else {
baseTokenProgram = result.accountList[instruction.Accounts[9]]
}
baseTokenProgram := pumpAccount(result, instruction, layout.BaseTokenProgram)
if !user.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, mint)
//&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount
@@ -287,34 +694,50 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions
}
userBase := getAccountBalanceAfterTx(result, ataUserIdx)
userQuote, _ := GetSolAfterTx(result, userIndex)
userQuote := decimal.Zero
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
userQuote = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuoteUserToken])
} else {
userQuoteLamports, _ := GetSolAfterTx(result, userIndex)
userQuote = decimal.NewFromUint64(userQuoteLamports)
}
bcIdx := instruction.Accounts[3]
bcAtaIndex := instruction.Accounts[4]
bcIdx := instruction.Accounts[layout.Pool]
bcAtaIndex := instruction.Accounts[layout.BasePoolToken]
quoteReserves := decimal.Zero
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
quoteReserves = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuotePoolToken])
} else {
solReserves, _ := GetSolAfterTx(result, bcIdx)
quoteReserves = decimal.NewFromUint64(solReserves)
}
tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex)
swaps := []Swap{
{
Program: SolProgramPump,
Event: event,
Pool: result.accountList[instruction.Accounts[3]],
Pool: pumpAccount(result, instruction, layout.Pool),
BaseMint: mint,
QuoteMint: solana.PublicKey{},
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: solana.PublicKey{},
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
User: user,
BaseAmount: decimal.NewFromUint64(tokenAmount),
QuoteAmount: decimal.NewFromUint64(solAmount),
QuoteAmount: decimal.NewFromUint64(quoteAmount),
BaseReserve: tokenReserves,
QuoteReserve: decimal.NewFromUint64(solReserves),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
QuoteReserve: quoteReserves,
Mayhem: isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
normalizePumpQuoteSideMint(&swaps[0])
}
return swaps, offset, nil
}
@@ -323,6 +746,10 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error
var programIndex = instruction.ProgramIDIndex
layout, ok := pumpTradeLayout(instruction)
if !ok {
return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction account layout, offset, %d, %d", offset[0], offset[1])
}
feeEventProgramIndex := 0
for i, b := range result.accountList {
@@ -337,6 +764,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
completeEvent CompleteEvent
completed bool
newoffset [2]uint
tradeFound bool
)
var prefixLen = offset[1]
@@ -357,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])
@@ -365,7 +796,10 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
}
if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) {
if bytes.Equal(innerInstr.Data[8:16], pumpTradeEventDiscriminator[8:16]) {
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&tradeEvent)
if tradeFound {
break
}
tradeEvent, err = decodePumpTradeEvent(innerInstr.Data[16:])
if offset[1] == 0 {
newoffset = [2]uint{offset[0] + 1, offset[1]}
} else {
@@ -374,39 +808,59 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
if err != nil {
return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
expectedIsBuy := !pumpInstructionIsSell(instruction.Data)
if tradeEvent.IsBuy != expectedIsBuy {
tradeEvent = PumpTradeEvent{}
continue
}
tradeFound = true
if !tradeEvent.IsBuy {
break
}
} else if bytes.Equal(innerInstr.Data[8:16], pumpCompleteEventDiscriminator[:]) {
if !tradeFound {
continue
}
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&completeEvent)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, pumpAccount(result, instruction, layout.Pool)) {
break
}
if offset[1] == 0 {
newoffset = [2]uint{offset[0] + 1, offset[1]}
} else {
newoffset = [2]uint{offset[0], prefixLen + uint(innerIndex) + 1}
}
if err != nil {
return nil, newoffset, fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
completed = true
break
}
}
}
if tradeEvent == (PumpTradeEvent{}) {
if !tradeFound {
return nil, increaseOffset(offset), fmt.Errorf("pmp buy/sell event not found, offset, %d, %d", offset[0], offset[1])
}
offset = [2]uint{newoffset[0], newoffset[1]}
var args PumpTradeArgs
if err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump buy/sell decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
event := ""
baseTokenProgram := solana.TokenProgramID
baseTokenProgram := pumpAccount(result, instruction, layout.BaseTokenProgram)
quoteMint := tradeEvent.QuoteMint
if quoteMint.IsZero() {
quoteMint = pumpAccount(result, instruction, layout.QuoteMint)
}
quoteTokenProgram := pumpAccount(result, instruction, layout.QuoteTokenProgram)
if tradeEvent.IsBuy {
event = "buy"
baseTokenProgram = result.accountList[instruction.Accounts[8]]
} else {
event = "sell"
baseTokenProgram = result.accountList[instruction.Accounts[9]]
}
if _, exists := tx.Token[tradeEvent.Mint]; !exists {
tx.Token[tradeEvent.Mint] = TokenMeta{
@@ -418,8 +872,8 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
var user = tradeEvent.User
ataUserIdx := instruction.Accounts[5]
userIndex := instruction.Accounts[6]
ataUserIdx := instruction.Accounts[layout.BaseUserToken]
userIndex := instruction.Accounts[layout.User]
if !tradeEvent.User.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, tradeEvent.Mint)
//&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount
@@ -431,14 +885,20 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
}
userBase := getAccountBalanceAfterTx(result, ataUserIdx)
userQuote, _ := GetSolAfterTx(result, userIndex)
userQuote := decimal.Zero
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
userQuote = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuoteUserToken])
} else {
userQuoteLamports, _ := GetSolAfterTx(result, userIndex)
userQuote = decimal.NewFromUint64(userQuoteLamports)
}
solAmount := tradeEvent.SolAmount
if tradeEvent.IsBuy && bytes.Equal(instruction.Data[:8], pumpBuyV2Discriminator[:]) {
quoteAmount := pumpQuoteAmount(tradeEvent)
if tradeEvent.IsBuy && pumpInstructionIsExactQuoteIn(instruction.Data) && !layout.IsV2 {
fee := tradeEvent.Fee + tradeEvent.CreatorFee
solAmount = tradeFeeArg.TradeSize
if solAmount > fee {
solAmount = solAmount - fee
quoteAmount = tradeFeeArg.TradeSize
if quoteAmount > fee {
quoteAmount = quoteAmount - fee
}
}
isCashbackCoin := tradeEvent.CashbackFeeBasisPoints > 0 || tradeEvent.Cashback > 0
@@ -446,44 +906,48 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
{
Program: SolProgramPump,
Event: event,
Pool: result.accountList[instruction.Accounts[3]],
Pool: pumpAccount(result, instruction, layout.Pool),
BaseMint: tradeEvent.Mint,
QuoteMint: solana.PublicKey{},
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: solana.PublicKey{},
QuoteTokenProgram: quoteTokenProgram,
Creator: tradeEvent.Creator,
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
User: user,
BaseAmount: decimal.NewFromUint64(tradeEvent.TokenAmount),
QuoteAmount: decimal.NewFromUint64(solAmount),
QuoteAmount: decimal.NewFromUint64(quoteAmount),
BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves),
QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
QuoteReserve: decimal.NewFromUint64(pumpQuoteReserve(tradeEvent)),
Mayhem: tradeEvent.MayhemMode || isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
UserQuoteBalance: userQuote,
EntryContract: entryContract,
Cashback: isCashbackCoin,
},
}
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
normalizePumpQuoteSideMint(&swaps[0])
}
if completed {
swaps = append(swaps, Swap{
Program: SolProgramPump,
Event: "complete",
Pool: result.accountList[instruction.Accounts[3]],
Pool: pumpAccount(result, instruction, layout.Pool),
BaseMint: tradeEvent.Mint,
QuoteMint: solana.PublicKey{},
BaseTokenProgram: result.accountList[instruction.Accounts[8]],
QuoteTokenProgram: solana.PublicKey{},
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
Creator: tradeEvent.Creator,
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
User: user,
BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves),
QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
QuoteReserve: decimal.NewFromUint64(pumpQuoteReserve(tradeEvent)),
Mayhem: tradeEvent.MayhemMode || isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
UserQuoteBalance: userQuote,
EntryContract: entryContract,
})
}
@@ -505,11 +969,74 @@ type MigrateEvent struct {
Pool solana.PublicKey
}
type pumpMigrateAccountLayout struct {
IsV2 bool
BaseMint int
QuoteMint int
Pool int
BasePoolToken int
QuotePoolToken int
User int
BaseTokenProgram int
QuoteTokenProgram int
}
func pumpMigrateLayout(instr Instruction) (pumpMigrateAccountLayout, bool) {
if len(instr.Data) < 8 {
return pumpMigrateAccountLayout{}, false
}
discriminator := instr.Data[:8]
switch {
case bytes.Equal(discriminator, pumpMigrateDiscriminator[:]):
if len(instr.Accounts) <= 14 {
return pumpMigrateAccountLayout{}, false
}
return pumpMigrateAccountLayout{
BaseMint: 2,
QuoteMint: 14,
Pool: 3,
BasePoolToken: 4,
QuotePoolToken: -1,
User: 5,
BaseTokenProgram: 7,
QuoteTokenProgram: -1,
}, true
case bytes.Equal(discriminator, pumpMigrateV2Discriminator[:]):
if len(instr.Accounts) <= 20 {
return pumpMigrateAccountLayout{}, false
}
return pumpMigrateAccountLayout{
IsV2: true,
BaseMint: 2,
QuoteMint: 3,
Pool: 4,
BasePoolToken: 5,
QuotePoolToken: 6,
User: 7,
BaseTokenProgram: 19,
QuoteTokenProgram: 20,
}, true
default:
return pumpMigrateAccountLayout{}, false
}
}
func decimalFromUint64WithFallback(primary, fallback uint64) decimal.Decimal {
if primary != 0 {
return decimal.NewFromUint64(primary)
}
return decimal.NewFromUint64(fallback)
}
func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error
programIndex := instr.ProgramIDIndex
layout, ok := pumpMigrateLayout(instr)
if !ok {
return nil, increaseOffset(offset), fmt.Errorf("unknown pump migrate instruction account layout, offset, %d, %d", offset[0], offset[1])
}
ammprogramIdx := 0
for i, b := range result.accountList {
if b.Equals(pumpAmmProgram) {
@@ -566,20 +1093,45 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction
offset = [2]uint{newoffset[0], newoffset[1]}
// verify migrate by checking create pool and migrate event
userIndex := instr.Accounts[5]
ataBondingCurveAccountIndex := instr.Accounts[4]
userIndex := instr.Accounts[layout.User]
ataBondingCurveAccountIndex := instr.Accounts[layout.BasePoolToken]
bc, err := getTokenBalanceAfterTx(result, ataBondingCurveAccountIndex)
if err != nil || bc == nil {
return nil, increaseOffset(offset), fmt.Errorf("pump migrate get bonding curve balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
baseTokenProgram := bc.ProgramIDAccount
if layout.IsV2 {
baseTokenProgram = pumpAccount(result, instr, layout.BaseTokenProgram)
}
quoteMint := createEvent.QuoteMint
if quoteMint.IsZero() {
quoteMint = pumpAccount(result, instr, layout.QuoteMint)
}
quoteTokenProgram := pumpAccount(result, instr, layout.QuoteTokenProgram)
if quoteTokenProgram.IsZero() && !quoteMint.IsZero() {
quoteTokenProgram = solana.TokenProgramID
}
quoteDecimals := createEvent.QuoteMintDecimals
if quoteDecimals == 0 {
quoteDecimals = pumpQuoteDecimals(result, quoteMint)
}
var userBase decimal.Decimal
if result.accountList[userIndex].Equals(pumpMigrationAccount) {
userBase = decimal.Zero
} else {
userBase = GetTokenBalanceAfterTx(result, userIndex, baseTokenProgram, migrateEvent.Mint)
}
userQuote, _ := GetSolAfterTx(result, userIndex)
userQuote := decimal.Zero
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
userQuote = GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint)
} else {
userQuoteLamports, _ := GetSolAfterTx(result, userIndex)
userQuote = decimal.NewFromUint64(userQuoteLamports)
}
baseAmount := decimalFromUint64WithFallback(createEvent.BaseAmountIn, migrateEvent.MintAmount)
quoteAmount := decimalFromUint64WithFallback(createEvent.QuoteAmountIn, migrateEvent.SolAmount)
baseReserve := decimalFromUint64WithFallback(createEvent.PoolBaseAmount, migrateEvent.MintAmount)
quoteReserve := decimalFromUint64WithFallback(createEvent.PoolQuoteAmount, migrateEvent.SolAmount)
if _, exists := tx.Token[migrateEvent.Mint]; !exists {
tx.Token[migrateEvent.Mint] = TokenMeta{
@@ -594,22 +1146,22 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction
Event: "migrate",
Pool: migrateEvent.BondingCurve,
BaseMint: migrateEvent.Mint,
QuoteMint: solana.PublicKey{},
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: solana.PublicKey{},
QuoteTokenProgram: quoteTokenProgram,
Creator: createEvent.Creator,
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
QuoteMintDecimals: quoteDecimals,
User: migrateEvent.User,
//BaseAmount: decimal.Decimal{},
//QuoteAmount: decimal.Decimal{},
BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount),
QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
Mayhem: createEvent.IsMayhemMode,
MigrateTopProgram: pumpAmmProgram,
MigrateToPool: migrateEvent.Pool,
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}
@@ -618,20 +1170,20 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction
Event: "create",
Pool: migrateEvent.Pool,
BaseMint: migrateEvent.Mint,
QuoteMint: wSolMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: quoteTokenProgram,
Creator: createEvent.Creator,
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
QuoteMintDecimals: quoteDecimals,
User: migrateEvent.User,
BaseAmount: decimal.NewFromUint64(migrateEvent.MintAmount),
QuoteAmount: decimal.NewFromUint64(migrateEvent.SolAmount),
BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount),
QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount),
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
Mayhem: createEvent.IsMayhemMode,
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
UserQuoteBalance: userQuote,
EntryContract: entryContract,
})

View File

@@ -1,6 +1,7 @@
package pump_parser
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
@@ -11,6 +12,31 @@ import (
"github.com/mr-tron/base58"
)
type legacyPumpTradeEvent struct {
Mint solana.PublicKey
SolAmount uint64
TokenAmount uint64
IsBuy bool
User solana.PublicKey
Timestamp int64
VirtualSolReserves uint64
VirtualTokenReserves uint64
RealSolReserves uint64
RealTokenReserves uint64
FeeRecipient solana.PublicKey
FeeBasisPoints uint64
Fee uint64
Creator solana.PublicKey
CreatorFeeBasisPoints uint64
CreatorFee uint64
TrackVolume bool
TotalUnclaimedTokens uint64
TotalClaimedTokens uint64
CurrentSolVolume uint64
LastUpdateTimestamp int64
IxName string
}
func TestTradeEvent(t *testing.T) {
hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e"
d, err := hex.DecodeString(hexData)
@@ -18,13 +44,21 @@ func TestTradeEvent(t *testing.T) {
t.Errorf("Failed to decode base64 data: %v", err)
}
var tradeEvent PumpTradeEvent
var tradeEvent legacyPumpTradeEvent
err = agbinary.NewBorshDecoder(d[16:]).Decode(&tradeEvent)
if err != nil {
t.Errorf("Failed to deserialize trade event: %v", err)
t.Fatalf("Failed to deserialize trade event: %v", err)
}
if tradeEvent.IxName != "buy_exact_sol_in" {
t.Fatalf("IxName = %q, want buy_exact_sol_in", tradeEvent.IxName)
}
if tradeEvent.SolAmount != 11725956 {
t.Fatalf("SolAmount = %d, want 11725956", tradeEvent.SolAmount)
}
if !tradeEvent.IsBuy {
t.Fatalf("IsBuy = false, want true")
}
t.Logf("Trade Event: %+v", tradeEvent)
xx, err := base58.Decode("3Bxs48EzTZB4tzRd")
@@ -43,3 +77,212 @@ func TestCal(t *testing.T) {
fmt.Println(solana.MustPublicKeyFromBase58("BM9CcyErJcu2mjrFvUsRRrD3snGeHDDVirJLvL6EjvMN").IsOnCurve())
}
func TestPumpCompleteMatchesTradeEvent(t *testing.T) {
mint := solana.MustPublicKeyFromBase58("8GNGkNnfBuoTP3QRnmdNzSYuuE15M8tvcNvxNsV4pump")
user := solana.MustPublicKeyFromBase58("DS95KxqUCCjwQaXhD7fhKatXbivwWDNrJdNV5ZcubGdz")
bondingCurve := solana.MustPublicKeyFromBase58("Gz5EX3X7kUDS48baijJKubQDKy3BBKpnMJQ3f3W1e9jA")
tradeEvent := PumpTradeEvent{
Mint: mint,
User: user,
}
completeEvent := CompleteEvent{
Mint: mint,
User: user,
BondingCurve: bondingCurve,
}
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, bondingCurve) {
t.Fatal("pumpCompleteMatchesTradeEvent() = false, want true")
}
completeEvent.User = solana.MustPublicKeyFromBase58("3g89wLRwJ5P22fkCdPJBAP7iiYAo6yY96geQvMYj6tYm")
if pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, bondingCurve) {
t.Fatal("pumpCompleteMatchesTradeEvent() = true for mismatched user")
}
}
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
got [8]byte
want [8]byte
}{
{name: "buy_exact_sol_in", got: pumpBuyExactSolInDiscriminator, want: [8]byte{56, 252, 116, 8, 158, 223, 205, 95}},
{name: "buy_v2", got: pumpBuyV2Discriminator, want: [8]byte{184, 23, 238, 97, 103, 197, 211, 61}},
{name: "buy_exact_quote_in_v2", got: pumpBuyExactQuoteInV2Discriminator, want: [8]byte{194, 171, 28, 70, 104, 77, 91, 47}},
{name: "sell_v2", got: pumpSellV2Discriminator, want: [8]byte{93, 246, 130, 60, 231, 233, 64, 178}},
{name: "create_v2", got: pumpCreateV2Discriminator, want: [8]byte{214, 144, 76, 236, 95, 139, 49, 180}},
{name: "migrate_v2", got: pumpMigrateV2Discriminator, want: [8]byte{187, 203, 18, 31, 206, 237, 254, 41}},
}
for _, tt := range tests {
if tt.got != tt.want {
t.Fatalf("%s discriminator = %v, want %v", tt.name, tt.got, tt.want)
}
}
}
func TestPumpMigrateLayoutV2(t *testing.T) {
accounts := make([]int, 27)
for i := range accounts {
accounts[i] = i
}
layout, ok := pumpMigrateLayout(Instruction{
Data: pumpMigrateV2Discriminator[:],
Accounts: accounts,
})
if !ok {
t.Fatal("migrate_v2 layout not recognized")
}
if !layout.IsV2 ||
layout.BaseMint != 2 ||
layout.QuoteMint != 3 ||
layout.Pool != 4 ||
layout.BasePoolToken != 5 ||
layout.QuotePoolToken != 6 ||
layout.User != 7 ||
layout.BaseTokenProgram != 19 ||
layout.QuoteTokenProgram != 20 {
t.Fatalf("migrate_v2 layout = %+v", layout)
}
}
func TestPumpTradeAmountInfoV2(t *testing.T) {
tests := []struct {
name string
disc [8]byte
wantMode SwapMode
}{
{name: "legacy exact quote in", disc: pumpBuyExactSolInDiscriminator, wantMode: SwapModeExactIn},
{name: "v2 exact quote in", disc: pumpBuyExactQuoteInV2Discriminator, wantMode: SwapModeExactIn},
{name: "v2 buy exact out", disc: pumpBuyV2Discriminator, wantMode: SwapModeExactOut},
{name: "v2 sell exact in", disc: pumpSellV2Discriminator, wantMode: SwapModeExactIn},
}
for _, tt := range tests {
mode, fixed, limit, ok := pumpTradeAmountInfoFromArgs(PumpTradeArgs{
Discriminator: tt.disc,
Amount1: 11,
Amount2: 22,
})
if !ok {
t.Fatalf("%s not recognized", tt.name)
}
if mode != tt.wantMode {
t.Fatalf("%s mode = %s, want %s", tt.name, mode.String(), tt.wantMode.String())
}
if fixed.String() != "11" || limit.String() != "22" {
t.Fatalf("%s fixed/limit = %s/%s, want 11/22", tt.name, fixed, limit)
}
}
}
func TestPumpCreateQuoteAccountsOptional(t *testing.T) {
createAccounts := make([]int, 17)
for i := range createAccounts {
createAccounts[i] = i
}
createV2Accounts := make([]int, 19)
for i := range createV2Accounts {
createV2Accounts[i] = i
}
createV2Accounts[16] = 14
createV2Accounts[18] = 16
accountList := make([]solana.PublicKey, 19)
accountList[14] = usdcMint
accountList[16] = solana.TokenProgramID
accountList[18] = solana.TokenProgramID
result := &RawTx{accountList: accountList}
quoteMint, quoteTokenProgram, quoteDecimals := pumpCreateQuoteAccounts(result, Instruction{
Data: pumpCreateDiscriminator[:],
Accounts: createAccounts,
}, PumpCreateEvent{})
if !quoteMint.IsZero() || !quoteTokenProgram.IsZero() || quoteDecimals != 9 {
t.Fatalf("create quote accounts = %s/%s/%d, want zero/zero/9", quoteMint, quoteTokenProgram, quoteDecimals)
}
quoteMint, quoteTokenProgram, quoteDecimals = pumpCreateQuoteAccounts(result, Instruction{
Data: pumpCreateV2Discriminator[:],
Accounts: createV2Accounts,
}, PumpCreateEvent{})
if !quoteMint.Equals(usdcMint) || !quoteTokenProgram.Equals(solana.TokenProgramID) || quoteDecimals != 6 {
t.Fatalf("create_v2 quote accounts = %s/%s/%d, want USDC/token/6", quoteMint, quoteTokenProgram, quoteDecimals)
}
}
func TestDecodePumpTradeEventV2QuoteFields(t *testing.T) {
user := solana.MustPublicKeyFromBase58("DS95KxqUCCjwQaXhD7fhKatXbivwWDNrJdNV5ZcubGdz")
mint := solana.MustPublicKeyFromBase58("8GNGkNnfBuoTP3QRnmdNzSYuuE15M8tvcNvxNsV4pump")
want := PumpTradeEvent{
Mint: mint,
SolAmount: 1,
TokenAmount: 2,
IsBuy: true,
User: user,
VirtualTokenReserves: 3,
RealTokenReserves: 4,
IxName: "buy_v2",
Shareholders: []PumpShareholder{{Address: user, ShareBps: 250}},
QuoteMint: usdcMint,
QuoteAmount: 5,
VirtualQuoteReserves: 6,
RealQuoteReserves: 7,
}
var buf bytes.Buffer
if err := agbinary.NewBorshEncoder(&buf).Encode(want); err != nil {
t.Fatalf("encode v2 trade event: %v", err)
}
got, err := decodePumpTradeEvent(buf.Bytes())
if err != nil {
t.Fatalf("decodePumpTradeEvent() error = %v", err)
}
if !got.QuoteMint.Equals(usdcMint) || got.QuoteAmount != 5 || got.VirtualQuoteReserves != 6 || got.RealQuoteReserves != 7 {
t.Fatalf("decoded quote fields = %s/%d/%d/%d", got.QuoteMint, got.QuoteAmount, got.VirtualQuoteReserves, got.RealQuoteReserves)
}
if len(got.Shareholders) != 1 || got.Shareholders[0].ShareBps != 250 {
t.Fatalf("decoded shareholders = %+v", got.Shareholders)
}
}
func TestDecodePumpTradeEventLegacyFallback(t *testing.T) {
hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e"
data, err := hex.DecodeString(hexData)
if err != nil {
t.Fatalf("decode hex: %v", err)
}
got, err := decodePumpTradeEvent(data[16:])
if err != nil {
t.Fatalf("decodePumpTradeEvent() legacy error = %v", err)
}
if got.IxName != "buy_exact_sol_in" || got.SolAmount != 11725956 || !got.IsBuy {
t.Fatalf("legacy event = %+v", got)
}
if !got.QuoteMint.IsZero() || got.QuoteAmount != 0 || got.RealQuoteReserves != 0 {
t.Fatalf("legacy quote fields = %s/%d/%d, want zero", got.QuoteMint, got.QuoteAmount, got.RealQuoteReserves)
}
}

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) {
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
@@ -261,7 +264,23 @@ type PumpSwapArgs struct {
Amount2 uint64
}
func pumpAmmSwapAmountInfoFromArgs(args PumpSwapArgs) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
switch {
case bytes.Equal(args.Discriminator[:], pumpAmmBuyV2Discriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpAmmBuyDiscriminator[:]):
return SwapModeExactOut, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpAmmSellDiscriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
default:
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
}
}
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])
}
@@ -361,8 +380,7 @@ func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions Inn
}
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
return []Swap{
{
swap := Swap{
Program: SolProgramPumpAMM,
Event: event,
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
@@ -381,11 +399,17 @@ func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions Inn
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
if swapMode, fixedAmount, limitAmount, ok := pumpAmmSwapAmountInfoFromArgs(args); ok {
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
}
return []Swap{swap}, offset, nil
}
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])
}
@@ -479,8 +503,7 @@ func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions In
}
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
return []Swap{
{
swap := Swap{
Program: SolProgramPumpAMM,
Event: event,
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
@@ -499,8 +522,11 @@ func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions In
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
if swapMode, fixedAmount, limitAmount, ok := pumpAmmSwapAmountInfoFromArgs(args); ok {
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
}
return []Swap{swap}, offset, nil
}
func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -508,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)
@@ -599,8 +628,11 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
}
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
return []Swap{
{
quoteAmount := decimal.NewFromUint64(event.UserQuoteAmountIn)
if event.IxName == "buy" {
quoteAmount = decimal.NewFromUint64(event.QuoteAmountIn)
}
swap := Swap{
Program: SolProgramPumpAMM,
Event: "buy",
Pool: event.Pool,
@@ -613,7 +645,7 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
QuoteMintDecimals: quoteMintDecimals,
User: eventUser,
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountIn),
QuoteAmount: quoteAmount,
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
@@ -621,14 +653,30 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
if bytes.Equal(instruction.Data[:8], pumpAmmBuyV2Discriminator[:]) {
swap.SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(event.UserQuoteAmountIn),
decimal.NewFromUint64(event.MinBaseAmountOut),
)
} else {
swap.SetSwapAmountInfo(
SwapModeExactOut,
decimal.NewFromUint64(event.BaseAmountOut),
decimal.NewFromUint64(event.MaxQuoteAmountIn),
)
}
return []Swap{swap}, offset, nil
}
func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
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 {
@@ -722,8 +770,7 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
}
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
return []Swap{
{
swap := Swap{
Program: SolProgramPumpAMM,
Event: "sell",
Pool: event.Pool,
@@ -744,14 +791,22 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(event.BaseAmountIn),
decimal.NewFromUint64(event.MinQuoteAmountOut),
)
return []Swap{swap}, offset, nil
}
func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
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 {
@@ -850,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 {

185
rawtx.go
View File

@@ -1,8 +1,12 @@
package pump_parser
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
bin "github.com/gagliardetto/binary"
@@ -109,6 +113,7 @@ type Instruction struct {
Data solana.Base58 `json:"data"`
ProgramIDIndex int `json:"programIdIndex"`
StackHeight *int `json:"stackHeight"`
LogEvents []solana.Base64 `json:"logEvents,omitempty"`
}
type InnerInstructions struct {
Index int `json:"index"`
@@ -180,6 +185,11 @@ type Transaction struct {
Signatures []solana.Signature `json:"signatures"`
}
type RawTxConvertOptions struct {
IgnoreLogMessages bool
ParseLogEvents bool
}
func (tx *Transaction) UnmarshalJSON(data []byte) error {
if len(data) == 0 || (len(data) == 4 && string(data) == "null") {
// TODO: is this an error?
@@ -308,7 +318,8 @@ func marshalRpcTransactionErr(err any) string {
return string(e)
}
func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, slot uint64, index int64) (*RawTx, error) {
func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, slot uint64, index int64, options ...RawTxConvertOptions) (*RawTx, error) {
option := rawTxConvertOption(options)
created := int64(0)
if blockTime != nil {
created = int64(*blockTime)
@@ -523,6 +534,8 @@ func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, s
})
}
applyRawTxConvertLogOptions(sTx, option)
return sTx, nil
}
@@ -833,7 +846,8 @@ func isAccountOwner(account, owner, mint solana.PublicKey) (bool, error) {
return account == ata, nil
}
func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateTransaction, created int64) (*RawTx, error) {
func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateTransaction, created int64, options ...RawTxConvertOptions) (*RawTx, error) {
option := rawTxConvertOption(options)
sTx := &RawTx{
BlockTime: created,
Slot: y.Slot,
@@ -863,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 {
@@ -1002,6 +1019,8 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT
})
}
applyRawTxConvertLogOptions(sTx, option)
// resolve the lookups
//{
// if sTx.Transaction.Message.IsVersioned() {
@@ -1021,6 +1040,168 @@ func newInt16(x uint16) *int {
return &y
}
func rawTxConvertOption(options []RawTxConvertOptions) RawTxConvertOptions {
out := RawTxConvertOptions{ParseLogEvents: true}
if len(options) > 0 {
out = options[0]
}
return out
}
func applyRawTxConvertLogOptions(tx *RawTx, option RawTxConvertOptions) {
if tx == nil {
return
}
if option.ParseLogEvents {
attachLogEventsToInstructions(tx, tx.Meta.LogMessages)
}
if option.IgnoreLogMessages {
tx.Meta.LogMessages = nil
}
}
type instructionLogFrame struct {
program string
instr *Instruction
}
type instructionLogTarget struct {
program string
stackHeight int
instr *Instruction
}
func attachLogEventsToInstructions(tx *RawTx, logMessages []string) {
if tx == nil || len(logMessages) == 0 {
return
}
targets := rawTxInstructionLogTargets(tx)
if len(targets) == 0 {
return
}
nextTarget := 0
var stack []instructionLogFrame
for _, logMessage := range logMessages {
if program, stackHeight, ok := parseProgramInvokeLog(logMessage); ok {
var instr *Instruction
for nextTarget < len(targets) {
target := targets[nextTarget]
nextTarget++
if target.program != program {
continue
}
if target.stackHeight != 0 && target.stackHeight != stackHeight {
continue
}
instr = target.instr
break
}
stack = append(stack, instructionLogFrame{program: program, instr: instr})
continue
}
if data, ok := parseProgramDataLog(logMessage); ok {
if len(stack) == 0 {
continue
}
top := stack[len(stack)-1]
if top.instr != nil {
top.instr.LogEvents = append(top.instr.LogEvents, data)
}
continue
}
if program, ok := parseProgramFinishedLog(logMessage); ok {
for i := len(stack) - 1; i >= 0; i-- {
if stack[i].program == program {
stack = stack[:i]
break
}
}
}
}
}
func rawTxInstructionLogTargets(tx *RawTx) []instructionLogTarget {
accountList := tx.getAccountList()
innerByOuter := make(map[int]*InnerInstructions, len(tx.Meta.InnerInstructions))
for i := range tx.Meta.InnerInstructions {
inner := &tx.Meta.InnerInstructions[i]
innerByOuter[inner.Index] = inner
}
out := make([]instructionLogTarget, 0, len(tx.Transaction.Message.Instructions))
for i := range tx.Transaction.Message.Instructions {
instr := &tx.Transaction.Message.Instructions[i]
if instr.ProgramIDIndex >= 0 && instr.ProgramIDIndex < len(accountList) {
out = append(out, instructionLogTarget{
program: accountList[instr.ProgramIDIndex].String(),
stackHeight: 1,
instr: instr,
})
}
if inner := innerByOuter[i]; inner != nil {
for j := range inner.Instructions {
innerInstr := &inner.Instructions[j]
if innerInstr.ProgramIDIndex < 0 || innerInstr.ProgramIDIndex >= len(accountList) {
continue
}
stackHeight := 0
if innerInstr.StackHeight != nil {
stackHeight = *innerInstr.StackHeight
}
out = append(out, instructionLogTarget{
program: accountList[innerInstr.ProgramIDIndex].String(),
stackHeight: stackHeight,
instr: innerInstr,
})
}
}
}
return out
}
func parseProgramInvokeLog(logMessage string) (string, int, bool) {
if !strings.HasPrefix(logMessage, "Program ") {
return "", 0, false
}
rest := strings.TrimPrefix(logMessage, "Program ")
program, suffix, ok := strings.Cut(rest, " invoke [")
if !ok {
return "", 0, false
}
suffix = strings.TrimSuffix(suffix, "]")
stackHeight, err := strconv.Atoi(suffix)
if err != nil {
return "", 0, false
}
return program, stackHeight, true
}
func parseProgramDataLog(logMessage string) (solana.Base64, bool) {
if !strings.HasPrefix(logMessage, "Program data: ") {
return nil, false
}
data, err := base64.StdEncoding.DecodeString(strings.TrimSpace(strings.TrimPrefix(logMessage, "Program data: ")))
if err != nil {
return nil, false
}
return solana.Base64(data), true
}
func parseProgramFinishedLog(logMessage string) (string, bool) {
if !strings.HasPrefix(logMessage, "Program ") {
return "", false
}
if !strings.HasSuffix(logMessage, " success") && !strings.Contains(logMessage, " failed:") {
return "", false
}
rest := strings.TrimPrefix(logMessage, "Program ")
program, _, ok := strings.Cut(rest, " ")
return program, ok
}
func newInt(x *uint32) *int {
if x == nil {
return nil

1812
rawtx_binary.go Normal file

File diff suppressed because it is too large Load Diff

434
rawtx_binary_test.go Normal file
View File

@@ -0,0 +1,434 @@
package pump_parser
import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
"github.com/shopspring/decimal"
)
func TestRawTxBinaryRoundTripRealFixture(t *testing.T) {
original := mustLoadRawTxFixture(t, "testdata/rpc/4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri.json")
encoded, err := EncodeRawTxBinary(original)
if err != nil {
t.Fatalf("EncodeRawTxBinary() error = %v", err)
}
decoded, err := DecodeRawTxBinary(encoded)
if err != nil {
t.Fatalf("DecodeRawTxBinary() error = %v", err)
}
assertRawTxAccountAccess(t, decoded)
if decoded.TxHash() != original.TxHash() {
t.Fatalf("TxHash = %s, want %s", decoded.TxHash(), original.TxHash())
}
if decoded.Slot != original.Slot {
t.Fatalf("Slot = %d, want %d", decoded.Slot, original.Slot)
}
if decoded.IndexWithinBlock != original.IndexWithinBlock {
t.Fatalf("IndexWithinBlock = %d, want %d", decoded.IndexWithinBlock, original.IndexWithinBlock)
}
if len(decoded.Meta.PostTokenBalances) != len(original.Meta.PostTokenBalances) {
t.Fatalf("PostTokenBalances len = %d, want %d", len(decoded.Meta.PostTokenBalances), len(original.Meta.PostTokenBalances))
}
if len(decoded.Meta.PostTokenBalances) > 0 {
got := decoded.Meta.PostTokenBalances[0]
want := original.Meta.PostTokenBalances[0]
if got.AccountIndex != want.AccountIndex {
t.Fatalf("token balance account index = %d, want %d", got.AccountIndex, want.AccountIndex)
}
wantMint := want.MintAccount
if wantMint.IsZero() && want.Mint != "" {
var err error
wantMint, err = solana.PublicKeyFromBase58(want.Mint)
if err != nil {
t.Fatalf("parse want mint: %v", err)
}
}
if got.MintAccount != wantMint {
t.Fatalf("token balance mint = %s, want %s", got.MintAccount, wantMint)
}
if got.UITokenAmount.Decimals != want.UITokenAmount.Decimals {
t.Fatalf("token balance decimals = %d, want %d", got.UITokenAmount.Decimals, want.UITokenAmount.Decimals)
}
if got.UITokenAmount.Amount != want.UITokenAmount.Amount {
t.Fatalf("token balance amount = %s, want %s", got.UITokenAmount.Amount, want.UITokenAmount.Amount)
}
}
}
func TestRawTxsBinaryBatchAndStreamRoundTrip(t *testing.T) {
tx1 := mustLoadRawTxFixture(t, "testdata/rpc/4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri.json")
tx2 := mustLoadRawTxFixture(t, "testdata/rpc/43EouSYkeVmLBZSKW1KptiQcAVvB6KX49wjztjgzkQ9iU38A6fF68k77bNy9Wn6fwjykqYsPorUKj8m6SFY7naf1.json")
tx2.BlockTime = tx1.BlockTime
original := []RawTx{*tx1, *tx2}
encoded, err := EncodeRawTxsBinary(original)
if err != nil {
t.Fatalf("EncodeRawTxsBinary() error = %v", err)
}
decoded, err := DecodeRawTxsBinary(encoded)
if err != nil {
t.Fatalf("DecodeRawTxsBinary() error = %v", err)
}
if len(decoded) != len(original) {
t.Fatalf("DecodeRawTxsBinary len = %d, want %d", len(decoded), len(original))
}
for i := range decoded {
assertRawTxAccountAccess(t, decoded[i])
if decoded[i].TxHash() != original[i].TxHash() {
t.Fatalf("decoded[%d].TxHash = %s, want %s", i, decoded[i].TxHash(), original[i].TxHash())
}
if decoded[i].BlockTime != original[i].BlockTime {
t.Fatalf("decoded[%d].BlockTime = %d, want %d", i, decoded[i].BlockTime, original[i].BlockTime)
}
}
var streamed int
for decodedTx, err := range DecodeRawTxsBinaryReader(bytes.NewReader(encoded)) {
if err != nil {
t.Fatalf("DecodeRawTxsBinaryReader() error = %v", err)
}
assertRawTxAccountAccess(t, decodedTx)
streamed++
}
if streamed != len(original) {
t.Fatalf("streamed tx count = %d, want %d", streamed, len(original))
}
}
func TestRawTxBlocksBinaryRoundTrip(t *testing.T) {
tx1 := mustLoadRawTxFixture(t, "testdata/rpc/4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri.json")
tx2 := mustLoadRawTxFixture(t, "testdata/rpc/43EouSYkeVmLBZSKW1KptiQcAVvB6KX49wjztjgzkQ9iU38A6fF68k77bNy9Wn6fwjykqYsPorUKj8m6SFY7naf1.json")
blocks := [][]RawTx{{*tx1}, {*tx2}}
encoded, err := EncodeRawTxBlocksBinary(blocks)
if err != nil {
t.Fatalf("EncodeRawTxBlocksBinary() error = %v", err)
}
decoded, err := DecodeRawTxBlocksBinary(encoded)
if err != nil {
t.Fatalf("DecodeRawTxBlocksBinary() error = %v", err)
}
if len(decoded) != len(blocks) {
t.Fatalf("block count = %d, want %d", len(decoded), len(blocks))
}
for blockIndex := range decoded {
if len(decoded[blockIndex]) != len(blocks[blockIndex]) {
t.Fatalf("block[%d] tx count = %d, want %d", blockIndex, len(decoded[blockIndex]), len(blocks[blockIndex]))
}
for txIndex := range decoded[blockIndex] {
assertRawTxAccountAccess(t, decoded[blockIndex][txIndex])
if decoded[blockIndex][txIndex].TxHash() != blocks[blockIndex][txIndex].TxHash() {
t.Fatalf("block[%d].tx[%d] hash mismatch", blockIndex, txIndex)
}
if decoded[blockIndex][txIndex].BlockTime != blocks[blockIndex][txIndex].BlockTime {
t.Fatalf("block[%d].tx[%d] block time = %d, want %d", blockIndex, txIndex, decoded[blockIndex][txIndex].BlockTime, blocks[blockIndex][txIndex].BlockTime)
}
}
}
}
func TestRawTxBinaryPreservesAccountListHelperBehavior(t *testing.T) {
owner := mustPubKey("SysvarRent111111111111111111111111111111111")
mint := solana.WrappedSol
tokenProgram := solana.TokenProgramID
ata, _, err := solana.FindProgramAddress([][]byte{
owner[:],
tokenProgram[:],
mint[:],
}, solana.SPLAssociatedTokenAccountProgramID)
if err != nil {
t.Fatalf("find ata: %v", err)
}
original := &RawTx{
accountList: []solana.PublicKey{owner, ata, mint, tokenProgram},
BlockTime: 1710000000,
Slot: 123,
IndexWithinBlock: 7,
Transaction: Transaction{Signatures: []solana.Signature{{1, 2, 3}}},
Meta: Meta{
PreBalances: []uint64{2_000_000_000, 0, 0, 0},
PostBalances: []uint64{1_500_000_000, 0, 0, 0},
PreTokenBalances: []TokenBalance{{
AccountIndex: 1,
MintAccount: mint,
OwnerAccount: &owner,
ProgramIDAccount: tokenProgram,
UITokenAmount: UITokenAmount{
Amount: "100",
Decimals: 9,
UIAmount: 0.0000001,
UIAmountString: "0.0000001",
},
}},
PostTokenBalances: []TokenBalance{{
AccountIndex: 1,
MintAccount: mint,
OwnerAccount: &owner,
ProgramIDAccount: tokenProgram,
UITokenAmount: UITokenAmount{
Amount: "250",
Decimals: 9,
UIAmount: 0.00000025,
UIAmountString: "0.00000025",
},
}},
},
}
binaryForm, err := NewRawTxBinary(original)
if err != nil {
t.Fatalf("NewRawTxBinary() error = %v", err)
}
if len(binaryForm.Meta.TokenBalances) != 1 {
t.Fatalf("binary token balance count = %d, want 1", len(binaryForm.Meta.TokenBalances))
}
if got := binaryForm.Meta.TokenBalances[0]; !got.HasPreAmount || !got.HasPostAmount || got.PreAmount != "100" || got.PostAmount != "250" {
t.Fatalf("merged binary token balance = %+v", got)
}
encoded, err := EncodeRawTxBinary(original)
if err != nil {
t.Fatalf("EncodeRawTxBinary() error = %v", err)
}
decoded, err := DecodeRawTxBinary(encoded)
if err != nil {
t.Fatalf("DecodeRawTxBinary() error = %v", err)
}
for _, tx := range []*RawTx{original, decoded} {
if got, err := GetSolAfterTx(tx, 0); err != nil || got != 1_500_000_000 {
t.Fatalf("GetSolAfterTx() = %d, %v; want 1500000000, nil", got, err)
}
balance, err := getTokenBalanceAfterTx(tx, 1)
if err != nil {
t.Fatalf("getTokenBalanceAfterTx() error = %v", err)
}
if balance.UITokenAmount.Amount != "250" {
t.Fatalf("getTokenBalanceAfterTx amount = %s, want 250", balance.UITokenAmount.Amount)
}
if got := getAccountBalanceAfterTx(tx, 1); !got.Equal(decimal.NewFromInt(250)) {
t.Fatalf("getAccountBalanceAfterTx() = %s, want 250", got)
}
if got := GetTokenBalanceAfterTx(tx, 0, tokenProgram, mint); !got.Equal(decimal.NewFromInt(250)) {
t.Fatalf("GetTokenBalanceAfterTx() = %s, want 250", got)
}
ataBalance, err := getAtaByOwner(tx, owner, mint)
if err != nil {
t.Fatalf("getAtaByOwner() error = %v", err)
}
if ataBalance.AccountIndex != 1 {
t.Fatalf("getAtaByOwner account index = %d, want 1", ataBalance.AccountIndex)
}
ataIndex, err := getAtaIdxByOwner(tx, owner, mint)
if err != nil {
t.Fatalf("getAtaIdxByOwner() error = %v", err)
}
if ataIndex != 1 {
t.Fatalf("getAtaIdxByOwner() = %d, want 1", ataIndex)
}
change, changeAtaIndex := tokenBalanceChange(tx, 0, tokenProgram, mint)
if !change.Equal(decimal.NewFromInt(150)) || changeAtaIndex != 1 {
t.Fatalf("tokenBalanceChange() = (%s, %d), want (150, 1)", change, changeAtaIndex)
}
}
}
func TestRawTxBinaryTokenBalanceAmountSupportsUint256(t *testing.T) {
owner := mustPubKey("SysvarRent111111111111111111111111111111111")
mint := solana.WrappedSol
tokenProgram := solana.TokenProgramID
amount := "340282366920938463463374607431768211455"
original := &RawTx{
accountList: []solana.PublicKey{owner, mint, tokenProgram},
Transaction: Transaction{
Signatures: []solana.Signature{{1, 2, 3}},
},
Meta: Meta{
PreBalances: []uint64{1},
PostBalances: []uint64{1},
PostTokenBalances: []TokenBalance{{
AccountIndex: 0,
MintAccount: mint,
OwnerAccount: &owner,
ProgramIDAccount: tokenProgram,
UITokenAmount: UITokenAmount{
Amount: amount,
Decimals: 9,
},
}},
},
}
encoded, err := EncodeRawTxBinary(original)
if err != nil {
t.Fatalf("EncodeRawTxBinary() error = %v", err)
}
decoded, err := DecodeRawTxBinary(encoded)
if err != nil {
t.Fatalf("DecodeRawTxBinary() error = %v", err)
}
got := decoded.Meta.PostTokenBalances[0].UITokenAmount
if got.Amount != amount {
t.Fatalf("Amount = %s, want %s", got.Amount, amount)
}
if got.UIAmountString != "340282366920938463463374607431.768211455" {
t.Fatalf("UIAmountString = %s", got.UIAmountString)
}
}
func TestRawTxBlocksBinarySaveSlots414696178To414696182(t *testing.T) {
rpcURL := os.Getenv("RAWTX_BINARY_RPC_URL")
if rpcURL == "" {
t.Skip("set RAWTX_BINARY_RPC_URL to run RPC-backed rawtx-binary block save test")
}
const startSlot uint64 = 414696178
const endSlot uint64 = 414696182
client := rpc.New(rpcURL)
rewards := false
version := uint64(0)
blocks := make([][]RawTx, 0, endSlot-startSlot+1)
totalTx := 0
filteredVote := 0
for slot := startSlot; slot <= endSlot; slot++ {
block, err := client.GetBlockWithOpts(context.Background(), slot, &rpc.GetBlockOpts{
TransactionDetails: rpc.TransactionDetailsFull,
Rewards: &rewards,
Commitment: rpc.CommitmentFinalized,
Encoding: solana.EncodingBase64,
MaxSupportedTransactionVersion: &version,
})
if err != nil {
t.Fatalf("get block %d: %v", slot, err)
}
var blockTime uint64
if block.BlockTime != nil {
blockTime = uint64(*block.BlockTime)
}
rawTxs := make([]RawTx, 0, len(block.Transactions))
for i, tx := range block.Transactions {
totalTx++
rawTx, err := FromRpcTransactionWithMeta(tx, &blockTime, slot, int64(i))
if err != nil {
t.Fatalf("slot %d tx[%d] convert: %v", slot, i, err)
}
if rawTxBinaryIsVoteTx(rawTx) {
filteredVote++
continue
}
rawTxs = append(rawTxs, *rawTx)
}
blocks = append(blocks, rawTxs)
}
encoded, err := EncodeRawTxBlocksBinary(blocks)
if err != nil {
t.Fatalf("EncodeRawTxBlocksBinary() error = %v", err)
}
outputPath := os.Getenv("RAWTX_BINARY_OUT")
if outputPath == "" {
outputPath = filepath.Join("testdata", "rawtx-binary", "rawtx-blocks-414696178-414696182.prbs")
}
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
t.Fatalf("create rawtx binary output dir: %v", err)
}
if err := os.WriteFile(outputPath, encoded, 0o644); err != nil {
t.Fatalf("write rawtx binary: %v", err)
}
decoded, err := DecodeRawTxBlocksBinary(encoded)
if err != nil {
t.Fatalf("DecodeRawTxBlocksBinary() error = %v", err)
}
if len(decoded) != len(blocks) {
t.Fatalf("decoded block count = %d, want %d", len(decoded), len(blocks))
}
savedTx := 0
for blockIndex := range decoded {
if len(decoded[blockIndex]) != len(blocks[blockIndex]) {
t.Fatalf("decoded block[%d] tx count = %d, want %d", blockIndex, len(decoded[blockIndex]), len(blocks[blockIndex]))
}
for txIndex := range decoded[blockIndex] {
assertRawTxAccountAccess(t, decoded[blockIndex][txIndex])
if rawTxBinaryIsVoteTx(decoded[blockIndex][txIndex]) {
t.Fatalf("decoded block[%d].tx[%d] is vote tx", blockIndex, txIndex)
}
savedTx++
}
}
if savedTx == 0 {
t.Fatal("saved tx count is zero")
}
t.Logf("saved rawtx binary: path=%s bytes=%d total_tx=%d saved_tx=%d filtered_vote=%d", outputPath, len(encoded), totalTx, savedTx, filteredVote)
}
func mustLoadRawTxFixture(t *testing.T, path string) *RawTx {
t.Helper()
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read fixture: %v", err)
}
var response RPCResponse
if err := json.Unmarshal(raw, &response); err != nil {
t.Fatalf("unmarshal fixture: %v", err)
}
return &response.Result
}
func assertRawTxAccountAccess(t *testing.T, tx *RawTx) {
t.Helper()
accounts := tx.GetAccountList()
if len(accounts) == 0 {
t.Fatal("decoded account list is empty")
}
for _, instr := range tx.Transaction.Message.Instructions {
if instr.ProgramIDIndex < 0 || instr.ProgramIDIndex >= len(accounts) {
t.Fatalf("instruction program id index %d out of range %d", instr.ProgramIDIndex, len(accounts))
}
for _, accountIndex := range instr.Accounts {
if accountIndex < 0 || accountIndex >= len(accounts) {
t.Fatalf("instruction account index %d out of range %d", accountIndex, len(accounts))
}
}
}
for _, inner := range tx.Meta.InnerInstructions {
for _, instr := range inner.Instructions {
if instr.ProgramIDIndex < 0 || instr.ProgramIDIndex >= len(accounts) {
t.Fatalf("inner instruction program id index %d out of range %d", instr.ProgramIDIndex, len(accounts))
}
for _, accountIndex := range instr.Accounts {
if accountIndex < 0 || accountIndex >= len(accounts) {
t.Fatalf("inner instruction account index %d out of range %d", accountIndex, len(accounts))
}
}
}
}
}
func rawTxBinaryIsVoteTx(tx *RawTx) bool {
if tx == nil {
return false
}
accountList := tx.GetAccountList()
for _, instr := range tx.Transaction.Message.Instructions {
if instr.ProgramIDIndex >= 0 && instr.ProgramIDIndex < len(accountList) && accountList[instr.ProgramIDIndex] == solana.VoteProgramID {
return true
}
}
return false
}

View File

@@ -1,12 +1,27 @@
package pump_parser
import (
"encoding/binary"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func decodeRaydiumClmmSwapArgs(data []byte) (amountSpecified uint64, otherAmountThreshold uint64, swapMode SwapMode, err error) {
if len(data) < 41 {
return 0, 0, SwapModeUnknown, fmt.Errorf("raydium clmm swap instruction data too short")
}
amountSpecified = binary.LittleEndian.Uint64(data[8:16])
otherAmountThreshold = binary.LittleEndian.Uint64(data[16:24])
isBaseInput := data[40] != 0
swapMode = SwapModeExactOut
if isBaseInput {
swapMode = SwapModeExactIn
}
return amountSpecified, otherAmountThreshold, swapMode, nil
}
func raydiumClmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumClmmProgramID) {
return nil, increaseOffset(offset), fmt.Errorf("raydiumClmm instruction not found, offset, %d, %d", offset[0], offset[1])
@@ -93,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")
}
@@ -133,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)
@@ -182,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")
@@ -278,25 +288,27 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
userTokenOutAccount int
)
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amountSpecified, otherAmountThreshold, swapMode, err := decodeRaydiumClmmSwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
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)
@@ -350,8 +362,7 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
offset[1] += 2
return []Swap{
{
swap := Swap{
Program: SolProgramRaydiumCLMM,
Event: "sell",
Pool: pool,
@@ -369,7 +380,8 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
}

View File

@@ -4,9 +4,20 @@ import (
"bytes"
"fmt"
agbinary "github.com/gagliardetto/binary"
"github.com/shopspring/decimal"
)
type raydiumCPmmSwapBaseInputArgs struct {
AmountIn uint64
MinimumAmountOut uint64
}
type raydiumCPmmSwapBaseOutputArgs struct {
MaxAmountIn uint64
AmountOut uint64
}
func raydiumCPmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumCPmmProgramID) {
return nil, increaseOffset(offset), fmt.Errorf("raydiumCPmm instruction not found, offset, %d, %d", offset[0], offset[1])
@@ -327,6 +338,30 @@ func raydiumCPmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
discriminator := *(*[8]byte)(instruction.Data[:8])
var swapMode SwapMode
var fixedAmount decimal.Decimal
var limitAmount decimal.Decimal
switch discriminator {
case raydiumCPmmSwapBaseInputDiscriminator:
var args raydiumCPmmSwapBaseInputArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode raydium cpmm swap_base_input args: %w", err)
}
swapMode = SwapModeExactIn
fixedAmount = decimal.NewFromUint64(args.AmountIn)
limitAmount = decimal.NewFromUint64(args.MinimumAmountOut)
case raydiumCPmmSwapBaseOutputDiscriminator:
var args raydiumCPmmSwapBaseOutputArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode raydium cpmm swap_base_output args: %w", err)
}
swapMode = SwapModeExactOut
fixedAmount = decimal.NewFromUint64(args.AmountOut)
limitAmount = decimal.NewFromUint64(args.MaxAmountIn)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
market := tx.rawTx.accountList[instruction.Accounts[3]]
// Get token accounts from instruction
tokenIn := tx.rawTx.accountList[instruction.Accounts[4]]
@@ -384,8 +419,7 @@ func raydiumCPmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions")
}
offset[1] += 2
return []Swap{
{
swap := Swap{
Program: SolProgramRaydiumCPMM,
Event: "sell",
Pool: market,
@@ -403,6 +437,8 @@ func raydiumCPmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
return []Swap{swap}, offset, nil
}

View File

@@ -367,7 +367,15 @@ type RaydiumLaunchLabSwapEvent struct {
}
type raydiumLaunchLabSwapArgs struct {
Amount uint64
OtherAmountThreshold uint64
}
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) {
@@ -375,6 +383,26 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
} else {
programName = SolProgramRaydiumLaunchLab
}
discriminator := *(*[8]byte)(instruction.Data[:8])
var swapMode SwapMode
var fixedAmount decimal.Decimal
var limitAmount decimal.Decimal
var swapArgs raydiumLaunchLabSwapArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&swapArgs); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode raydium launchlab swap args: %w", err)
}
switch discriminator {
case raydiumLaunchLabSellExactInDiscriminator, raydiumLaunchLabBuyExactInDiscriminator:
swapMode = SwapModeExactIn
fixedAmount = decimal.NewFromUint64(swapArgs.Amount)
limitAmount = decimal.NewFromUint64(swapArgs.OtherAmountThreshold)
case raydiumLaunchLabSellExactOutDiscriminator, raydiumLaunchLabBuyExactOutDiscriminator:
swapMode = SwapModeExactOut
fixedAmount = decimal.NewFromUint64(swapArgs.Amount)
limitAmount = decimal.NewFromUint64(swapArgs.OtherAmountThreshold)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
user := tx.rawTx.accountList[instruction.Accounts[0]]
pool := tx.rawTx.accountList[instruction.Accounts[4]]
@@ -447,7 +475,7 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
userBase := getAccountBalanceAfterTx(tx.rawTx, userBaseIdx)
userQuote := getAccountBalanceAfterTx(tx.rawTx, userQuoteIdx)
return []Swap{{
swap := Swap{
Program: programName,
Event: event,
Pool: pool,
@@ -466,5 +494,8 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
}}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
return []Swap{swap}, offset, nil
}

View File

@@ -1,11 +1,26 @@
package pump_parser
import (
"encoding/binary"
"fmt"
"github.com/shopspring/decimal"
)
func decodeRaydiumV4SwapArgs(data []byte) (amountSpecified uint64, otherAmountThreshold uint64, swapMode SwapMode, err error) {
if len(data) < 17 {
return 0, 0, SwapModeUnknown, fmt.Errorf("raydium v4 swap instruction data too short")
}
switch data[0] {
case raydiumV4SwapBaseInDiscriminator, raydiumV4SwapBaseInV2Discriminator:
return binary.LittleEndian.Uint64(data[1:9]), binary.LittleEndian.Uint64(data[9:17]), SwapModeExactIn, nil
case raydiumV4SwapBaseOutDiscriminator, raydiumV4SwapBaseOutV2Discriminator:
return binary.LittleEndian.Uint64(data[9:17]), binary.LittleEndian.Uint64(data[1:9]), SwapModeExactOut, nil
default:
return 0, 0, SwapModeUnknown, InstructionIgnoredError
}
}
func raydiumV4Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumV4Program) {
return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 instruction not found, offset, %d, %d", offset[0], offset[1])
@@ -314,6 +329,10 @@ func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions Inne
vaultQuoteIdx = instruction.Accounts[6]
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amountSpecified, otherAmountThreshold, swapMode, err := decodeRaydiumV4SwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
@@ -376,8 +395,7 @@ func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions Inne
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
return []Swap{
{
swap := Swap{
Program: SolProgramRaydiumV4,
Event: event,
Pool: ammAccount,
@@ -396,17 +414,25 @@ func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions Inne
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
}
func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
accountsLen := len(instruction.Accounts)
if accountsLen != 8 {
if accountsLen < 8 {
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 swapv2 instruction, offset %d, %d", offset[0], offset[1])
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amountSpecified, otherAmountThreshold, swapMode, err := decodeRaydiumV4SwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
// Raydium's documented V2 layout uses the first 8 accounts. Routed CPI calls
// may append extra readonly accounts (for example the Raydium program id) at
// the tail, so we only require the canonical prefix here.
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
user := tx.rawTx.accountList[instruction.Accounts[7]]
userSourceTokenAccount := tx.rawTx.accountList[instruction.Accounts[5]]
@@ -472,8 +498,7 @@ func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions In
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
return []Swap{
{
swap := Swap{
Program: SolProgramRaydiumV4,
Event: event,
Pool: ammAccount,
@@ -492,6 +517,7 @@ func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions In
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
}

144
raydiumv4_test.go Normal file
View File

@@ -0,0 +1,144 @@
package pump_parser
import (
"encoding/binary"
"testing"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func transferInstructionData(amount uint64) solana.Base58 {
data := make([]byte, 9)
data[0] = 3
binary.LittleEndian.PutUint64(data[1:], amount)
return solana.Base58(data)
}
func raydiumV4SwapInstructionData(discriminator byte, amountSpecified, otherAmountThreshold uint64) solana.Base58 {
data := make([]byte, 17)
data[0] = discriminator
binary.LittleEndian.PutUint64(data[1:9], amountSpecified)
binary.LittleEndian.PutUint64(data[9:17], otherAmountThreshold)
return solana.Base58(data)
}
func TestRaydiumV4SwapV2ParserAllowsTrailingReadonlyAccounts(t *testing.T) {
t.Parallel()
accountList := make([]solana.PublicKey, 32)
for i := range accountList {
accountList[i] = testPublicKey(byte(i + 1))
}
accountList[0] = solana.TokenProgramID
accountList[8] = raydiumV4Program
accountList[20] = testPublicKey(200)
accountList[21] = testPublicKey(201)
accountList[22] = testPublicKey(202)
outerInstruction := Instruction{ProgramIDIndex: 20}
swapInstruction := Instruction{
Accounts: []int{0, 1, 2, 3, 4, 5, 6, 7, 8},
ProgramIDIndex: 8,
Data: raydiumV4SwapInstructionData(raydiumV4SwapBaseInV2Discriminator, 55, 42),
}
innerInstructions := InnerInstructions{
Index: 0,
Instructions: []Instruction{
swapInstruction,
{
Accounts: []int{5, 4, 7},
ProgramIDIndex: 0,
Data: transferInstructionData(55),
},
{
Accounts: []int{3, 6, 2},
ProgramIDIndex: 0,
Data: transferInstructionData(42),
},
},
}
rawTx := &RawTx{
accountList: accountList,
Meta: Meta{
PostTokenBalances: []TokenBalance{
{
AccountIndex: 3,
MintAccount: accountList[21],
ProgramIDAccount: solana.TokenProgramID,
UITokenAmount: UITokenAmount{
Amount: "1000",
Decimals: 6,
},
},
{
AccountIndex: 4,
MintAccount: accountList[22],
ProgramIDAccount: solana.TokenProgramID,
UITokenAmount: UITokenAmount{
Amount: "2000",
Decimals: 9,
},
},
{
AccountIndex: 5,
MintAccount: accountList[22],
ProgramIDAccount: solana.TokenProgramID,
UITokenAmount: UITokenAmount{
Amount: "300",
Decimals: 9,
},
},
{
AccountIndex: 6,
MintAccount: accountList[21],
ProgramIDAccount: solana.TokenProgramID,
UITokenAmount: UITokenAmount{
Amount: "400",
Decimals: 6,
},
},
},
},
Transaction: Transaction{
Message: Message{
Instructions: []Instruction{outerInstruction},
},
},
}
tx := &Tx{rawTx: rawTx}
swaps, nextOffset, err := raydiumv4SwapV2Parser(tx, swapInstruction, innerInstructions, [2]uint{0, 1})
if err != nil {
t.Fatalf("raydiumv4SwapV2Parser() error = %v", err)
}
if len(swaps) != 1 {
t.Fatalf("raydiumv4SwapV2Parser() swaps len = %d, want 1", len(swaps))
}
if nextOffset != [2]uint{0, 4} {
t.Fatalf("raydiumv4SwapV2Parser() nextOffset = %v, want [0 4]", nextOffset)
}
swap := swaps[0]
if swap.Event != "buy" {
t.Fatalf("swap.Event = %q, want %q", swap.Event, "buy")
}
if !swap.Pool.Equals(accountList[1]) {
t.Fatalf("swap.Pool = %s, want %s", swap.Pool, accountList[1])
}
if !swap.User.Equals(accountList[7]) {
t.Fatalf("swap.User = %s, want %s", swap.User, accountList[7])
}
if !swap.EntryContract.Equals(accountList[20]) {
t.Fatalf("swap.EntryContract = %s, want %s", swap.EntryContract, accountList[20])
}
if !swap.BaseAmount.Equal(decimal.NewFromInt(42)) {
t.Fatalf("swap.BaseAmount = %s, want 42", swap.BaseAmount)
}
if !swap.QuoteAmount.Equal(decimal.NewFromInt(55)) {
t.Fatalf("swap.QuoteAmount = %s, want 55", swap.QuoteAmount)
}
}

230
swap_amounts.go Normal file
View File

@@ -0,0 +1,230 @@
package pump_parser
import (
"encoding/json"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
var maxSlippageBps = decimal.NewFromInt(10000)
func normalizeSlippageBps(value decimal.Decimal) decimal.Decimal {
//if value.IsNegative() {
// return decimal.Zero
//}
//if value.GreaterThan(maxSlippageBps) {
// return maxSlippageBps
//}
return value
}
type SwapMode uint8
type SwapAmountSide uint8
type SwapLimitType uint8
const (
SwapModeUnknown SwapMode = iota
SwapModeExactIn
SwapModeExactOut
)
const (
SwapAmountSideUnknown SwapAmountSide = iota
SwapAmountSideBase
SwapAmountSideQuote
)
const (
SwapLimitTypeUnknown SwapLimitType = iota
SwapLimitTypeMinOut
SwapLimitTypeMaxIn
)
func (m SwapMode) String() string {
switch m {
case SwapModeExactIn:
return "exact_in"
case SwapModeExactOut:
return "exact_out"
default:
return ""
}
}
func (m SwapMode) MarshalJSON() ([]byte, error) {
return json.Marshal(m.String())
}
func (s SwapAmountSide) String() string {
switch s {
case SwapAmountSideBase:
return "base"
case SwapAmountSideQuote:
return "quote"
default:
return ""
}
}
func (s SwapAmountSide) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
func (t SwapLimitType) String() string {
switch t {
case SwapLimitTypeMinOut:
return "min_out"
case SwapLimitTypeMaxIn:
return "max_in"
default:
return ""
}
}
func (t SwapLimitType) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}
func swapAmountForSide(baseAmount, quoteAmount decimal.Decimal, side SwapAmountSide) decimal.Decimal {
switch side {
case SwapAmountSideBase:
return baseAmount
case SwapAmountSideQuote:
return quoteAmount
default:
return decimal.Zero
}
}
func swapMintForSide(baseMint, quoteMint solana.PublicKey, side SwapAmountSide) solana.PublicKey {
switch side {
case SwapAmountSideBase:
return baseMint
case SwapAmountSideQuote:
return quoteMint
default:
return solana.PublicKey{}
}
}
func oppositeSwapAmountSide(side SwapAmountSide) SwapAmountSide {
switch side {
case SwapAmountSideBase:
return SwapAmountSideQuote
case SwapAmountSideQuote:
return SwapAmountSideBase
default:
return SwapAmountSideUnknown
}
}
func fixedSwapAmountSide(event string, swapMode SwapMode) SwapAmountSide {
switch swapMode {
case SwapModeExactIn:
switch event {
case TxEventBuy:
return SwapAmountSideQuote
case TxEventSell:
return SwapAmountSideBase
}
case SwapModeExactOut:
switch event {
case TxEventBuy:
return SwapAmountSideBase
case TxEventSell:
return SwapAmountSideQuote
}
}
return SwapAmountSideUnknown
}
func limitSwapAmountType(swapMode SwapMode) SwapLimitType {
switch swapMode {
case SwapModeExactIn:
return SwapLimitTypeMinOut
case SwapModeExactOut:
return SwapLimitTypeMaxIn
default:
return SwapLimitTypeUnknown
}
}
func calculateLimitSlippageBps(limitType SwapLimitType, limitAmount, actualAmount decimal.Decimal) decimal.Decimal {
var value decimal.Decimal
switch limitType {
case SwapLimitTypeMinOut:
if !actualAmount.IsPositive() {
if !limitAmount.IsPositive() {
value = maxSlippageBps
break
}
value = maxSlippageBps.Neg()
break
}
if !limitAmount.IsPositive() {
value = maxSlippageBps
break
}
value = actualAmount.Sub(limitAmount).Mul(maxSlippageBps).Div(actualAmount)
case SwapLimitTypeMaxIn:
if !limitAmount.IsPositive() {
if !actualAmount.IsPositive() {
value = maxSlippageBps
break
}
value = maxSlippageBps.Neg()
break
}
value = limitAmount.Sub(actualAmount).Mul(maxSlippageBps).Div(limitAmount)
default:
value = decimal.Zero
}
return normalizeSlippageBps(value)
}
func (s *Swap) SetSwapAmountInfoDetailed(
swapMode SwapMode,
fixedAmount decimal.Decimal,
fixedSide SwapAmountSide,
fixedMint solana.PublicKey,
limitType SwapLimitType,
limitAmount decimal.Decimal,
limitSide SwapAmountSide,
limitMint solana.PublicKey,
actualLimitAmount decimal.Decimal,
) {
s.SwapMode = swapMode
s.FixedAmount = fixedAmount
s.FixedAmountSide = fixedSide
s.FixedMint = fixedMint
s.LimitAmountType = limitType
s.LimitAmount = limitAmount
s.LimitAmountSide = limitSide
s.LimitMint = limitMint
s.ActualLimitAmount = actualLimitAmount
s.ActualLimitAmountSide = limitSide
s.SlippageBps = calculateLimitSlippageBps(limitType, limitAmount, actualLimitAmount)
}
func (s *Swap) SetSwapAmountInfo(swapMode SwapMode, fixedAmount, limitAmount decimal.Decimal) {
fixedSide := fixedSwapAmountSide(s.Event, swapMode)
if fixedSide == SwapAmountSideUnknown {
return
}
limitType := limitSwapAmountType(swapMode)
limitSide := oppositeSwapAmountSide(fixedSide)
actualLimitAmount := swapAmountForSide(s.BaseAmount, s.QuoteAmount, limitSide)
s.SetSwapAmountInfoDetailed(
swapMode,
fixedAmount,
fixedSide,
swapMintForSide(s.BaseMint, s.QuoteMint, fixedSide),
limitType,
limitAmount,
limitSide,
swapMintForSide(s.BaseMint, s.QuoteMint, limitSide),
actualLimitAmount,
)
}

387
swap_amounts_oracle_test.go Normal file
View File

@@ -0,0 +1,387 @@
package pump_parser
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
"github.com/shopspring/decimal"
)
type swapOracleCase struct {
name string
txHash string
index int
program string
event string
swapMode SwapMode
fixedAmount string
fixedAmountSide SwapAmountSide
fixedMint string
limitAmountType SwapLimitType
limitAmount string
limitAmountSide SwapAmountSide
limitMint string
actualLimitAmount string
actualLimitAmountSide SwapAmountSide
slippageBps string
}
func TestSwapAmountOracleSamples(t *testing.T) {
EnableAllParsers()
cases := []swapOracleCase{
{
name: "pump buy exact out",
txHash: "5ybEYcXYhFNfCNAu1o7ovM1Rw5285PBzAwsj4ezwmPRLkYtXX91GhcvAgTVZvdCVV6upsGH8DwYeseNswPhEfVbg",
index: 0,
program: "Pump",
event: TxEventBuy,
swapMode: SwapModeExactOut,
fixedAmount: "1459556161603",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "CEwaxx5j1K61JMYXavcxihVQW4NxC6c4NQ27veFpYUYA",
limitAmountType: SwapLimitTypeMaxIn,
limitAmount: "100000001",
limitAmountSide: SwapAmountSideQuote,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "98765431",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "123.4569987654300123",
},
{
name: "raydium v4 exact out",
txHash: "3Q1BhUvqm889oJfCn96AZAJqyHi1uxdeoKQknCc9xQcajhZS5APfwzkHuNTkPJEhyGknj7VyLpkNoMmNPK3No6hC",
index: 0,
program: "RaydiumV4",
event: TxEventSell,
swapMode: SwapModeExactOut,
fixedAmount: "432588",
fixedAmountSide: SwapAmountSideQuote,
fixedMint: "2qEHjDLDLbuBgRYvsxhc5D6uDWAivNFZGan56P1tpump",
limitAmountType: SwapLimitTypeMaxIn,
limitAmount: "18446744073709551615",
limitAmountSide: SwapAmountSideBase,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "279099",
actualLimitAmountSide: SwapAmountSideBase,
slippageBps: "9999.9999999998487001",
},
{
name: "pump amm exact in",
txHash: "3Q1BhUvqm889oJfCn96AZAJqyHi1uxdeoKQknCc9xQcajhZS5APfwzkHuNTkPJEhyGknj7VyLpkNoMmNPK3No6hC",
index: 1,
program: "PumpAMM",
event: TxEventSell,
swapMode: SwapModeExactIn,
fixedAmount: "432588",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "2qEHjDLDLbuBgRYvsxhc5D6uDWAivNFZGan56P1tpump",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "0",
limitAmountSide: SwapAmountSideQuote,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "284317",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "10000",
},
{
name: "meteora dlmm exact in",
txHash: "5uw9Uwe9KDLCzUNf1sRr7EjKqE2Xs8qUHaoC1CRFhkE3TMpo5TimwApW65pd6pmB7Cp92XXMaZ9jQav6aXRZGtoS",
index: 0,
program: "MeteoraDLMM",
event: TxEventBuy,
swapMode: SwapModeExactIn,
fixedAmount: "17684137",
fixedAmountSide: SwapAmountSideQuote,
fixedMint: "So11111111111111111111111111111111111111112",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "0",
limitAmountSide: SwapAmountSideBase,
limitMint: "METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m",
actualLimitAmount: "50437818",
actualLimitAmountSide: SwapAmountSideBase,
slippageBps: "10000",
},
{
name: "orca whirlpool exact in",
txHash: "5uw9Uwe9KDLCzUNf1sRr7EjKqE2Xs8qUHaoC1CRFhkE3TMpo5TimwApW65pd6pmB7Cp92XXMaZ9jQav6aXRZGtoS",
index: 1,
program: "OrcaWhirPool",
event: TxEventSell,
swapMode: SwapModeExactIn,
fixedAmount: "50437818",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "0",
limitAmountSide: SwapAmountSideQuote,
limitMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
actualLimitAmount: "1438802",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "10000",
},
{
name: "raydium v4 exact in",
txHash: "5uw9Uwe9KDLCzUNf1sRr7EjKqE2Xs8qUHaoC1CRFhkE3TMpo5TimwApW65pd6pmB7Cp92XXMaZ9jQav6aXRZGtoS",
index: 2,
program: "RaydiumV4",
event: TxEventBuy,
swapMode: SwapModeExactIn,
fixedAmount: "1438802",
fixedAmountSide: SwapAmountSideQuote,
fixedMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "0",
limitAmountSide: SwapAmountSideBase,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "19059759",
actualLimitAmountSide: SwapAmountSideBase,
slippageBps: "10000",
},
{
name: "raydium clmm exact in",
txHash: "3XoRKna49qCAuF75ctmaYupNmYWuFm5AU73ULQjxNUxz9qJzuKqMRqq5Z88L6DooWTF44UxnxMXwqLn5t9NsoCoZ",
index: 2,
program: "RaydiumCLMM",
event: TxEventSell,
swapMode: SwapModeExactIn,
fixedAmount: "1569519567845",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "CiKu4eHsVrc1eueVQeHn7qhXTcVu95gSQmBpX4utjL9z",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "0",
limitAmountSide: SwapAmountSideQuote,
limitMint: "6sQgvhAYtYFrahcjB1hKfB3ZC5YDVdfYvAqK1GKe93c9",
actualLimitAmount: "366578",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "10000",
},
{
name: "raydium cpmm exact in",
txHash: "288FAsrj7h6hTKywtVaqCqHAbNZ6x3Xuich9kQMGVarVUnUjkqTabxQE9JHyranGY9eqUivZbBTzC5dH1BEuJ6pa",
index: 2,
program: "RaydiumCPMM",
event: TxEventSell,
swapMode: SwapModeExactIn,
fixedAmount: "1260040377905",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "3f7wfg9yHLtGKvy75MmqsVT1ueTFoqyySQbusrX1YAQ4",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "0",
limitAmountSide: SwapAmountSideQuote,
limitMint: "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp",
actualLimitAmount: "802507591",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "10000",
},
{
name: "raydium launchlab exact in",
txHash: "1r3gfEse3WAy5H6h4jMSNq1K5KZNCrMdAtCnpBSE1xkHQEt3EJ2J6Lk6ihQshrfsrS5FbqP5WuUSZG6zPCJB5TE",
index: 0,
program: "RaydiumLaunchLab",
event: TxEventBuy,
swapMode: SwapModeExactIn,
fixedAmount: "10000000",
fixedAmountSide: SwapAmountSideQuote,
fixedMint: "So11111111111111111111111111111111111111112",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "5976144139694",
limitAmountSide: SwapAmountSideBase,
limitMint: "Attr2sqaXr76XqaDxdtnQ4QAEsaFdgTGr599F7ytgray",
actualLimitAmount: "6129378604814",
actualLimitAmountSide: SwapAmountSideBase,
slippageBps: "249.9999999994289796",
},
{
name: "meteora pools exact in",
txHash: "5jQk6mbhtExpUFskRy2AfKWbLgXDv2USiGkq9tQWauGVKduGdTqscgxyDCPgBryr4kz5hDT5CE9TpVTKDoPhkBmt",
index: 0,
program: "MeteoraPools",
event: TxEventBuy,
swapMode: SwapModeExactIn,
fixedAmount: "75404052467",
fixedAmountSide: SwapAmountSideQuote,
fixedMint: "STrikemJEk2tFVYpg7SMo9nGPrnJ56fHnS1K7PV2fPw",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "30605141",
limitAmountSide: SwapAmountSideBase,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "31556751",
actualLimitAmountSide: SwapAmountSideBase,
slippageBps: "301.5551252408715967",
},
{
name: "meteora bonding curve exact in",
txHash: "5Qsq1ueenSs4KgVRgwXmBVFvMR3Asq9MmXwmqQimxDdWLdiJy6dVfmYqa2YCvkNH1Gx7aCzJqg4t9gN9ECfxH2JS",
index: 0,
program: "MeteoraBondingCurve",
event: TxEventSell,
swapMode: SwapModeExactIn,
fixedAmount: "11022737683",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "8FosqFryatEMV4ZeFR1gLmSmxBLcQ2NCibpZxFRPPF34",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "49672101",
limitAmountSide: SwapAmountSideQuote,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "49672101",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "0",
},
{
name: "meteora damm v2 exact in",
txHash: "43EouSYkeVmLBZSKW1KptiQcAVvB6KX49wjztjgzkQ9iU38A6fF68k77bNy9Wn6fwjykqYsPorUKj8m6SFY7naf1",
index: 0,
program: "MeteoraAmmV2",
event: TxEventSell,
swapMode: SwapModeExactIn,
fixedAmount: "11846",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "So11111111111111111111111111111111111111112",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "30893426",
limitAmountSide: SwapAmountSideQuote,
limitMint: "CdDoeyd67nuzmMCF8Dd3RzbxiTRk41Xd922Veu9kGvDE",
actualLimitAmount: "33325162",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "729.6996785792069068",
},
{
name: "meteora damm v2 exact out",
txHash: "BD7GZaXaJc2hzSNPe6Q5yeej7rZLQFMpdx4rZwPhGTyHP43iMAR7LxymRSPGXnefAxSqi5sMsEPS1cjyQjup3Eu",
index: 0,
program: "MeteoraAmmV2",
event: TxEventBuy,
swapMode: SwapModeExactOut,
fixedAmount: "512761043",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "DPfZc59DLrKyVTJDoKB8CBFgCndsjUzxy6fdbxk4Zms9",
limitAmountType: SwapLimitTypeMaxIn,
limitAmount: "71386496",
limitAmountSide: SwapAmountSideQuote,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "70020377",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "191.3693872857970225",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
tx := mustParseRPCFixtureTx(t, tc.txHash)
if tc.index >= len(tx.Swaps) {
t.Fatalf("swap index %d out of range, len=%d", tc.index, len(tx.Swaps))
}
swap := tx.Swaps[tc.index]
if swap.Program != tc.program {
t.Fatalf("program = %q, want %q", swap.Program, tc.program)
}
if swap.Event != tc.event {
t.Fatalf("event = %q, want %q", swap.Event, tc.event)
}
if swap.SwapMode != tc.swapMode {
t.Fatalf("swap mode = %s, want %s", swap.SwapMode.String(), tc.swapMode.String())
}
assertDecimalString(t, "fixed_amount", swap.FixedAmount, tc.fixedAmount)
if swap.FixedAmountSide != tc.fixedAmountSide {
t.Fatalf("fixed amount side = %s, want %s", swap.FixedAmountSide.String(), tc.fixedAmountSide.String())
}
assertPublicKey(t, "fixed_mint", swap.FixedMint, tc.fixedMint)
if swap.LimitAmountType != tc.limitAmountType {
t.Fatalf("limit amount type = %s, want %s", swap.LimitAmountType.String(), tc.limitAmountType.String())
}
assertDecimalString(t, "limit_amount", swap.LimitAmount, tc.limitAmount)
if swap.LimitAmountSide != tc.limitAmountSide {
t.Fatalf("limit amount side = %s, want %s", swap.LimitAmountSide.String(), tc.limitAmountSide.String())
}
assertPublicKey(t, "limit_mint", swap.LimitMint, tc.limitMint)
assertDecimalString(t, "actual_limit_amount", swap.ActualLimitAmount, tc.actualLimitAmount)
if swap.ActualLimitAmountSide != tc.actualLimitAmountSide {
t.Fatalf("actual limit amount side = %s, want %s", swap.ActualLimitAmountSide.String(), tc.actualLimitAmountSide.String())
}
assertDecimalString(t, "slippage_bps", swap.SlippageBps, tc.slippageBps)
})
}
}
func mustParseRPCFixtureTx(t *testing.T, txHash string) *Tx {
t.Helper()
fixturePath := filepath.Join("testdata", "rpc", txHash+".json")
raw, err := os.ReadFile(fixturePath)
if err != nil {
t.Fatalf("read fixture %s: %v", fixturePath, err)
}
var response struct {
Result *rpc.GetTransactionResult `json:"result"`
}
if err := json.Unmarshal(raw, &response); err != nil {
t.Fatalf("unmarshal fixture %s: %v", fixturePath, err)
}
if response.Result == nil || response.Result.Transaction == nil || response.Result.Meta == nil {
t.Fatalf("fixture %s is missing transaction data", fixturePath)
}
rawBinary := response.Result.Transaction.GetBinary()
if len(rawBinary) == 0 {
t.Fatalf("fixture %s has empty transaction bytes", fixturePath)
}
txWithMeta := rpc.TransactionWithMeta{
Slot: response.Result.Slot,
BlockTime: response.Result.BlockTime,
Transaction: rpc.DataBytesOrJSONFromBytes(rawBinary),
Meta: response.Result.Meta,
Version: response.Result.Version,
}
var blockTime *uint64
if response.Result.BlockTime != nil {
bt := uint64(*response.Result.BlockTime)
blockTime = &bt
}
rawTx, err := FromRpcTransactionWithMeta(txWithMeta, blockTime, response.Result.Slot, 0)
if err != nil {
t.Fatalf("convert fixture %s: %v", fixturePath, err)
}
tx, err := ParseRawTx(rawTx)
if err != nil {
t.Fatalf("parse fixture %s: %v", fixturePath, err)
}
return tx
}
func assertDecimalString(t *testing.T, field string, got decimal.Decimal, want string) {
t.Helper()
wantDecimal, err := decimal.NewFromString(want)
if err != nil {
t.Fatalf("invalid expected decimal for %s: %v", field, err)
}
if !got.Equal(wantDecimal) {
t.Fatalf("%s = %s, want %s", field, got.String(), want)
}
}
func assertPublicKey(t *testing.T, field string, got solana.PublicKey, want string) {
t.Helper()
wantKey := solana.MustPublicKeyFromBase58(want)
if !got.Equals(wantKey) {
t.Fatalf("%s = %s, want %s", field, got, wantKey)
}
}

206
swap_amounts_test.go Normal file
View File

@@ -0,0 +1,206 @@
package pump_parser
import (
"testing"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func TestSetSwapAmountInfoExactInBuy(t *testing.T) {
swap := Swap{
Event: TxEventBuy,
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
BaseAmount: decimal.NewFromInt(120),
QuoteAmount: decimal.NewFromInt(100),
}
swap.SetSwapAmountInfo(SwapModeExactIn, decimal.NewFromInt(100), decimal.NewFromInt(110))
if swap.FixedAmountSide != SwapAmountSideQuote {
t.Fatalf("fixed side = %s, want quote", swap.FixedAmountSide.String())
}
if swap.LimitAmountType != SwapLimitTypeMinOut {
t.Fatalf("limit type = %s, want min_out", swap.LimitAmountType.String())
}
if swap.LimitAmountSide != SwapAmountSideBase {
t.Fatalf("limit side = %s, want base", swap.LimitAmountSide.String())
}
if !swap.ActualLimitAmount.Equal(decimal.NewFromInt(120)) {
t.Fatalf("actual limit amount = %s, want 120", swap.ActualLimitAmount)
}
if got := swap.SlippageBps.StringFixed(4); got != "833.3333" {
t.Fatalf("slippage bps = %s, want 833.3333", got)
}
}
func TestSetSwapAmountInfoExactOutSell(t *testing.T) {
swap := Swap{
Event: TxEventSell,
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
BaseAmount: decimal.NewFromInt(95),
QuoteAmount: decimal.NewFromInt(100),
}
swap.SetSwapAmountInfo(SwapModeExactOut, decimal.NewFromInt(100), decimal.NewFromInt(105))
if swap.FixedAmountSide != SwapAmountSideQuote {
t.Fatalf("fixed side = %s, want quote", swap.FixedAmountSide.String())
}
if swap.LimitAmountType != SwapLimitTypeMaxIn {
t.Fatalf("limit type = %s, want max_in", swap.LimitAmountType.String())
}
if swap.LimitAmountSide != SwapAmountSideBase {
t.Fatalf("limit side = %s, want base", swap.LimitAmountSide.String())
}
if !swap.ActualLimitAmount.Equal(decimal.NewFromInt(95)) {
t.Fatalf("actual limit amount = %s, want 95", swap.ActualLimitAmount)
}
if got := swap.SlippageBps.StringFixed(4); got != "952.3810" {
t.Fatalf("slippage bps = %s, want 952.3810", got)
}
}
func TestSetSwapAmountInfoExactInZeroLimitUsesMaxSlippage(t *testing.T) {
swap := Swap{
Event: TxEventSell,
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
BaseAmount: decimal.NewFromInt(50),
QuoteAmount: decimal.NewFromInt(25),
}
swap.SetSwapAmountInfo(SwapModeExactIn, decimal.NewFromInt(50), decimal.Zero)
if got := swap.SlippageBps.String(); got != "10000" {
t.Fatalf("slippage bps = %s, want 10000", got)
}
}
func TestSetSwapAmountInfoExactInNegativeHeadroomClampsToZero(t *testing.T) {
swap := Swap{
Event: TxEventBuy,
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
BaseAmount: decimal.NewFromInt(90),
QuoteAmount: decimal.NewFromInt(100),
}
swap.SetSwapAmountInfo(SwapModeExactIn, decimal.NewFromInt(100), decimal.NewFromInt(110))
if got := swap.SlippageBps.String(); got != "0" {
t.Fatalf("slippage bps = %s, want 0", got)
}
}
func TestSetSwapAmountInfoExactOutNegativeHeadroomClampsToZero(t *testing.T) {
swap := Swap{
Event: TxEventSell,
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
BaseAmount: decimal.NewFromInt(120),
QuoteAmount: decimal.NewFromInt(100),
}
swap.SetSwapAmountInfo(SwapModeExactOut, decimal.NewFromInt(100), decimal.NewFromInt(105))
if got := swap.SlippageBps.String(); got != "0" {
t.Fatalf("slippage bps = %s, want 0", got)
}
}
func TestMeteoraDammSwapAmountInfo(t *testing.T) {
tests := []struct {
name string
event string
params *struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}
wantMode SwapMode
wantFixed int64
wantLimit int64
}{
{
name: "sell exact in uses amount0 as input and amount1 as min out",
event: TxEventSell,
params: &struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}{Amount0: 100, Amount1: 95, SwapMode: 0},
wantMode: SwapModeExactIn,
wantFixed: 100,
wantLimit: 95,
},
{
name: "sell partial fill follows exact in semantics",
event: TxEventSell,
params: &struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}{Amount0: 101, Amount1: 96, SwapMode: 1},
wantMode: SwapModeExactIn,
wantFixed: 101,
wantLimit: 96,
},
{
name: "buy exact in keeps amount0 as input and amount1 as min out",
event: TxEventBuy,
params: &struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}{Amount0: 130, Amount1: 120, SwapMode: 0},
wantMode: SwapModeExactIn,
wantFixed: 130,
wantLimit: 120,
},
{
name: "buy exact out uses amount0 as target output and amount1 as max input",
event: TxEventBuy,
params: &struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}{Amount0: 120, Amount1: 130, SwapMode: 2},
wantMode: SwapModeExactOut,
wantFixed: 120,
wantLimit: 130,
},
{
name: "sell exact out keeps amount0 as target output and amount1 as max input",
event: TxEventSell,
params: &struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}{Amount0: 140, Amount1: 150, SwapMode: 2},
wantMode: SwapModeExactOut,
wantFixed: 140,
wantLimit: 150,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotMode, gotFixed, gotLimit, ok := meteoraDammSwapAmountInfo(tt.event, tt.params)
if !ok {
t.Fatal("ok = false, want true")
}
if gotMode != tt.wantMode {
t.Fatalf("mode = %s, want %s", gotMode.String(), tt.wantMode.String())
}
if !gotFixed.Equal(decimal.NewFromInt(tt.wantFixed)) {
t.Fatalf("fixed = %s, want %d", gotFixed, tt.wantFixed)
}
if !gotLimit.Equal(decimal.NewFromInt(tt.wantLimit)) {
t.Fatalf("limit = %s, want %d", gotLimit, tt.wantLimit)
}
})
}
}

View File

@@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

27
tx.go
View File

@@ -31,6 +31,18 @@ type Swap struct {
BaseAmount decimal.Decimal
QuoteAmount decimal.Decimal
SwapMode SwapMode
FixedAmount decimal.Decimal
FixedAmountSide SwapAmountSide
FixedMint solana.PublicKey
LimitAmountType SwapLimitType
LimitAmount decimal.Decimal
LimitAmountSide SwapAmountSide
LimitMint solana.PublicKey
ActualLimitAmount decimal.Decimal
ActualLimitAmountSide SwapAmountSide
SlippageBps decimal.Decimal
BaseReserve decimal.Decimal
QuoteReserve decimal.Decimal
Mayhem bool
@@ -52,19 +64,18 @@ type Swap struct {
StartBinId int32
EndBinId int32
RemoveBp int32
BinChanges []DlmmBinLiquidityChange
PositionAccount solana.PublicKey
FeeAmount decimal.Decimal
FeeBps string
LpFeeAmount decimal.Decimal
FeeSide string
FeeMint solana.PublicKey
FeeTokenProgram solana.PublicKey
FeeMintDecimals uint8
ConsumeUnit uint64
}
type DlmmBinLiquidityChange struct {
BinId int32
AmountX decimal.Decimal
AmountY decimal.Decimal
BpsToRemove uint16
}
type platformInfo struct {
Platform string
PlatformFee decimal.Decimal

2249
tx_binary.go Normal file

File diff suppressed because it is too large Load Diff

146
tx_binary_realdata_test.go Normal file
View File

@@ -0,0 +1,146 @@
package pump_parser
import (
"encoding/json"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"github.com/gagliardetto/solana-go/rpc"
)
func TestTxBinaryRealFixtureSizes(t *testing.T) {
fixtures, err := filepath.Glob(filepath.Join("testdata", "rpc", "*.json"))
if err != nil {
t.Fatalf("glob fixtures: %v", err)
}
if len(fixtures) == 0 {
t.Fatal("no rpc fixtures found")
}
sort.Strings(fixtures)
type sizeResult struct {
name string
swaps int
platforms int
mevAgents int
addresses int
encodedBytes int
fixtureBytes int
txBinaryBytes int
}
results := make([]sizeResult, 0, len(fixtures))
totalEncoded := 0
for _, fixture := range fixtures {
tx, rawTxBytesLen, fixtureBytesLen := mustParseRPCFixtureTxForBinarySize(t, fixture)
binaryTx, err := NewTxBinary(tx)
if err != nil {
t.Fatalf("build tx binary fixture %s: %v", fixture, err)
}
encoded, err := binaryTx.MarshalBinary()
if err != nil {
t.Fatalf("encode fixture %s: %v", fixture, err)
}
result := sizeResult{
name: strings.TrimSuffix(filepath.Base(fixture), filepath.Ext(fixture)),
swaps: len(tx.Swaps),
platforms: len(tx.Platform),
mevAgents: len(tx.MevAgent),
addresses: len(binaryTx.AddressTable),
encodedBytes: len(encoded),
fixtureBytes: fixtureBytesLen,
txBinaryBytes: rawTxBytesLen,
}
results = append(results, result)
totalEncoded += result.encodedBytes
}
for _, result := range results {
t.Logf(
"%s encoded=%dB swaps=%d platforms=%d mev=%d addresses=%d fixture_json=%dB raw_tx=%dB",
result.name,
result.encodedBytes,
result.swaps,
result.platforms,
result.mevAgents,
result.addresses,
result.fixtureBytes,
result.txBinaryBytes,
)
}
minResult := results[0]
maxResult := results[0]
for _, result := range results[1:] {
if result.encodedBytes < minResult.encodedBytes {
minResult = result
}
if result.encodedBytes > maxResult.encodedBytes {
maxResult = result
}
}
t.Logf(
"summary fixtures=%d avg=%dB min=%dB(%s) max=%dB(%s)",
len(results),
totalEncoded/len(results),
minResult.encodedBytes,
minResult.name,
maxResult.encodedBytes,
maxResult.name,
)
}
func mustParseRPCFixtureTxForBinarySize(t *testing.T, fixturePath string) (*Tx, int, int) {
t.Helper()
raw, err := os.ReadFile(fixturePath)
if err != nil {
t.Fatalf("read fixture %s: %v", fixturePath, err)
}
var response struct {
Result *rpc.GetTransactionResult `json:"result"`
}
if err := json.Unmarshal(raw, &response); err != nil {
t.Fatalf("unmarshal fixture %s: %v", fixturePath, err)
}
if response.Result == nil || response.Result.Transaction == nil || response.Result.Meta == nil {
t.Fatalf("fixture %s is missing transaction data", fixturePath)
}
rawBinary := response.Result.Transaction.GetBinary()
if len(rawBinary) == 0 {
t.Fatalf("fixture %s has empty transaction bytes", fixturePath)
}
txWithMeta := rpc.TransactionWithMeta{
Slot: response.Result.Slot,
BlockTime: response.Result.BlockTime,
Transaction: rpc.DataBytesOrJSONFromBytes(rawBinary),
Meta: response.Result.Meta,
Version: response.Result.Version,
}
var blockTime *uint64
if response.Result.BlockTime != nil {
bt := uint64(*response.Result.BlockTime)
blockTime = &bt
}
rawTx, err := FromRpcTransactionWithMeta(txWithMeta, blockTime, response.Result.Slot, 0)
if err != nil {
t.Fatalf("convert fixture %s: %v", fixturePath, err)
}
tx, err := ParseRawTx(rawTx)
if err != nil {
t.Fatalf("parse fixture %s: %v", fixturePath, err)
}
return tx, len(rawBinary), len(raw)
}

1138
tx_binary_test.go Normal file

File diff suppressed because it is too large Load Diff