Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44eecac087 | ||
|
|
9f17ffce61 | ||
|
|
e4eaddec4e | ||
|
|
9454c3f6c7 | ||
|
|
39bfeb085f | ||
|
|
10885d5e08 | ||
|
|
2406f6d087 | ||
|
|
8b608889cb | ||
|
|
8d4aad1932 | ||
|
|
5cd3a97d81 | ||
|
|
0a4aabc67f | ||
|
|
d46e8b651c | ||
|
|
43659ea4e4 | ||
|
|
6414e6a25f | ||
|
|
273e87b8ad | ||
|
|
bb858c643e | ||
|
|
a620df5837 | ||
|
|
36da96eeaf | ||
|
|
a765fafddd | ||
|
|
738e417167 | ||
|
|
51f1511c8f | ||
|
|
7dfe003e5b | ||
|
|
fe94888b14 | ||
|
|
1dd843c393 | ||
|
|
d2879efcc6 | ||
|
|
e761fd6f84 | ||
|
|
ab0e87a48a | ||
|
|
fb8d93f426 | ||
|
|
0cc843b370 | ||
|
|
d9a214b4b4 | ||
|
|
047b549d0f | ||
|
|
9327eab010 |
249
README.md
Normal file
249
README.md
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
# pump-parser
|
||||||
|
|
||||||
|
Solana transaction parser focused on swap, liquidity, migration, platform, MEV, compute budget, and compact binary persistence workflows.
|
||||||
|
|
||||||
|
The package works with a normalized `RawTx` representation, parses it into `Tx`, and emits one or more `Swap` records when a supported protocol action is found.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Parse Solana RPC / Yellowstone transactions into local `RawTx`.
|
||||||
|
- Extract swap and liquidity events into `Tx.Swaps`.
|
||||||
|
- Preserve transaction metadata such as slot, block time, signer, fee, CU limit, CU consumed, token balances, platform fees, and MEV agent hints.
|
||||||
|
- Encode/decode parsed transactions with the `PTXB` / `PTXS` binary formats.
|
||||||
|
- Encode/decode raw transactions with the `PRTX` / `PRTS` / `PRBS` binary formats.
|
||||||
|
- Stream decode large `PTXS` / `PRTS` payloads without loading every transaction into memory.
|
||||||
|
- Merge `PTXS` batches while remapping address tables.
|
||||||
|
|
||||||
|
## Supported Parsers
|
||||||
|
|
||||||
|
Default parser initialization enables the hot-path Pump parsers only:
|
||||||
|
|
||||||
|
- Pump
|
||||||
|
- Pump AMM
|
||||||
|
|
||||||
|
`EnableAllParsers()` additionally enables:
|
||||||
|
|
||||||
|
- Meteora DLMM
|
||||||
|
- Meteora Pools
|
||||||
|
- Meteora DAMM v2
|
||||||
|
- Meteora Bonding Curve
|
||||||
|
- Orca Whirlpool
|
||||||
|
- Raydium AMM v4
|
||||||
|
- Raydium CLMM
|
||||||
|
- Raydium CPMM
|
||||||
|
- Raydium LaunchLab
|
||||||
|
|
||||||
|
Use `InitParser(WithMeteoraDlmm())` when only Meteora DLMM should be added to the default parser set.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/thloyi/pump-parser
|
||||||
|
```
|
||||||
|
|
||||||
|
This module currently declares `go 1.25.1` in `go.mod`.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
pump_parser "github.com/thloyi/pump-parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Enable all known parser programs. Omit this call to keep the default
|
||||||
|
// Pump + Pump AMM parser set.
|
||||||
|
pump_parser.EnableAllParsers()
|
||||||
|
|
||||||
|
var rawTx *pump_parser.RawTx
|
||||||
|
// Fill rawTx from RPC, Yellowstone, JSON, or RawTx binary decoding.
|
||||||
|
|
||||||
|
tx, err := pump_parser.ParseRawTx(rawTx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("tx:", tx.GetTxHash())
|
||||||
|
for _, swap := range tx.Swaps {
|
||||||
|
fmt.Printf("%s %s base=%s quote=%s pool=%s user=%s\n",
|
||||||
|
swap.Program,
|
||||||
|
swap.Event,
|
||||||
|
swap.BaseAmount,
|
||||||
|
swap.QuoteAmount,
|
||||||
|
swap.Pool,
|
||||||
|
swap.User,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Swap Result
|
||||||
|
|
||||||
|
`ParseRawTx` returns a `Tx`. A transaction can contain zero, one, or multiple parsed swap records in `Tx.Swaps`.
|
||||||
|
|
||||||
|
Each `Swap` describes one protocol-level swap, liquidity, migration, or pool operation that the parser can normalize. The most important fields are:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `Program` | Normalized protocol name, such as `Pump`, `PumpAMM`, `MeteoraDLMM`, `RaydiumV4`, or `OrcaWhirPool`. |
|
||||||
|
| `Event` | Normalized action, such as `buy`, `sell`, `add_liquidity`, `remove_liquidity`, `create`, `complete`, or `migrate`. |
|
||||||
|
| `TxIndex` | Approximate execution order across outer and inner instructions. |
|
||||||
|
| `InstrIdx` / `InnerIdx` | Outer instruction index and inner instruction index where the swap was found. |
|
||||||
|
| `Pool` | Pool, pair, bonding curve, or market account for the parsed action. |
|
||||||
|
| `BaseMint` / `QuoteMint` | Base and quote mint accounts after parser normalization. |
|
||||||
|
| `BaseTokenProgram` / `QuoteTokenProgram` | Token program for each side, useful when Token-2022 is involved. |
|
||||||
|
| `BaseMintDecimals` / `QuoteMintDecimals` | Decimals used to interpret raw token amounts. |
|
||||||
|
| `User` | User or effective owner account for the action. If the parsed user is not on-curve, the parser may fall back to the transaction signer. |
|
||||||
|
| `BaseAmount` / `QuoteAmount` | Actual parsed base-side and quote-side amounts, stored as `decimal.Decimal`. |
|
||||||
|
| `BaseReserve` / `QuoteReserve` | Pool reserves when the protocol event or accounts expose them. |
|
||||||
|
| `UserBaseBalance` / `UserQuoteBalance` | User token balances after the transaction when available from token balance metadata. |
|
||||||
|
| `AfterSOLBalance` | User or signer SOL balance after the transaction. |
|
||||||
|
| `EntryContract` | Known router / entry contract account when detected. |
|
||||||
|
| `Mayhem` / `Cashback` | Protocol or platform-specific labels detected from the transaction path. |
|
||||||
|
|
||||||
|
Swap amount and slippage fields normalize instruction intent:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `SwapMode` | `exact_in`, `exact_out`, or empty when unknown. |
|
||||||
|
| `FixedAmount` / `FixedAmountSide` / `FixedMint` | The user-specified fixed side of the swap. For `exact_in`, this is the input amount. For `exact_out`, this is the target output amount. |
|
||||||
|
| `LimitAmountType` | `min_out` for `exact_in`, `max_in` for `exact_out`, or empty when unknown. |
|
||||||
|
| `LimitAmount` / `LimitAmountSide` / `LimitMint` | User-specified limit on the opposite side. |
|
||||||
|
| `ActualLimitAmount` / `ActualLimitAmountSide` | Actual executed amount on the limited side. |
|
||||||
|
| `SlippageBps` | Remaining headroom to the user's limit in basis points. See `SLIPPAGE_MAPPING.md` for protocol-specific derivation rules. |
|
||||||
|
|
||||||
|
Liquidity, migration, and DLMM-specific fields are populated only for protocols that expose them:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `Creator` | Creator account when exposed by pool or launch events. |
|
||||||
|
| `MigrateToPool` / `MigrateTopProgram` | Destination pool and program for migration events. |
|
||||||
|
| `LpMint` | LP token mint for liquidity operations when available. |
|
||||||
|
| `ActiveBinId` / `StartBinId` / `EndBinId` | Meteora DLMM bin identifiers. |
|
||||||
|
| `RemoveBp` | DLMM remove-liquidity basis points. |
|
||||||
|
| `PositionAccount` | DLMM position account. |
|
||||||
|
| `FeeAmount` / `LpFeeAmount` | Parsed fee amounts when the protocol exposes fee breakdowns. |
|
||||||
|
| `FeeSide` / `FeeMint` / `FeeTokenProgram` / `FeeMintDecimals` | Fee side and mint metadata. |
|
||||||
|
| `ConsumeUnit` | Per-swap compute unit value when available. |
|
||||||
|
|
||||||
|
Amounts are normalized into decimals in parser output, but the exact scale depends on the source event and parser path. For persisted binary output, `tx_binary.go` defines the conversion rules and schema version.
|
||||||
|
|
||||||
|
## Convert Transactions
|
||||||
|
|
||||||
|
RPC transactions can be converted with `FromRpcTransactionWithMeta`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
rawTx, err := pump_parser.FromRpcTransactionWithMeta(
|
||||||
|
txWithMeta,
|
||||||
|
blockTime,
|
||||||
|
slot,
|
||||||
|
indexWithinBlock,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Yellowstone transactions can be converted with `ConvertYellowstoneGrpcTransactionToSolanaTransaction`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
rawTx, err := pump_parser.ConvertYellowstoneGrpcTransactionToSolanaTransaction(
|
||||||
|
update,
|
||||||
|
createdUnix,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Both conversion paths accept optional `RawTxConvertOptions`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
rawTx, err := pump_parser.FromRpcTransactionWithMeta(
|
||||||
|
txWithMeta,
|
||||||
|
blockTime,
|
||||||
|
slot,
|
||||||
|
indexWithinBlock,
|
||||||
|
pump_parser.RawTxConvertOptions{
|
||||||
|
ParseLogEvents: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ParseLogEvents` attaches decoded program log events to matching instructions. `IgnoreLogMessages` skips log-message retention.
|
||||||
|
|
||||||
|
## Binary Formats
|
||||||
|
|
||||||
|
Parsed transaction binary:
|
||||||
|
|
||||||
|
- `EncodeTxBinary` / `DecodeTxBinary` for one parsed `Tx`.
|
||||||
|
- `EncodeTxsBinary` / `DecodeTxsBinary` for a batch of parsed `Tx`.
|
||||||
|
- `DecodeTxsBinaryReader` for streaming `PTXS` reads.
|
||||||
|
- `MergeTxsBinaryBytes` and `MergeTxsBinarySourcesToWriter` for merging `PTXS` batches.
|
||||||
|
|
||||||
|
Raw transaction binary:
|
||||||
|
|
||||||
|
- `EncodeRawTxBinary` / `DecodeRawTxBinary` for one `RawTx`.
|
||||||
|
- `EncodeRawTxsBinary` / `DecodeRawTxsBinary` for a batch of `RawTx`.
|
||||||
|
- `EncodeRawTxBlocksBinary` / `DecodeRawTxBlocksBinary` for grouped block data.
|
||||||
|
- `DecodeRawTxsBinaryReader` for streaming `PRTS` reads.
|
||||||
|
|
||||||
|
Format magic values:
|
||||||
|
|
||||||
|
- `PTXB`: one parsed `Tx`.
|
||||||
|
- `PTXS`: parsed `Tx` batch.
|
||||||
|
- `PRTX`: one raw `RawTx`.
|
||||||
|
- `PRTS`: raw `RawTx` batch.
|
||||||
|
- `PRBS`: raw block-grouped `RawTx` batch.
|
||||||
|
|
||||||
|
When adding or renaming transaction-facing enum values, update `tx_binary.go` enum tables by appending new values only. Reordering existing values changes persisted numeric IDs.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Parse a live transaction by signature:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TX_HASH=<signature> go run ./cmd/rpc_parse
|
||||||
|
```
|
||||||
|
|
||||||
|
Collect Yellowstone transactions into a `.prbs` raw-block binary file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
YELLOWSTONE_X_TOKEN=<token> go run ./cmd/collect_yellowstone_rawtx_binary \
|
||||||
|
-endpoint ams.rpc.orbitflare.com:10000 \
|
||||||
|
-duration 5m \
|
||||||
|
-output testdata/rawtx-binary/sample.prbs
|
||||||
|
```
|
||||||
|
|
||||||
|
Measure parsed `Tx` binary size from a `getBlock` JSON payload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/measure_tx_binary_block \
|
||||||
|
-file /path/to/block.json \
|
||||||
|
-slot 413539056 \
|
||||||
|
-swaps-only
|
||||||
|
```
|
||||||
|
|
||||||
|
Analyze raw transaction binary size distribution:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/analyze_rawtx_binary_size \
|
||||||
|
-file testdata/rawtx-binary/sample.prbs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Run the full test suite:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful focused checks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -run 'TestTxBinary|TestRawTxBinary' .
|
||||||
|
go test -run 'TestMetaoraPoolSwapEventFromLogsUsesMatchingInvocation|TestMetaoraPoolSwapInstructionOccurrenceIncludesInnerInstructions|TestAttachLogEventsToInstructions' .
|
||||||
|
TX_HASH=<signature> go run ./cmd/rpc_parse
|
||||||
|
```
|
||||||
|
|
||||||
|
For parser regressions, reproduce with the exact transaction signature first, then add a targeted unit test or fixture around the corrected parser path.
|
||||||
156
SLIPPAGE_MAPPING.md
Normal file
156
SLIPPAGE_MAPPING.md
Normal 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`
|
||||||
@@ -25,6 +25,9 @@ func chainLinkParser(tx *Tx, instruction Instruction, inners InnerInstructions,
|
|||||||
}
|
}
|
||||||
|
|
||||||
decode := instruction.Data
|
decode := instruction.Data
|
||||||
|
if len(decode) < 4 {
|
||||||
|
return increaseOffset(offset), nil
|
||||||
|
}
|
||||||
discriminator := binary.LittleEndian.Uint32(decode[0:4])
|
discriminator := binary.LittleEndian.Uint32(decode[0:4])
|
||||||
|
|
||||||
switch discriminator {
|
switch discriminator {
|
||||||
@@ -55,6 +58,9 @@ func chainLinkSubmitParser(instruction Instruction, inners InnerInstructions, of
|
|||||||
if storeInstruction.Accounts[0] >= len(tx.rawTx.accountList) || tx.rawTx.accountList[storeInstruction.Accounts[0]] != chainlinkSOLUSDFeedAccount {
|
if storeInstruction.Accounts[0] >= len(tx.rawTx.accountList) || tx.rawTx.accountList[storeInstruction.Accounts[0]] != chainlinkSOLUSDFeedAccount {
|
||||||
return increaseOffset(offset), InstructionIgnoredError
|
return increaseOffset(offset), InstructionIgnoredError
|
||||||
}
|
}
|
||||||
|
if len(storeInstruction.Data) < 8 {
|
||||||
|
return increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
if !bytes.Equal(storeInstruction.Data[0:8], chainlinkSubmitDiscriminator[:]) {
|
if !bytes.Equal(storeInstruction.Data[0:8], chainlinkSubmitDiscriminator[:]) {
|
||||||
return increaseOffset(offset), InstructionIgnoredError
|
return increaseOffset(offset), InstructionIgnoredError
|
||||||
}
|
}
|
||||||
|
|||||||
594
cmd/analyze_rawtx_binary_size/main.go
Normal file
594
cmd/analyze_rawtx_binary_size/main.go
Normal 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
|
||||||
|
}
|
||||||
302
cmd/collect_yellowstone_rawtx_binary/main.go
Normal file
302
cmd/collect_yellowstone_rawtx_binary/main.go
Normal 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)
|
||||||
|
}
|
||||||
134
cmd/measure_tx_binary_block/main.go
Normal file
134
cmd/measure_tx_binary_block/main.go
Normal 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
133
cmd/rpc_parse/main.go
Normal 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
21
codex.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Codex Notes
|
||||||
|
|
||||||
|
## Tx Binary enum synchronization
|
||||||
|
|
||||||
|
When adding or renaming transaction-facing enum values, keep the binary format definitions in sync.
|
||||||
|
|
||||||
|
This includes, but is not limited to:
|
||||||
|
|
||||||
|
- tx events
|
||||||
|
- programs
|
||||||
|
- platforms
|
||||||
|
- MEV agents
|
||||||
|
- swap modes, amount sides, limit types, and fee sides
|
||||||
|
|
||||||
|
Checklist:
|
||||||
|
|
||||||
|
1. Add the public constant in the normal source location, such as `enum.go`.
|
||||||
|
2. Add any address mapping in `consts.go` when the enum is account-derived, such as platform or MEV agent detection.
|
||||||
|
3. Append the new value to the matching versioned enum list in `tx_binary.go` under `txBinaryEnumTables`.
|
||||||
|
4. Do not reorder or insert into existing `tx_binary.go` enum lists unless the binary version is intentionally changed; append to preserve existing numeric IDs.
|
||||||
|
5. Add or update tx-binary round-trip coverage so encoding and decoding the new enum value is exercised.
|
||||||
18
consts.go
18
consts.go
@@ -186,6 +186,12 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas,
|
solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas,
|
||||||
solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas,
|
solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas,
|
||||||
solana.MustPublicKeyFromBase58("soyasF3QPWPAKKmgA3GjfWax1kmTT1aoqSGxPzVLNUQ"): MevAgentSoyas,
|
solana.MustPublicKeyFromBase58("soyasF3QPWPAKKmgA3GjfWax1kmTT1aoqSGxPzVLNUQ"): MevAgentSoyas,
|
||||||
|
solana.MustPublicKeyFromBase58("soyasi59njacMUPvo3TM5paHjeK8pYSdovXgFi32gRt"): MevAgentSoyas,
|
||||||
|
solana.MustPublicKeyFromBase58("soyasQYhJxv8uZgWDxhg72td6piAf7XTkoyWHtSATEz"): MevAgentSoyas,
|
||||||
|
solana.MustPublicKeyFromBase58("soyastP66xyYC8XADXZjdMM5BAVGD2YRvz8dwtLsqb8"): MevAgentSoyas,
|
||||||
|
solana.MustPublicKeyFromBase58("soyasvdgUJWYcUCzDxpmjUnNjH7KamXLXTzLwFvdVPE"): MevAgentSoyas,
|
||||||
|
solana.MustPublicKeyFromBase58("soyasvxAunisNxaoRxkKGjNir7KmbwYnr37JmefkX9G"): MevAgentSoyas,
|
||||||
|
solana.MustPublicKeyFromBase58("soyas5doVFUwH8s5zK8gEvCL5KR5ogDmf52LsrJEZ9h"): MevAgentSoyas,
|
||||||
solana.MustPublicKeyFromBase58("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium,
|
solana.MustPublicKeyFromBase58("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium,
|
||||||
solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium,
|
solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium,
|
||||||
solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium,
|
solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium,
|
||||||
@@ -200,6 +206,8 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("b1ooMDLjzz4QqecNsJ8bBXzJTzfAPDCP3CxijTS2K93"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("b1ooMDLjzz4QqecNsJ8bBXzJTzfAPDCP3CxijTS2K93"): MevAgentAstralane,
|
||||||
@@ -381,6 +389,16 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("t7qUQU35sLpPydh42BcPmtEfTWW8gBe4Ry3gjwVnokJ"): MevAgentRaiden,
|
solana.MustPublicKeyFromBase58("t7qUQU35sLpPydh42BcPmtEfTWW8gBe4Ry3gjwVnokJ"): MevAgentRaiden,
|
||||||
solana.MustPublicKeyFromBase58("t8pPgarSK3TnuLbbHmoE1RCQdLxfxuPqNTyFjBKahok"): MevAgentRaiden,
|
solana.MustPublicKeyFromBase58("t8pPgarSK3TnuLbbHmoE1RCQdLxfxuPqNTyFjBKahok"): MevAgentRaiden,
|
||||||
solana.MustPublicKeyFromBase58("t96GGdw3MiaGR993XN8PSsRpKGXx56t5Wf6zcF1hBpY"): MevAgentRaiden,
|
solana.MustPublicKeyFromBase58("t96GGdw3MiaGR993XN8PSsRpKGXx56t5Wf6zcF1hBpY"): MevAgentRaiden,
|
||||||
|
solana.MustPublicKeyFromBase58("7HkiWXe5deJvzn4D6kgMUFCADwX9Z4DMrdjNSSxN6bPp"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zanrUknLZXzT9JPj968A7RfgCjp77Lx1W1xKRAtfshb"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zanHbk2UsiT3jKsKjD7UuEqS5Vgpmcd4pG9HycAAV8g"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zanNazKCXNRoKnPS9BBbFTELTpNwUDJxeKEb1JtZJer"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zan3gbFhXCjGLHhRe2vaXRDta5fCrYiYr3Dq4RLvpfU"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zan6WoE3DX5aK7FMQT1vSGsGrgZG1ngns3oCsFMnBHU"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zan8Nb9fB4zMDsuTRP9R65QZbc9v2Cjn5a4Hjwnj8D3"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zanJgoR7ALJAJ6ohoKs6aS9T71D9ZkNN9gYM5xUsi3u"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zanAtYifQP7Bo6kStB97mJvzqSDW1toKNibWibwcKDd"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("GWT5UjDheZzoqinLavJkYvSRH5sakW8vDRdAgrUS5ZcS"): MevAgentTunneling,
|
||||||
}
|
}
|
||||||
|
|
||||||
var entryContractAddresses = map[solana.PublicKey]string{
|
var entryContractAddresses = map[solana.PublicKey]string{
|
||||||
|
|||||||
27
enum.go
27
enum.go
@@ -21,6 +21,8 @@ const (
|
|||||||
MevAgentSpeedlanding = "speedlanding"
|
MevAgentSpeedlanding = "speedlanding"
|
||||||
MevAgentAllenhark = "allenhark"
|
MevAgentAllenhark = "allenhark"
|
||||||
MevAgentRaiden = "raiden"
|
MevAgentRaiden = "raiden"
|
||||||
|
MevAgentZan = "zan"
|
||||||
|
MevAgentTunneling = "tunneling"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -119,9 +121,24 @@ func GetConditionByProgram(program string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TxEventAddLP = "add"
|
TxEventAddLP = "add"
|
||||||
TxEventRemoveLP = "remove"
|
TxEventRemoveLP = "remove"
|
||||||
TxEventBuy = "buy"
|
TxEventBuy = "buy"
|
||||||
TxEventSell = "sell"
|
TxEventSell = "sell"
|
||||||
TxEventBurn = "burn"
|
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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func main() {
|
|||||||
// laserstream-mainnet-slc.helius-rpc.com:80
|
// laserstream-mainnet-slc.helius-rpc.com:80
|
||||||
|
|
||||||
ch := make(chan example.SubscriptionMessage, 1)
|
ch := make(chan example.SubscriptionMessage, 1)
|
||||||
go example.RunLoopWithReConnect(context.Background(), "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)
|
// var tokenTxs = make(map[string]*types.Tx)
|
||||||
// currentBlock := uint64(0)
|
// currentBlock := uint64(0)
|
||||||
for msg := range ch {
|
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) {
|
if len(ptx.Swaps) > 0 {
|
||||||
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(),
|
for _, swap := range ptx.Swaps {
|
||||||
ptx.Swaps[0].BaseAmount.Div(decimal.NewFromInt(1e6)), ptx.Swaps[0].QuoteAmount.Div(decimal.NewFromInt(1e9)))
|
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
|
// currentBlock = ptx.Block
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -45,9 +45,11 @@ type Client struct {
|
|||||||
firstMessage bool
|
firstMessage bool
|
||||||
|
|
||||||
handler Handler
|
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 subscription pb.SubscribeRequest
|
||||||
|
|
||||||
//var failed = true
|
//var failed = true
|
||||||
@@ -58,10 +60,10 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client
|
|||||||
Vote: &vote,
|
Vote: &vote,
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.Transactions["transactions_sub"].AccountInclude = []string{
|
//subscription.Transactions["transactions_sub"].AccountInclude = []string{
|
||||||
"pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
|
// "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
|
||||||
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
|
// "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
|
||||||
}
|
//}
|
||||||
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
|
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
|
||||||
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
|
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client
|
|||||||
lastReceiveTime: time.Now(),
|
lastReceiveTime: time.Now(),
|
||||||
subStatus: false,
|
subStatus: false,
|
||||||
subscription: &subscription,
|
subscription: &subscription,
|
||||||
|
xToken: xtoken,
|
||||||
}
|
}
|
||||||
c.handler = NewPumpHandler(func(tx *types.Tx) {
|
c.handler = NewPumpHandler(func(tx *types.Tx) {
|
||||||
c.sendTx(tx)
|
c.sendTx(tx)
|
||||||
@@ -112,12 +115,12 @@ func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Clien
|
|||||||
return c
|
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
|
var client *Client
|
||||||
if program == types.SolProgramRaydiumLaunchLab {
|
if program == types.SolProgramRaydiumLaunchLab {
|
||||||
client = NewClientWithLaunchLab(endpoint, ch)
|
client = NewClientWithLaunchLab(endpoint, ch)
|
||||||
} else {
|
} else {
|
||||||
client = NewClientWithPumpSwap(endpoint, ch)
|
client = NewClientWithPumpSwap(endpoint, token, ch)
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -206,12 +209,13 @@ func (c *Client) grpcSubscribe(ctx context.Context, conn *grpc.ClientConn) error
|
|||||||
log.Printf("Subscription request: %s", string(subscriptionJson))
|
log.Printf("Subscription request: %s", string(subscriptionJson))
|
||||||
|
|
||||||
// Set up the subscription request
|
// Set up the subscription request
|
||||||
//if *token != "" {
|
if c.xToken != "" {
|
||||||
// md := metadata.New(map[string]string{"x-token": *token})
|
fmt.Println("xtoken", c.xToken)
|
||||||
// ctx = metadata.NewOutgoingContext(ctx, md)
|
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)
|
//md := metadata.New(map[string]string{"x-token": "5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"})
|
||||||
|
//ctx = metadata.NewOutgoingContext(ctx, md)
|
||||||
|
|
||||||
stream, err := client.Subscribe(ctx)
|
stream, err := client.Subscribe(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,29 +2,18 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"log/slog"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gagliardetto/solana-go"
|
"github.com/gagliardetto/solana-go"
|
||||||
"github.com/gagliardetto/solana-go/rpc"
|
"github.com/gagliardetto/solana-go/rpc"
|
||||||
"github.com/jackc/pgtype"
|
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
solana_parser "github.com/thloyi/pump-parser"
|
solana_parser "github.com/thloyi/pump-parser"
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ()
|
var ()
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
var slot uint64 = 403021435
|
var slot uint64 = 414696178
|
||||||
var data = NewBlockData(decimal.NewFromFloat(100.0))
|
|
||||||
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
|
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
|
||||||
var rewards = false
|
var rewards = false
|
||||||
var version uint64 = 0
|
var version uint64 = 0
|
||||||
@@ -42,7 +31,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
solana_parser.EnableAllParsers()
|
solana_parser.EnableAllParsers()
|
||||||
|
|
||||||
var txs []*solana_parser.Tx
|
var txs []solana_parser.Tx
|
||||||
for i, tx := range blocks.Transactions {
|
for i, tx := range blocks.Transactions {
|
||||||
var blockTime uint64
|
var blockTime uint64
|
||||||
if blocks.BlockTime != nil {
|
if blocks.BlockTime != nil {
|
||||||
@@ -61,766 +50,11 @@ func main() {
|
|||||||
fmt.Println("parse tx error:", i, rawTx.TxHash(), err)
|
fmt.Println("parse tx error:", i, rawTx.TxHash(), err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
txs = append(txs, parsedTx)
|
txs = append(txs, *parsedTx)
|
||||||
}
|
}
|
||||||
for _, result := range txs {
|
_, err = solana_parser.EncodeTxsBinary(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, "")
|
|
||||||
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, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
fmt.Println("EncodeTxsBinary err", 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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func main() {
|
|||||||
var data = NewBlockData(decimal.NewFromFloat(100.0))
|
var data = NewBlockData(decimal.NewFromFloat(100.0))
|
||||||
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
|
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
|
||||||
var version uint64 = 0
|
var version uint64 = 0
|
||||||
txSig, _ := solana.SignatureFromBase58("2LCw5yZy6sGTWKpJNxpFxR11M66cXPsrGmJXnQmWW9QVv6SDWRmu1aevc6yE9NeUz78mFb4T8TEx9w5781NHnz2T")
|
txSig, _ := solana.SignatureFromBase58("4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri")
|
||||||
tx, err := client.GetTransaction(context.Background(), txSig, &rpc.GetTransactionOpts{
|
tx, err := client.GetTransaction(context.Background(), txSig, &rpc.GetTransactionOpts{
|
||||||
Commitment: rpc.CommitmentFinalized,
|
Commitment: rpc.CommitmentFinalized,
|
||||||
Encoding: solana.EncodingBase64,
|
Encoding: solana.EncodingBase64,
|
||||||
@@ -78,6 +78,10 @@ func main() {
|
|||||||
i++
|
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 {
|
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)
|
//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("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err)
|
||||||
|
|||||||
86
meta.go
86
meta.go
@@ -20,12 +20,16 @@ var mayhemFeeAccounts = []solana.PublicKey{
|
|||||||
|
|
||||||
var pumpGetFeesDiscriminator = calculateDiscriminator("global:get_fees")
|
var pumpGetFeesDiscriminator = calculateDiscriminator("global:get_fees")
|
||||||
var pumpBuyDiscriminator = calculateDiscriminator("global:buy")
|
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 pumpSellDiscriminator = calculateDiscriminator("global:sell")
|
||||||
|
var pumpSellV2Discriminator = calculateDiscriminator("global:sell_v2")
|
||||||
var pumpCreateDiscriminator = calculateDiscriminator("global:create")
|
var pumpCreateDiscriminator = calculateDiscriminator("global:create")
|
||||||
var pumpCreateV2Discriminator = calculateDiscriminator("global:create_v2")
|
var pumpCreateV2Discriminator = calculateDiscriminator("global:create_v2")
|
||||||
var pumpAdminSetCreatorDiscriminator = calculateDiscriminator("global:admin_set_creator")
|
var pumpAdminSetCreatorDiscriminator = calculateDiscriminator("global:admin_set_creator")
|
||||||
var pumpMigrateDiscriminator = calculateDiscriminator("global:migrate")
|
var pumpMigrateDiscriminator = calculateDiscriminator("global:migrate")
|
||||||
|
var pumpMigrateV2Discriminator = calculateDiscriminator("global:migrate_v2")
|
||||||
|
|
||||||
var pumpEventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29}
|
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}
|
var pumpTradeEventDiscriminator = [16]byte{228, 69, 165, 46, 81, 203, 154, 29, 189, 219, 127, 211, 78, 230, 97, 238}
|
||||||
@@ -68,41 +72,50 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
meteoraInitializeLbPairDiscriminator = calculateDiscriminator("global:initialize_lb_pair2")
|
meteoraInitializeCustomizablePermissionlessLbPairDiscriminator = calculateDiscriminator("global:initialize_customizable_permissionless_lb_pair")
|
||||||
meteoraInitializeLbPairEventDiscriminator = calculateDiscriminator("event:LbPairCreate")
|
meteoraInitializeCustomizablePermissionlessLbPair2Discriminator = calculateDiscriminator("global:initialize_customizable_permissionless_lb_pair2")
|
||||||
meteoraDlmmSwapDiscriminator = calculateDiscriminator("global:swap")
|
meteoraInitializeLbPairDiscriminator = calculateDiscriminator("global:initialize_lb_pair")
|
||||||
meteoraDlmmSwap2Discriminator = calculateDiscriminator("global:swap2")
|
meteoraInitializeLbPair2Discriminator = calculateDiscriminator("global:initialize_lb_pair2")
|
||||||
meteoraDlmmSwapExactOutDiscriminator = calculateDiscriminator("global:swap_exact_out")
|
meteoraInitializePermissionLbPairDiscriminator = calculateDiscriminator("global:initialize_permission_lb_pair")
|
||||||
meteoraDlmmSwapExactOut2Discriminator = calculateDiscriminator("global:swap_exact_out2")
|
meteoraInitializeLbPairEventDiscriminator = calculateDiscriminator("event:LbPairCreate")
|
||||||
meteoraDlmmSwapWithPriceImpactDiscriminator = calculateDiscriminator("global:swap_with_price_impact")
|
meteoraDlmmSwapDiscriminator = calculateDiscriminator("global:swap")
|
||||||
meteoraDlmmSwapWithPriceImpact2Discriminator = calculateDiscriminator("global:swap_with_price_impact2")
|
meteoraDlmmSwap2Discriminator = calculateDiscriminator("global:swap2")
|
||||||
meteoraDlmmInitializePositionDiscriminator = calculateDiscriminator("global:initialize_position")
|
meteoraDlmmSwapExactOutDiscriminator = calculateDiscriminator("global:swap_exact_out")
|
||||||
meteoraDlmmInitializePosition2Discriminator = calculateDiscriminator("global:initialize_position2")
|
meteoraDlmmSwapExactOut2Discriminator = calculateDiscriminator("global:swap_exact_out2")
|
||||||
meteoraDlmmInitializePositionByOperatorDiscriminator = calculateDiscriminator("global:initialize_position_by_operator")
|
meteoraDlmmSwapWithPriceImpactDiscriminator = calculateDiscriminator("global:swap_with_price_impact")
|
||||||
meteoraDlmmInitializePositionPdaDiscriminator = calculateDiscriminator("global:initialize_position_pda")
|
meteoraDlmmSwapWithPriceImpact2Discriminator = calculateDiscriminator("global:swap_with_price_impact2")
|
||||||
meteoraDlmmClosePositionDiscriminator = calculateDiscriminator("global:close_position")
|
meteoraDlmmInitializePositionDiscriminator = calculateDiscriminator("global:initialize_position")
|
||||||
meteoraDlmmClosePosition2Discriminator = calculateDiscriminator("global:close_position2")
|
meteoraDlmmInitializePosition2Discriminator = calculateDiscriminator("global:initialize_position2")
|
||||||
meteoraDlmmClosePositionIfEmptyDiscriminator = calculateDiscriminator("global:close_position_if_empty")
|
meteoraDlmmInitializePositionByOperatorDiscriminator = calculateDiscriminator("global:initialize_position_by_operator")
|
||||||
meteoraDlmmSwapEventDiscriminator = calculateDiscriminator("event:Swap")
|
meteoraDlmmInitializePositionPdaDiscriminator = calculateDiscriminator("global:initialize_position_pda")
|
||||||
meteoraDlmmAddLiquidityDiscriminator = calculateDiscriminator("global:add_liquidity")
|
meteoraDlmmClosePositionDiscriminator = calculateDiscriminator("global:close_position")
|
||||||
meteoraDlmmAddLiquidity2Discriminator = calculateDiscriminator("global:add_liquidity2")
|
meteoraDlmmClosePosition2Discriminator = calculateDiscriminator("global:close_position2")
|
||||||
meteoraDlmmAddLiquidityByStrategyDiscriminator = calculateDiscriminator("global:add_liquidity_by_strategy")
|
meteoraDlmmClosePositionIfEmptyDiscriminator = calculateDiscriminator("global:close_position_if_empty")
|
||||||
meteoraDlmmAddLiquidityByStrategy2Discriminator = calculateDiscriminator("global:add_liquidity_by_strategy2")
|
meteoraDlmmSwapEventDiscriminator = calculateDiscriminator("event:Swap")
|
||||||
meteoraDlmmAddLiquidityByWeightDiscriminator = calculateDiscriminator("global:add_liquidity_by_weight")
|
meteoraDlmmAddLiquidityDiscriminator = calculateDiscriminator("global:add_liquidity")
|
||||||
meteoraDlmmClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee")
|
meteoraDlmmAddLiquidity2Discriminator = calculateDiscriminator("global:add_liquidity2")
|
||||||
meteoraDlmmClaimFee2Discriminator = calculateDiscriminator("global:claim_fee2")
|
meteoraDlmmAddLiquidityByStrategyDiscriminator = calculateDiscriminator("global:add_liquidity_by_strategy")
|
||||||
meteoraDlmmRebalanceLiquidityDiscriminator = calculateDiscriminator("global:rebalance_liquidity")
|
meteoraDlmmAddLiquidityByStrategy2Discriminator = calculateDiscriminator("global:add_liquidity_by_strategy2")
|
||||||
meteoraDlmmRemoveLiquidityDiscriminator = calculateDiscriminator("global:remove_liquidity")
|
meteoraDlmmAddLiquidityByWeightDiscriminator = calculateDiscriminator("global:add_liquidity_by_weight")
|
||||||
meteoraDlmmRemoveLiquidity2Discriminator = calculateDiscriminator("global:remove_liquidity2")
|
meteoraDlmmAddLiquidityOneSideDiscriminator = calculateDiscriminator("global:add_liquidity_one_side")
|
||||||
meteoraDlmmRemoveLiquidityByRangeDiscriminator = calculateDiscriminator("global:remove_liquidity_by_range")
|
meteoraDlmmAddLiquidityOneSidePreciseDiscriminator = calculateDiscriminator("global:add_liquidity_one_side_precise")
|
||||||
meteoraDlmmRemoveLiquidityByRange2Discriminator = calculateDiscriminator("global:remove_liquidity_by_range2")
|
meteoraDlmmAddLiquidityOneSidePrecise2Discriminator = calculateDiscriminator("global:add_liquidity_one_side_precise2")
|
||||||
meteoraDlmmAddLiquidityEventDiscriminator = calculateDiscriminator("event:AddLiquidity")
|
meteoraDlmmAddLiquidityByStrategyOneSideDiscriminator = calculateDiscriminator("global:add_liquidity_by_strategy_one_side")
|
||||||
meteoraDlmmClaimFeeEventDiscriminator = calculateDiscriminator("event:ClaimFee")
|
meteoraDlmmClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee")
|
||||||
meteoraDlmmClaimFee2EventDiscriminator = calculateDiscriminator("event:ClaimFee2")
|
meteoraDlmmClaimFee2Discriminator = calculateDiscriminator("global:claim_fee2")
|
||||||
meteoraDlmmPositionCloseEventDiscriminator = calculateDiscriminator("event:PositionClose")
|
meteoraDlmmRebalanceLiquidityDiscriminator = calculateDiscriminator("global:rebalance_liquidity")
|
||||||
meteoraDlmmPositionCreateEventDiscriminator = calculateDiscriminator("event:PositionCreate")
|
meteoraDlmmRemoveAllLiquidityDiscriminator = calculateDiscriminator("global:remove_all_liquidity")
|
||||||
meteoraDlmmRebalancingEventDiscriminator = calculateDiscriminator("event:Rebalancing")
|
meteoraDlmmRemoveLiquidityDiscriminator = calculateDiscriminator("global:remove_liquidity")
|
||||||
meteoraDlmmRemoveLiquidityEventDiscriminator = calculateDiscriminator("event:RemoveLiquidity")
|
meteoraDlmmRemoveLiquidity2Discriminator = calculateDiscriminator("global:remove_liquidity2")
|
||||||
|
meteoraDlmmRemoveLiquidityByRangeDiscriminator = calculateDiscriminator("global:remove_liquidity_by_range")
|
||||||
|
meteoraDlmmRemoveLiquidityByRange2Discriminator = calculateDiscriminator("global:remove_liquidity_by_range2")
|
||||||
|
meteoraDlmmAddLiquidityEventDiscriminator = calculateDiscriminator("event:AddLiquidity")
|
||||||
|
meteoraDlmmClaimFeeEventDiscriminator = calculateDiscriminator("event:ClaimFee")
|
||||||
|
meteoraDlmmClaimFee2EventDiscriminator = calculateDiscriminator("event:ClaimFee2")
|
||||||
|
meteoraDlmmPositionCloseEventDiscriminator = calculateDiscriminator("event:PositionClose")
|
||||||
|
meteoraDlmmPositionCreateEventDiscriminator = calculateDiscriminator("event:PositionCreate")
|
||||||
|
meteoraDlmmRebalancingEventDiscriminator = calculateDiscriminator("event:Rebalancing")
|
||||||
|
meteoraDlmmRemoveLiquidityEventDiscriminator = calculateDiscriminator("event:RemoveLiquidity")
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -123,6 +136,7 @@ var (
|
|||||||
metaoraPoolRemoveBalanceLiquidityDiscriminator = calculateDiscriminator("global:remove_balance_liquidity")
|
metaoraPoolRemoveBalanceLiquidityDiscriminator = calculateDiscriminator("global:remove_balance_liquidity")
|
||||||
metaoraPoolClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee")
|
metaoraPoolClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee")
|
||||||
metaoraPoolBootstrapLiquidityDiscriminator = calculateDiscriminator("global:bootstrap_liquidity")
|
metaoraPoolBootstrapLiquidityDiscriminator = calculateDiscriminator("global:bootstrap_liquidity")
|
||||||
|
metaoraPoolSwapEventDiscriminator = calculateDiscriminator("event:Swap")
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
657
metaoradlmm.go
657
metaoradlmm.go
@@ -66,6 +66,13 @@ type dlmmPositionCloseEvent struct {
|
|||||||
Owner solana.PublicKey
|
Owner solana.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dlmmLbPairCreateEvent struct {
|
||||||
|
LbPair solana.PublicKey
|
||||||
|
BinStep uint16
|
||||||
|
TokenX solana.PublicKey
|
||||||
|
TokenY solana.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
type dlmmClaimFeeInnerEvent struct {
|
type dlmmClaimFeeInnerEvent struct {
|
||||||
LbPair solana.PublicKey
|
LbPair solana.PublicKey
|
||||||
Position solana.PublicKey
|
Position solana.PublicKey
|
||||||
@@ -178,6 +185,53 @@ type dlmmAddLiquidityByWeightArgs struct {
|
|||||||
LiquidityParameter dlmmLiquidityParameterByWeight
|
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 {
|
type dlmmRemoveLiquidityArgs struct {
|
||||||
BinLiquidityRemoval []dlmmBinLiquidityReduction
|
BinLiquidityRemoval []dlmmBinLiquidityReduction
|
||||||
}
|
}
|
||||||
@@ -264,6 +318,16 @@ type dlmmLiquidityAccounts struct {
|
|||||||
tokenYProgramIdx int
|
tokenYProgramIdx int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dlmmOneSideLiquidityAccounts struct {
|
||||||
|
positionIdx int
|
||||||
|
poolIdx int
|
||||||
|
userTokenIdx int
|
||||||
|
reserveIdx int
|
||||||
|
tokenMintIdx int
|
||||||
|
userIdx int
|
||||||
|
tokenProgramIdx int
|
||||||
|
}
|
||||||
|
|
||||||
var meteoraDlmmEventAuthority = func() solana.PublicKey {
|
var meteoraDlmmEventAuthority = func() solana.PublicKey {
|
||||||
key, _, err := solana.FindProgramAddress([][]byte{[]byte("__event_authority")}, meteoraDlmmProgram)
|
key, _, err := solana.FindProgramAddress([][]byte{[]byte("__event_authority")}, meteoraDlmmProgram)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -283,7 +347,11 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI
|
|||||||
|
|
||||||
discriminator := *(*[8]byte)(decode[:8])
|
discriminator := *(*[8]byte)(decode[:8])
|
||||||
switch discriminator {
|
switch discriminator {
|
||||||
case meteoraInitializeLbPairDiscriminator:
|
case meteoraInitializeCustomizablePermissionlessLbPairDiscriminator,
|
||||||
|
meteoraInitializeCustomizablePermissionlessLbPair2Discriminator,
|
||||||
|
meteoraInitializeLbPairDiscriminator,
|
||||||
|
meteoraInitializeLbPair2Discriminator,
|
||||||
|
meteoraInitializePermissionLbPairDiscriminator:
|
||||||
return metaoradlmmInitializeParser(tx, instruction, innerInstructions, offset)
|
return metaoradlmmInitializeParser(tx, instruction, innerInstructions, offset)
|
||||||
case meteoraDlmmInitializePositionDiscriminator, meteoraDlmmInitializePosition2Discriminator,
|
case meteoraDlmmInitializePositionDiscriminator, meteoraDlmmInitializePosition2Discriminator,
|
||||||
meteoraDlmmInitializePositionByOperatorDiscriminator, meteoraDlmmInitializePositionPdaDiscriminator:
|
meteoraDlmmInitializePositionByOperatorDiscriminator, meteoraDlmmInitializePositionPdaDiscriminator:
|
||||||
@@ -294,13 +362,15 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI
|
|||||||
return metaoradlmmSwap2Parser(tx, instruction, innerInstructions, offset)
|
return metaoradlmmSwap2Parser(tx, instruction, innerInstructions, offset)
|
||||||
case meteoraDlmmAddLiquidityDiscriminator, meteoraDlmmAddLiquidity2Discriminator,
|
case meteoraDlmmAddLiquidityDiscriminator, meteoraDlmmAddLiquidity2Discriminator,
|
||||||
meteoraDlmmAddLiquidityByStrategyDiscriminator, meteoraDlmmAddLiquidityByStrategy2Discriminator,
|
meteoraDlmmAddLiquidityByStrategyDiscriminator, meteoraDlmmAddLiquidityByStrategy2Discriminator,
|
||||||
meteoraDlmmAddLiquidityByWeightDiscriminator:
|
meteoraDlmmAddLiquidityByWeightDiscriminator, meteoraDlmmAddLiquidityOneSideDiscriminator,
|
||||||
|
meteoraDlmmAddLiquidityOneSidePreciseDiscriminator, meteoraDlmmAddLiquidityOneSidePrecise2Discriminator,
|
||||||
|
meteoraDlmmAddLiquidityByStrategyOneSideDiscriminator:
|
||||||
return metaoradlmmAddLiquidityParser(tx, instruction, innerInstructions, offset)
|
return metaoradlmmAddLiquidityParser(tx, instruction, innerInstructions, offset)
|
||||||
case meteoraDlmmClaimFeeDiscriminator, meteoraDlmmClaimFee2Discriminator:
|
case meteoraDlmmClaimFeeDiscriminator, meteoraDlmmClaimFee2Discriminator:
|
||||||
return metaoradlmmClaimFeeParser(tx, instruction, innerInstructions, offset)
|
return metaoradlmmClaimFeeParser(tx, instruction, innerInstructions, offset)
|
||||||
case meteoraDlmmRebalanceLiquidityDiscriminator:
|
case meteoraDlmmRebalanceLiquidityDiscriminator:
|
||||||
return metaoradlmmRebalanceLiquidityParser(tx, instruction, innerInstructions, offset)
|
return metaoradlmmRebalanceLiquidityParser(tx, instruction, innerInstructions, offset)
|
||||||
case meteoraDlmmRemoveLiquidityDiscriminator, meteoraDlmmRemoveLiquidity2Discriminator,
|
case meteoraDlmmRemoveAllLiquidityDiscriminator, meteoraDlmmRemoveLiquidityDiscriminator, meteoraDlmmRemoveLiquidity2Discriminator,
|
||||||
meteoraDlmmRemoveLiquidityByRangeDiscriminator, meteoraDlmmRemoveLiquidityByRange2Discriminator:
|
meteoraDlmmRemoveLiquidityByRangeDiscriminator, meteoraDlmmRemoveLiquidityByRange2Discriminator:
|
||||||
return metaoradlmmRemoveLiquidityParser(tx, instruction, innerInstructions, offset)
|
return metaoradlmmRemoveLiquidityParser(tx, instruction, innerInstructions, offset)
|
||||||
case meteoraDlmmClosePositionDiscriminator, meteoraDlmmClosePosition2Discriminator, meteoraDlmmClosePositionIfEmptyDiscriminator:
|
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) {
|
func metaoradlmmInitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
market := tx.rawTx.accountList[instruction.Accounts[0]]
|
accounts, err := resolveDlmmInitializeAccounts(tx.rawTx, instruction.Data, instruction.Accounts)
|
||||||
token0 := tx.rawTx.accountList[instruction.Accounts[2]]
|
if err != nil {
|
||||||
token1 := tx.rawTx.accountList[instruction.Accounts[3]]
|
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]
|
entryContract := tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
|
||||||
var baseDecimals uint8
|
findMintDecimals := func(mint solana.PublicKey) uint8 {
|
||||||
var quoteDecimals uint8
|
for _, acc := range tx.rawTx.Meta.PostTokenBalances {
|
||||||
for _, acc := range tx.rawTx.Meta.PostTokenBalances {
|
if acc.MintAccount.Equals(mint) {
|
||||||
if acc.MintAccount.Equals(token0) {
|
return uint8(acc.UITokenAmount.Decimals)
|
||||||
baseDecimals = uint8(acc.UITokenAmount.Decimals)
|
}
|
||||||
}
|
|
||||||
if acc.MintAccount.Equals(token1) {
|
|
||||||
quoteDecimals = uint8(acc.UITokenAmount.Decimals)
|
|
||||||
}
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
swap := Swap{
|
swap := Swap{
|
||||||
Program: SolProgramMeteoraDLMM,
|
Program: SolProgramMeteoraDLMM,
|
||||||
Event: "create",
|
Event: "create",
|
||||||
Pool: market,
|
Pool: accounts.pool,
|
||||||
BaseMint: token0,
|
BaseMint: accounts.token0,
|
||||||
QuoteMint: token1,
|
QuoteMint: accounts.token1,
|
||||||
BaseTokenProgram: tx.rawTx.accountList[instruction.Accounts[11]],
|
BaseTokenProgram: accounts.baseTokenProgram,
|
||||||
QuoteTokenProgram: tx.rawTx.accountList[instruction.Accounts[12]],
|
QuoteTokenProgram: accounts.quoteTokenProgram,
|
||||||
Creator: tx.rawTx.accountList[0],
|
Creator: tx.rawTx.accountList[0],
|
||||||
BaseMintDecimals: baseDecimals,
|
BaseMintDecimals: findMintDecimals(accounts.token0),
|
||||||
QuoteMintDecimals: quoteDecimals,
|
QuoteMintDecimals: findMintDecimals(accounts.token1),
|
||||||
User: tx.rawTx.accountList[instruction.Accounts[8]],
|
User: accounts.user,
|
||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
}
|
}
|
||||||
var prefixLen = offset[1]
|
createEvent, nextOffset, found, err := dlmmLbPairCreateEventFromInnerInstructions(innerInstructions, instruction, offset)
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
|
||||||
if err != nil {
|
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
|
if found {
|
||||||
|
offset = nextOffset
|
||||||
for innerIndex, innerInstr := range inners {
|
if !createEvent.LbPair.IsZero() {
|
||||||
if innerInstr.ProgramIDIndex == programIndex && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraInitializeLbPairEventDiscriminator[:]) {
|
swap.Pool = createEvent.LbPair
|
||||||
if offset[1] == 0 {
|
|
||||||
offset[0] += 1
|
|
||||||
} else {
|
|
||||||
offset[1] = uint(innerIndex) + 1 + prefixLen
|
|
||||||
}
|
|
||||||
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
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
@@ -410,10 +558,13 @@ func metaoradlmmPositionCreateParser(tx *Tx, instruction Instruction, innerInstr
|
|||||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm create position accounts parse error: %v, offset, %d, %d", err, offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm create position accounts parse error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
createEvent, nextOffset, err := dlmmPositionCreateEventFromInnerInstructions(innerInstructions, instruction, offset)
|
createEvent, nextOffset, found, err := dlmmPositionCreateEventFromInnerInstructions(innerInstructions, instruction, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nextOffset, err
|
return nil, nextOffset, err
|
||||||
}
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, nextOffset, InstructionIgnoredError
|
||||||
|
}
|
||||||
offset = nextOffset
|
offset = nextOffset
|
||||||
|
|
||||||
if !createEvent.LbPair.IsZero() {
|
if !createEvent.LbPair.IsZero() {
|
||||||
@@ -515,22 +666,33 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
}
|
}
|
||||||
|
|
||||||
discriminator := *(*[8]byte)(decode[:8])
|
discriminator := *(*[8]byte)(decode[:8])
|
||||||
|
var swapMode SwapMode
|
||||||
|
var fixedAmount decimal.Decimal
|
||||||
|
var limitAmount decimal.Decimal
|
||||||
switch discriminator {
|
switch discriminator {
|
||||||
case meteoraDlmmSwapDiscriminator, meteoraDlmmSwap2Discriminator:
|
case meteoraDlmmSwapDiscriminator, meteoraDlmmSwap2Discriminator:
|
||||||
var args meteoraDlmmSwapArgs
|
var args meteoraDlmmSwapArgs
|
||||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
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])
|
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:
|
case meteoraDlmmSwapExactOutDiscriminator, meteoraDlmmSwapExactOut2Discriminator:
|
||||||
var args meteoraDlmmSwapExactOutArgs
|
var args meteoraDlmmSwapExactOutArgs
|
||||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
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])
|
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:
|
case meteoraDlmmSwapWithPriceImpactDiscriminator, meteoraDlmmSwapWithPriceImpact2Discriminator:
|
||||||
var args meteoraDlmmSwapWithPriceImpactArgs
|
var args meteoraDlmmSwapWithPriceImpactArgs
|
||||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
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])
|
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:
|
default:
|
||||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
}
|
}
|
||||||
@@ -667,6 +829,18 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
userQuote = userQuote.Add(decimal.NewFromUint64(solAmount))
|
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{
|
swap := Swap{
|
||||||
Program: SolProgramMeteoraDLMM,
|
Program: SolProgramMeteoraDLMM,
|
||||||
@@ -682,6 +856,13 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
User: eventUser,
|
User: eventUser,
|
||||||
BaseAmount: baseAmount,
|
BaseAmount: baseAmount,
|
||||||
QuoteAmount: quoteAmount,
|
QuoteAmount: quoteAmount,
|
||||||
|
FeeAmount: feeAmount,
|
||||||
|
FeeBps: dlmmSwapFeeBpsString(swapEvent.FeeBps),
|
||||||
|
LpFeeAmount: lpFeeAmount,
|
||||||
|
FeeSide: feeSide,
|
||||||
|
FeeMint: feeMint,
|
||||||
|
FeeTokenProgram: feeTokenProgram,
|
||||||
|
FeeMintDecimals: feeDecimals,
|
||||||
BaseReserve: baseReserve,
|
BaseReserve: baseReserve,
|
||||||
QuoteReserve: quoteReserve,
|
QuoteReserve: quoteReserve,
|
||||||
UserBaseBalance: userBase,
|
UserBaseBalance: userBase,
|
||||||
@@ -691,6 +872,7 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
StartBinId: swapEvent.StartBinId,
|
StartBinId: swapEvent.StartBinId,
|
||||||
EndBinId: swapEvent.EndBinId,
|
EndBinId: swapEvent.EndBinId,
|
||||||
}
|
}
|
||||||
|
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||||
|
|
||||||
return []Swap{swap}, offset, nil
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
@@ -699,6 +881,39 @@ func metaoradlmmSwap2Parser(tx *Tx, instruction Instruction, innerInstructions I
|
|||||||
return metaoradlmmSwapParser(tx, instruction, innerInstructions, offset)
|
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) {
|
func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
|
|
||||||
@@ -726,7 +941,7 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
weightDist []dlmmBinLiquidityDistributionByWeight
|
weightDist []dlmmBinLiquidityDistributionByWeight
|
||||||
startBinId int32
|
startBinId int32
|
||||||
endBinId int32
|
endBinId int32
|
||||||
hasRange bool
|
oneSide bool
|
||||||
)
|
)
|
||||||
|
|
||||||
switch discriminator {
|
switch discriminator {
|
||||||
@@ -739,7 +954,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
amountY = args.LiquidityParameter.AmountY
|
amountY = args.LiquidityParameter.AmountY
|
||||||
binDist = args.LiquidityParameter.BinLiquidityDist
|
binDist = args.LiquidityParameter.BinLiquidityDist
|
||||||
startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist)
|
startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist)
|
||||||
hasRange = len(binDist) > 0
|
|
||||||
case meteoraDlmmAddLiquidity2Discriminator:
|
case meteoraDlmmAddLiquidity2Discriminator:
|
||||||
var args dlmmAddLiquidity2Args
|
var args dlmmAddLiquidity2Args
|
||||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||||
@@ -749,7 +963,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
amountY = args.LiquidityParameter.AmountY
|
amountY = args.LiquidityParameter.AmountY
|
||||||
binDist = args.LiquidityParameter.BinLiquidityDist
|
binDist = args.LiquidityParameter.BinLiquidityDist
|
||||||
startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist)
|
startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist)
|
||||||
hasRange = len(binDist) > 0
|
|
||||||
case meteoraDlmmAddLiquidityByStrategyDiscriminator:
|
case meteoraDlmmAddLiquidityByStrategyDiscriminator:
|
||||||
var args dlmmAddLiquidityByStrategyArgs
|
var args dlmmAddLiquidityByStrategyArgs
|
||||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||||
@@ -759,7 +972,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
amountY = args.LiquidityParameter.AmountY
|
amountY = args.LiquidityParameter.AmountY
|
||||||
startBinId = args.LiquidityParameter.StrategyParameters.MinBinId
|
startBinId = args.LiquidityParameter.StrategyParameters.MinBinId
|
||||||
endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId
|
endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId
|
||||||
hasRange = true
|
|
||||||
case meteoraDlmmAddLiquidityByStrategy2Discriminator:
|
case meteoraDlmmAddLiquidityByStrategy2Discriminator:
|
||||||
var args dlmmAddLiquidityByStrategy2Args
|
var args dlmmAddLiquidityByStrategy2Args
|
||||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||||
@@ -769,7 +981,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
amountY = args.LiquidityParameter.AmountY
|
amountY = args.LiquidityParameter.AmountY
|
||||||
startBinId = args.LiquidityParameter.StrategyParameters.MinBinId
|
startBinId = args.LiquidityParameter.StrategyParameters.MinBinId
|
||||||
endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId
|
endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId
|
||||||
hasRange = true
|
|
||||||
case meteoraDlmmAddLiquidityByWeightDiscriminator:
|
case meteoraDlmmAddLiquidityByWeightDiscriminator:
|
||||||
var args dlmmAddLiquidityByWeightArgs
|
var args dlmmAddLiquidityByWeightArgs
|
||||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||||
@@ -779,16 +990,40 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
amountY = args.LiquidityParameter.AmountY
|
amountY = args.LiquidityParameter.AmountY
|
||||||
weightDist = args.LiquidityParameter.BinLiquidityDist
|
weightDist = args.LiquidityParameter.BinLiquidityDist
|
||||||
startBinId, endBinId = dlmmMinMaxBinIdFromWeightDistribution(weightDist)
|
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:
|
default:
|
||||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
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)
|
addEvent, nextOffset, err := dlmmAddLiquidityEventFromInnerInstructions(innerInstructions, instruction, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nextOffset, err
|
return nil, nextOffset, err
|
||||||
@@ -797,14 +1032,17 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
amountX = addEvent.Amounts[0]
|
amountX = addEvent.Amounts[0]
|
||||||
amountY = addEvent.Amounts[1]
|
amountY = addEvent.Amounts[1]
|
||||||
|
|
||||||
binChanges := []DlmmBinLiquidityChange(nil)
|
if oneSide {
|
||||||
if len(binDist) > 0 {
|
swaps, err := dlmmBuildOneSideAddSwap(tx, instruction, addEvent, startBinId, endBinId, entryContract)
|
||||||
binChanges = dlmmBinChangesFromDistribution(amountX, amountY, binDist)
|
if err != nil {
|
||||||
} else if len(weightDist) > 0 {
|
return nil, offset, err
|
||||||
// Weight-only params do not preserve per-side amounts for each bin, so keep the affected range only.
|
}
|
||||||
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, 0)
|
return swaps, offset, nil
|
||||||
} else if hasRange {
|
}
|
||||||
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, 0)
|
|
||||||
|
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]
|
pool := result.accountList[accounts.poolIdx]
|
||||||
@@ -837,7 +1075,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
baseAmount = amountYDec
|
baseAmount = amountYDec
|
||||||
quoteAmount = amountXDec
|
quoteAmount = amountXDec
|
||||||
}
|
}
|
||||||
|
|
||||||
eventUser := result.accountList[accounts.userIdx]
|
eventUser := result.accountList[accounts.userIdx]
|
||||||
if !addEvent.From.IsZero() {
|
if !addEvent.From.IsZero() {
|
||||||
eventUser = addEvent.From
|
eventUser = addEvent.From
|
||||||
@@ -898,7 +1135,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
ActiveBinId: addEvent.ActiveBinId,
|
ActiveBinId: addEvent.ActiveBinId,
|
||||||
StartBinId: startBinId,
|
StartBinId: startBinId,
|
||||||
EndBinId: endBinId,
|
EndBinId: endBinId,
|
||||||
BinChanges: binChanges,
|
|
||||||
PositionAccount: result.accountList[accounts.positionIdx],
|
PositionAccount: result.accountList[accounts.positionIdx],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -926,19 +1162,19 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
|
|||||||
|
|
||||||
discriminator := *(*[8]byte)(decode[:8])
|
discriminator := *(*[8]byte)(decode[:8])
|
||||||
var (
|
var (
|
||||||
binChanges []DlmmBinLiquidityChange
|
|
||||||
startBinId int32
|
startBinId int32
|
||||||
endBinId int32
|
endBinId int32
|
||||||
removeBp int32
|
removeBp int32
|
||||||
)
|
)
|
||||||
|
|
||||||
switch discriminator {
|
switch discriminator {
|
||||||
|
case meteoraDlmmRemoveAllLiquidityDiscriminator:
|
||||||
|
removeBp = 10000
|
||||||
case meteoraDlmmRemoveLiquidityDiscriminator:
|
case meteoraDlmmRemoveLiquidityDiscriminator:
|
||||||
var args dlmmRemoveLiquidityArgs
|
var args dlmmRemoveLiquidityArgs
|
||||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
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])
|
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)
|
startBinId, endBinId = dlmmMinMaxBinIdFromReduction(args.BinLiquidityRemoval)
|
||||||
removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval)
|
removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval)
|
||||||
case meteoraDlmmRemoveLiquidity2Discriminator:
|
case meteoraDlmmRemoveLiquidity2Discriminator:
|
||||||
@@ -946,7 +1182,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
|
|||||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
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])
|
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)
|
startBinId, endBinId = dlmmMinMaxBinIdFromReduction(args.BinLiquidityRemoval)
|
||||||
removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval)
|
removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval)
|
||||||
case meteoraDlmmRemoveLiquidityByRangeDiscriminator:
|
case meteoraDlmmRemoveLiquidityByRangeDiscriminator:
|
||||||
@@ -957,7 +1192,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
|
|||||||
startBinId = args.FromBinId
|
startBinId = args.FromBinId
|
||||||
endBinId = args.ToBinId
|
endBinId = args.ToBinId
|
||||||
removeBp = int32(args.BpsToRemove)
|
removeBp = int32(args.BpsToRemove)
|
||||||
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, args.BpsToRemove)
|
|
||||||
case meteoraDlmmRemoveLiquidityByRange2Discriminator:
|
case meteoraDlmmRemoveLiquidityByRange2Discriminator:
|
||||||
var args dlmmRemoveLiquidityByRange2Args
|
var args dlmmRemoveLiquidityByRange2Args
|
||||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||||
@@ -966,7 +1200,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
|
|||||||
startBinId = args.FromBinId
|
startBinId = args.FromBinId
|
||||||
endBinId = args.ToBinId
|
endBinId = args.ToBinId
|
||||||
removeBp = int32(args.BpsToRemove)
|
removeBp = int32(args.BpsToRemove)
|
||||||
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, args.BpsToRemove)
|
|
||||||
default:
|
default:
|
||||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
}
|
}
|
||||||
@@ -1012,7 +1245,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
|
|||||||
baseAmount = amountYDec
|
baseAmount = amountYDec
|
||||||
quoteAmount = amountXDec
|
quoteAmount = amountXDec
|
||||||
}
|
}
|
||||||
|
|
||||||
eventUser := result.accountList[accounts.userIdx]
|
eventUser := result.accountList[accounts.userIdx]
|
||||||
if !removeEvent.From.IsZero() {
|
if !removeEvent.From.IsZero() {
|
||||||
eventUser = removeEvent.From
|
eventUser = removeEvent.From
|
||||||
@@ -1074,7 +1306,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
|
|||||||
StartBinId: startBinId,
|
StartBinId: startBinId,
|
||||||
EndBinId: endBinId,
|
EndBinId: endBinId,
|
||||||
RemoveBp: removeBp,
|
RemoveBp: removeBp,
|
||||||
BinChanges: binChanges,
|
|
||||||
PositionAccount: result.accountList[accounts.positionIdx],
|
PositionAccount: result.accountList[accounts.positionIdx],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1318,7 +1549,6 @@ func metaoradlmmRebalanceLiquidityParser(tx *Tx, instruction Instruction, innerI
|
|||||||
ActiveBinId: event.ActiveBinId,
|
ActiveBinId: event.ActiveBinId,
|
||||||
StartBinId: event.OldMinBinId,
|
StartBinId: event.OldMinBinId,
|
||||||
EndBinId: event.OldMaxBinId,
|
EndBinId: event.OldMaxBinId,
|
||||||
BinChanges: dlmmBinChangesFromRange(event.OldMinBinId, event.OldMaxBinId, 0),
|
|
||||||
PositionAccount: result.accountList[accounts.positionIdx],
|
PositionAccount: result.accountList[accounts.positionIdx],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1344,7 +1574,6 @@ func metaoradlmmRebalanceLiquidityParser(tx *Tx, instruction Instruction, innerI
|
|||||||
ActiveBinId: event.ActiveBinId,
|
ActiveBinId: event.ActiveBinId,
|
||||||
StartBinId: event.NewMinBinId,
|
StartBinId: event.NewMinBinId,
|
||||||
EndBinId: event.NewMaxBinId,
|
EndBinId: event.NewMaxBinId,
|
||||||
BinChanges: dlmmBinChangesFromRange(event.NewMinBinId, event.NewMaxBinId, 0),
|
|
||||||
PositionAccount: result.accountList[accounts.positionIdx],
|
PositionAccount: result.accountList[accounts.positionIdx],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1478,11 +1707,11 @@ func dlmmRebalancingEventFromInnerInstructions(innerInstructions InnerInstructio
|
|||||||
return dlmmRebalancingEvent{}, increaseOffset(offset), fmt.Errorf("meteora dlmm rebalance liquidity event not found, offset, %d, %d", offset[0], prefixLen)
|
return dlmmRebalancingEvent{}, increaseOffset(offset), fmt.Errorf("meteora dlmm rebalance liquidity event not found, offset, %d, %d", offset[0], prefixLen)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dlmmPositionCreateEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmPositionCreateEvent, [2]uint, error) {
|
func dlmmPositionCreateEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmPositionCreateEvent, [2]uint, bool, error) {
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dlmmPositionCreateEvent{}, increaseOffset(offset), fmt.Errorf("meteora dlmm create position get inner instructions error: %v, offset, %d, %d", err, offset[0], prefixLen)
|
return dlmmPositionCreateEvent{}, increaseOffset(offset), false, fmt.Errorf("meteora dlmm create position get inner instructions error: %v, offset, %d, %d", err, offset[0], prefixLen)
|
||||||
}
|
}
|
||||||
for innerIndex, innerInstr := range inners {
|
for innerIndex, innerInstr := range inners {
|
||||||
if innerInstr.ProgramIDIndex != instruction.ProgramIDIndex {
|
if innerInstr.ProgramIDIndex != instruction.ProgramIDIndex {
|
||||||
@@ -1497,9 +1726,9 @@ func dlmmPositionCreateEventFromInnerInstructions(innerInstructions InnerInstruc
|
|||||||
} else {
|
} else {
|
||||||
offset[1] = uint(innerIndex) + 1 + prefixLen
|
offset[1] = uint(innerIndex) + 1 + prefixLen
|
||||||
}
|
}
|
||||||
return event, offset, nil
|
return event, offset, true, nil
|
||||||
}
|
}
|
||||||
return dlmmPositionCreateEvent{}, increaseOffset(offset), fmt.Errorf("meteora dlmm create position event not found, offset, %d, %d", offset[0], prefixLen)
|
return dlmmPositionCreateEvent{}, increaseOffset(offset), false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dlmmPositionCloseEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmPositionCloseEvent, [2]uint, bool, error) {
|
func dlmmPositionCloseEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmPositionCloseEvent, [2]uint, bool, error) {
|
||||||
@@ -1526,6 +1755,51 @@ func dlmmPositionCloseEventFromInnerInstructions(innerInstructions InnerInstruct
|
|||||||
return dlmmPositionCloseEvent{}, increaseOffset(offset), false, nil
|
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) {
|
func dlmmDecodeAddLiquidityEvent(data []byte) (dlmmAddLiquidityEvent, bool) {
|
||||||
switch {
|
switch {
|
||||||
case len(data) >= 8 && bytes.Equal(data[:8], meteoraDlmmAddLiquidityEventDiscriminator[:]):
|
case len(data) >= 8 && bytes.Equal(data[:8], meteoraDlmmAddLiquidityEventDiscriminator[:]):
|
||||||
@@ -1732,16 +2006,12 @@ func resolveDlmmSwapAccounts(result *RawTx, accounts []int) (dlmmSwapAccounts, e
|
|||||||
if eventAuthorityPos < len(accounts) && accountList[accounts[eventAuthorityPos]].Equals(solana.MemoProgramID) {
|
if eventAuthorityPos < len(accounts) && accountList[accounts[eventAuthorityPos]].Equals(solana.MemoProgramID) {
|
||||||
eventAuthorityPos++
|
eventAuthorityPos++
|
||||||
}
|
}
|
||||||
programPos := eventAuthorityPos + 1
|
if eventAuthorityPos >= len(accounts) {
|
||||||
if programPos >= len(accounts) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !accountList[accounts[eventAuthorityPos]].Equals(meteoraDlmmEventAuthority) {
|
if !accountList[accounts[eventAuthorityPos]].Equals(meteoraDlmmEventAuthority) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !accountList[accounts[programPos]].Equals(meteoraDlmmProgram) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if hostFeePresent && oraclePos+1 < len(accounts) && dlmmIsSigner(result, accounts[oraclePos+1]) {
|
if hostFeePresent && oraclePos+1 < len(accounts) && dlmmIsSigner(result, accounts[oraclePos+1]) {
|
||||||
continue
|
continue
|
||||||
@@ -1819,6 +2089,154 @@ func resolveDlmmLiquidityAccounts(result *RawTx, accounts []int) (dlmmLiquidityA
|
|||||||
}, nil
|
}, 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) {
|
func resolveDlmmClaimFeeAccounts(result *RawTx, data []byte, accounts []int) (dlmmLiquidityAccounts, error) {
|
||||||
if len(data) < 8 {
|
if len(data) < 8 {
|
||||||
return dlmmLiquidityAccounts{}, fmt.Errorf("instruction data too short")
|
return dlmmLiquidityAccounts{}, fmt.Errorf("instruction data too short")
|
||||||
@@ -1990,56 +2408,67 @@ func dlmmTokenBalanceMeta(result *RawTx, accountIndex int) (TokenBalance, bool)
|
|||||||
return TokenBalance{}, false
|
return TokenBalance{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func dlmmBinChangesFromDistribution(amountX, amountY uint64, dist []dlmmBinLiquidityDistribution) []DlmmBinLiquidityChange {
|
func dlmmAllocateByWeights(total uint64, weights []uint64) []decimal.Decimal {
|
||||||
if len(dist) == 0 {
|
if len(weights) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
totalX := decimal.NewFromUint64(amountX)
|
|
||||||
totalY := decimal.NewFromUint64(amountY)
|
sumWeights := uint64(0)
|
||||||
denom := decimal.NewFromInt(10000)
|
for _, weight := range weights {
|
||||||
changes := make([]DlmmBinLiquidityChange, 0, len(dist))
|
sumWeights += weight
|
||||||
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
|
if sumWeights == 0 {
|
||||||
|
sumWeights = uint64(len(weights))
|
||||||
|
weights = append([]uint64(nil), weights...)
|
||||||
|
for i := range weights {
|
||||||
|
weights[i] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
remaining -= amount
|
||||||
|
}
|
||||||
|
allocations[i] = decimal.NewFromUint64(amount)
|
||||||
|
}
|
||||||
|
return allocations
|
||||||
}
|
}
|
||||||
|
|
||||||
func dlmmBinChangesFromReduction(reduction []dlmmBinLiquidityReduction) []DlmmBinLiquidityChange {
|
func dlmmApplySignedAllocation(values []decimal.Decimal, negative bool) []decimal.Decimal {
|
||||||
if len(reduction) == 0 {
|
if !negative {
|
||||||
return nil
|
return values
|
||||||
}
|
}
|
||||||
changes := make([]DlmmBinLiquidityChange, 0, len(reduction))
|
out := make([]decimal.Decimal, len(values))
|
||||||
for _, item := range reduction {
|
for i, value := range values {
|
||||||
changes = append(changes, DlmmBinLiquidityChange{
|
out[i] = value.Neg()
|
||||||
BinId: item.BinId,
|
|
||||||
BpsToRemove: item.BpsToRemove,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return changes
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func dlmmBinChangesFromRange(startBinId, endBinId int32, bpsToRemove uint16) []DlmmBinLiquidityChange {
|
func dlmmMinMaxBinIDFromCompressedDeposits(bins []dlmmCompressedBinDepositAmount) (startBinID, endBinID int32) {
|
||||||
if startBinId > endBinId {
|
if len(bins) == 0 {
|
||||||
startBinId, endBinId = endBinId, startBinId
|
return 0, 0
|
||||||
}
|
}
|
||||||
count := int(endBinId-startBinId) + 1
|
startBinID = bins[0].BinID
|
||||||
if count <= 0 {
|
endBinID = bins[0].BinID
|
||||||
return nil
|
for _, bin := range bins[1:] {
|
||||||
|
if bin.BinID < startBinID {
|
||||||
|
startBinID = bin.BinID
|
||||||
|
}
|
||||||
|
if bin.BinID > endBinID {
|
||||||
|
endBinID = bin.BinID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
changes := make([]DlmmBinLiquidityChange, 0, count)
|
return startBinID, endBinID
|
||||||
for binId := startBinId; binId <= endBinId; binId++ {
|
|
||||||
changes = append(changes, DlmmBinLiquidityChange{
|
|
||||||
BinId: binId,
|
|
||||||
BpsToRemove: bpsToRemove,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return changes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func dlmmCommonRemoveBp(reduction []dlmmBinLiquidityReduction) int32 {
|
func dlmmCommonRemoveBp(reduction []dlmmBinLiquidityReduction) int32 {
|
||||||
|
|||||||
468
metaoradlmm_test.go
Normal file
468
metaoradlmm_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
216
metaorapool.go
216
metaorapool.go
@@ -2,8 +2,10 @@ package pump_parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
agbinary "github.com/gagliardetto/binary"
|
agbinary "github.com/gagliardetto/binary"
|
||||||
"github.com/gagliardetto/solana-go"
|
"github.com/gagliardetto/solana-go"
|
||||||
@@ -15,6 +17,19 @@ type metaoraPoolInitializePoolData struct {
|
|||||||
TokenBAmount uint64 `json:"tokenBAmount"`
|
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 (
|
var (
|
||||||
meteoraVaultProgram = solana.MustPublicKeyFromBase58("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi")
|
meteoraVaultProgram = solana.MustPublicKeyFromBase58("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi")
|
||||||
meteoraVaultDepositDiscriminator = []byte{0xf2, 0x23, 0xc6, 0x89, 0x52, 0xe1, 0xf2, 0xb6}
|
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) {
|
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]]
|
pool := tx.rawTx.accountList[instruction.Accounts[0]]
|
||||||
payer := tx.rawTx.accountList[instruction.Accounts[12]]
|
payer := tx.rawTx.accountList[instruction.Accounts[12]]
|
||||||
|
|
||||||
@@ -846,6 +869,9 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !baseFound || !quoteFound {
|
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")
|
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,
|
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
|
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
152
metaorapool_test.go
Normal 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
|
||||||
|
}
|
||||||
@@ -385,5 +385,12 @@ func metaBcSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerIn
|
|||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if swapEvent.Params != nil {
|
||||||
|
swaps[0].SetSwapAmountInfo(
|
||||||
|
SwapModeExactIn,
|
||||||
|
decimal.NewFromUint64(swapEvent.Params.AmountIn),
|
||||||
|
decimal.NewFromUint64(swapEvent.Params.MinimumAmountOut),
|
||||||
|
)
|
||||||
|
}
|
||||||
return swaps, offset, nil
|
return swaps, offset, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,13 @@ type MetaoraDammInitializePoolEvent struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func meteoraDammV2InitializePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func meteoraDammV2InitializePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if len(instruction.Accounts) < 12 {
|
requiredAccounts := 12
|
||||||
|
if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializePoolWithDynamicConfig[:]) {
|
||||||
|
requiredAccounts = 13
|
||||||
|
} else if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializeCustomizablePoolDiscriminator[:]) {
|
||||||
|
requiredAccounts = 11
|
||||||
|
}
|
||||||
|
if len(instruction.Accounts) < requiredAccounts {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
||||||
}
|
}
|
||||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
@@ -188,6 +194,36 @@ type meteoraDammSwapEvent struct {
|
|||||||
ReserveBAmount uint64
|
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) {
|
func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if len(instruction.Accounts) < 9 {
|
if len(instruction.Accounts) < 9 {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
||||||
@@ -276,28 +312,30 @@ func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerI
|
|||||||
return nil, offset, fmt.Errorf("invalid trade direction")
|
return nil, offset, fmt.Errorf("invalid trade direction")
|
||||||
}
|
}
|
||||||
|
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramMeteoraAmmV2,
|
||||||
Program: SolProgramMeteoraAmmV2,
|
Event: event,
|
||||||
Event: event,
|
Pool: swapEvent.Pool,
|
||||||
Pool: swapEvent.Pool,
|
BaseMint: baseMint,
|
||||||
BaseMint: baseMint,
|
QuoteMint: quoteMint,
|
||||||
QuoteMint: quoteMint,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
QuoteTokenProgram: quoteTokenProgram,
|
Creator: solana.PublicKey{},
|
||||||
Creator: solana.PublicKey{},
|
BaseMintDecimals: baseMintDecimals,
|
||||||
BaseMintDecimals: baseMintDecimals,
|
QuoteMintDecimals: quoteMintDecimals,
|
||||||
QuoteMintDecimals: quoteMintDecimals,
|
User: payer,
|
||||||
User: payer,
|
BaseAmount: baseAmount,
|
||||||
BaseAmount: baseAmount,
|
QuoteAmount: quoteAmount,
|
||||||
QuoteAmount: quoteAmount,
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
if swapMode, fixedAmount, limitAmount, ok := meteoraDammSwapAmountInfo(event, swapEvent.Params); ok {
|
||||||
}, offset, nil
|
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) {
|
func meteoraDammV2RemoveLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if len(instruction.Accounts) < 8 {
|
if len(instruction.Accounts) < 9 {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
||||||
}
|
}
|
||||||
tokenAMint := tx.rawTx.accountList[instruction.Accounts[7]]
|
tokenAMint := tx.rawTx.accountList[instruction.Accounts[7]]
|
||||||
|
|||||||
241
orcawhirpool.go
241
orcawhirpool.go
@@ -1,12 +1,33 @@
|
|||||||
package pump_parser
|
package pump_parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gagliardetto/solana-go"
|
"github.com/gagliardetto/solana-go"
|
||||||
"github.com/shopspring/decimal"
|
"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) {
|
func orcaWhirPoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(orcaProgramID) {
|
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])
|
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])
|
//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])) {
|
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
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
} 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
|
quoteFound = true
|
||||||
}
|
}
|
||||||
if baseFound && quoteFound {
|
if baseFound && quoteFound {
|
||||||
@@ -260,7 +281,7 @@ func orcaWhirPoolLiquidityParser(tx *Tx, instruction Instruction, innerInstructi
|
|||||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
}
|
}
|
||||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
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)) {
|
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")
|
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
|
continue
|
||||||
}
|
}
|
||||||
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
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
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
} 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
|
quoteFound = true
|
||||||
}
|
}
|
||||||
if baseFound && quoteFound {
|
if baseFound && quoteFound {
|
||||||
@@ -367,7 +388,7 @@ func orcaWhirPoolLiquidityV2Parser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
return nil, offset, InstructionIgnoredError
|
return nil, offset, InstructionIgnoredError
|
||||||
}
|
}
|
||||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
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)) {
|
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")
|
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])
|
//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])) {
|
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
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
} 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
|
quoteFound = true
|
||||||
}
|
}
|
||||||
if (baseFound && quoteFound) || i >= 6 {
|
if (baseFound && quoteFound) || i >= 6 {
|
||||||
@@ -472,7 +493,7 @@ func orcaWhirPoolCollectFeeParser(tx *Tx, instruction Instruction, innerInstruct
|
|||||||
return nil, offset, InstructionIgnoredError
|
return nil, offset, InstructionIgnoredError
|
||||||
}
|
}
|
||||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
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)) {
|
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")
|
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])
|
//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])) {
|
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
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
} 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
|
quoteFound = true
|
||||||
}
|
}
|
||||||
if (baseFound && quoteFound) || i >= 6 {
|
if (baseFound && quoteFound) || i >= 6 {
|
||||||
@@ -574,7 +595,7 @@ func orcaWhirPoolCollectFeeV2Parser(tx *Tx, instruction Instruction, innerInstru
|
|||||||
return nil, offset, InstructionIgnoredError
|
return nil, offset, InstructionIgnoredError
|
||||||
}
|
}
|
||||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
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)) {
|
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")
|
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])
|
// 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])) {
|
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
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
} 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
|
quoteFound = true
|
||||||
}
|
}
|
||||||
if (baseFound && quoteFound) || i >= 6 {
|
if (baseFound && quoteFound) || i >= 6 {
|
||||||
@@ -676,7 +697,7 @@ func orcaWhirPoolCollectProtocolFeeV2Parser(tx *Tx, instruction Instruction, inn
|
|||||||
return nil, offset, InstructionIgnoredError
|
return nil, offset, InstructionIgnoredError
|
||||||
}
|
}
|
||||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
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)) {
|
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")
|
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]
|
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]]
|
user := tx.rawTx.accountList[instruction.Accounts[1]]
|
||||||
pool := tx.rawTx.accountList[instruction.Accounts[2]]
|
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])
|
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)) {
|
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
|
||||||
baseAmount = decimal.NewFromInt(int64(amount))
|
baseAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(vault0Account) && to.Equals(token0Account) {
|
if from.Equals(vault0Account) && to.Equals(token0Account) {
|
||||||
event = "buy"
|
event = "buy"
|
||||||
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
|
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
|
||||||
@@ -763,7 +792,7 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
|
|||||||
}
|
}
|
||||||
baseFound = true
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
|
} 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) {
|
if from.Equals(vault1Account) && to.Equals(token1Account) {
|
||||||
event = "sell"
|
event = "sell"
|
||||||
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
|
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
|
||||||
@@ -781,27 +810,28 @@ 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 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,
|
||||||
Program: SolProgramOrcaWhirPool,
|
Event: event,
|
||||||
Event: event,
|
Pool: pool,
|
||||||
Pool: pool,
|
BaseMint: baseTokenBalance.MintAccount,
|
||||||
BaseMint: baseTokenBalance.MintAccount,
|
QuoteMint: quoteTokenBalance.MintAccount,
|
||||||
QuoteMint: quoteTokenBalance.MintAccount,
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
||||||
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
||||||
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
||||||
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
||||||
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
BaseAmount: baseAmount,
|
||||||
BaseAmount: baseAmount,
|
QuoteAmount: quoteAmount,
|
||||||
QuoteAmount: quoteAmount,
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
User: user,
|
||||||
User: user,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
|
||||||
}, offset, nil
|
|
||||||
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
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]
|
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]]
|
user := tx.rawTx.accountList[instruction.Accounts[3]]
|
||||||
pool := tx.rawTx.accountList[instruction.Accounts[4]]
|
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])
|
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)) {
|
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
|
||||||
baseAmount = decimal.NewFromInt(int64(amount))
|
baseAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(vault0Account) && to.Equals(token0Account) {
|
if from.Equals(vault0Account) && to.Equals(token0Account) {
|
||||||
event = "buy"
|
event = "buy"
|
||||||
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
|
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
|
||||||
@@ -864,7 +902,7 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
|
|||||||
}
|
}
|
||||||
baseFound = true
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
|
} 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) {
|
if from.Equals(vault1Account) && to.Equals(token1Account) {
|
||||||
event = "sell"
|
event = "sell"
|
||||||
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
|
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
|
||||||
@@ -883,27 +921,28 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
|
|||||||
}
|
}
|
||||||
offset[1] += uint(skipOffset + 1)
|
offset[1] += uint(skipOffset + 1)
|
||||||
|
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramOrcaWhirPool,
|
||||||
Program: SolProgramOrcaWhirPool,
|
Event: event,
|
||||||
Event: event,
|
Pool: pool,
|
||||||
Pool: pool,
|
BaseMint: baseTokenBalance.MintAccount,
|
||||||
BaseMint: baseTokenBalance.MintAccount,
|
QuoteMint: quoteTokenBalance.MintAccount,
|
||||||
QuoteMint: quoteTokenBalance.MintAccount,
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
||||||
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
||||||
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
||||||
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
||||||
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
BaseAmount: baseAmount,
|
||||||
BaseAmount: baseAmount,
|
QuoteAmount: quoteAmount,
|
||||||
QuoteAmount: quoteAmount,
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
User: user,
|
||||||
User: user,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
|
||||||
}, offset, nil
|
|
||||||
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
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]
|
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]]
|
user := tx.rawTx.accountList[instruction.Accounts[1]]
|
||||||
pool1 := tx.rawTx.accountList[instruction.Accounts[2]]
|
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])
|
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]) {
|
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]) {
|
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
|
||||||
event = "buy"
|
event = "buy"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
} 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
|
baseFound = true
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
} 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]) {
|
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool1UserQuote]) {
|
||||||
event = "sell"
|
event = "sell"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool1UserQuote]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
} 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])
|
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]) {
|
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]) {
|
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool2UserBase]) {
|
||||||
event = "buy"
|
event = "buy"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool2UserBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
} 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
|
baseFound = true
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
} 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]) {
|
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
|
||||||
event = "sell"
|
event = "sell"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
} 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,
|
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
|
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]
|
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]]
|
user := tx.rawTx.accountList[instruction.Accounts[14]]
|
||||||
pool1 := tx.rawTx.accountList[instruction.Accounts[0]]
|
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])
|
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]) {
|
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]) {
|
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
|
||||||
event = "buy"
|
event = "buy"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
} 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
|
baseFound = true
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
} 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]) {
|
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
||||||
event = "sell"
|
event = "sell"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
} 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])
|
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]) {
|
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]) {
|
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
||||||
event = "buy"
|
event = "buy"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
} 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
|
baseFound = true
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
} 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]) {
|
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
|
||||||
event = "sell"
|
event = "sell"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
} 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,
|
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
|
return swaps, offset, nil
|
||||||
}
|
}
|
||||||
|
|||||||
23
orcawhirpool_test.go
Normal file
23
orcawhirpool_test.go
Normal 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")
|
||||||
|
}
|
||||||
724
pump.go
724
pump.go
@@ -33,7 +33,7 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct
|
|||||||
discriminator := *(*[8]byte)(decode[:8])
|
discriminator := *(*[8]byte)(decode[:8])
|
||||||
|
|
||||||
switch discriminator {
|
switch discriminator {
|
||||||
case pumpBuyV2Discriminator, pumpBuyDiscriminator, pumpSellDiscriminator:
|
case pumpBuyExactSolInDiscriminator, pumpBuyDiscriminator, pumpBuyV2Discriminator, pumpBuyExactQuoteInV2Discriminator, pumpSellDiscriminator, pumpSellV2Discriminator:
|
||||||
if tx.Err != nil {
|
if tx.Err != nil {
|
||||||
return failedTxBuyOrSellParser(tx, instruction, innerInstructions, offset)
|
return failedTxBuyOrSellParser(tx, instruction, innerInstructions, offset)
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct
|
|||||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
}
|
}
|
||||||
return CreateParser(tx, instruction, innerInstructions, offset)
|
return CreateParser(tx, instruction, innerInstructions, offset)
|
||||||
case pumpMigrateDiscriminator:
|
case pumpMigrateDiscriminator, pumpMigrateV2Discriminator:
|
||||||
if tx.Err != nil {
|
if tx.Err != nil {
|
||||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
}
|
}
|
||||||
@@ -89,6 +89,56 @@ type PumpCreateEvent struct {
|
|||||||
TokenProgram solana.PublicKey
|
TokenProgram solana.PublicKey
|
||||||
IsMayhemMode bool
|
IsMayhemMode bool
|
||||||
IsCashbackEnabled 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) {
|
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 {
|
for innerIndex, innerInstr := range inners {
|
||||||
if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], pumpCreateEventDiscriminator[:]) {
|
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 {
|
if offset[1] == 0 {
|
||||||
offset[0] += 1
|
offset[0] += 1
|
||||||
} else {
|
} else {
|
||||||
@@ -123,12 +173,25 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
|
|||||||
}
|
}
|
||||||
userIndex := 0
|
userIndex := 0
|
||||||
if bytes.HasPrefix(instr.Data, pumpCreateV2Discriminator[:]) {
|
if bytes.HasPrefix(instr.Data, pumpCreateV2Discriminator[:]) {
|
||||||
|
if len(instr.Accounts) < 6 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
userIndex = instr.Accounts[5]
|
userIndex = instr.Accounts[5]
|
||||||
} else if bytes.HasPrefix(instr.Data, pumpCreateDiscriminator[:]) {
|
} else if bytes.HasPrefix(instr.Data, pumpCreateDiscriminator[:]) {
|
||||||
|
if len(instr.Accounts) < 8 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
userIndex = instr.Accounts[7]
|
userIndex = instr.Accounts[7]
|
||||||
|
} else {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
}
|
}
|
||||||
userBase := getAccountBalanceAfterTx(result, userIndex)
|
userBase := getAccountBalanceAfterTx(result, userIndex)
|
||||||
userQuote, _ := GetSolAfterTx(result, userIndex)
|
userQuote, _ := GetSolAfterTx(result, userIndex)
|
||||||
|
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))
|
totalSupply := decimal.NewFromUint64(createEvent.TokenTotalSupply).Div(decimal.New(1, 6))
|
||||||
tx.Token[createEvent.Mint] = TokenMeta{
|
tx.Token[createEvent.Mint] = TokenMeta{
|
||||||
@@ -146,12 +209,12 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
|
|||||||
Event: "create",
|
Event: "create",
|
||||||
Pool: createEvent.BondingCurve,
|
Pool: createEvent.BondingCurve,
|
||||||
BaseMint: createEvent.Mint,
|
BaseMint: createEvent.Mint,
|
||||||
QuoteMint: solana.PublicKey{},
|
QuoteMint: quoteMint,
|
||||||
BaseTokenProgram: createEvent.TokenProgram,
|
BaseTokenProgram: createEvent.TokenProgram,
|
||||||
QuoteTokenProgram: solana.PublicKey{},
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
Creator: createEvent.Creator,
|
Creator: createEvent.Creator,
|
||||||
BaseMintDecimals: 6,
|
BaseMintDecimals: 6,
|
||||||
QuoteMintDecimals: 9,
|
QuoteMintDecimals: quoteDecimals,
|
||||||
User: createEvent.User,
|
User: createEvent.User,
|
||||||
BaseAmount: decimal.Zero,
|
BaseAmount: decimal.Zero,
|
||||||
QuoteAmount: decimal.Zero,
|
QuoteAmount: decimal.Zero,
|
||||||
@@ -160,7 +223,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
|
|||||||
Mayhem: createEvent.IsMayhemMode,
|
Mayhem: createEvent.IsMayhemMode,
|
||||||
Cashback: createEvent.IsCashbackEnabled,
|
Cashback: createEvent.IsCashbackEnabled,
|
||||||
UserBaseBalance: userBase,
|
UserBaseBalance: userBase,
|
||||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
UserQuoteBalance: userQuoteBalance,
|
||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
},
|
},
|
||||||
}, offset, nil
|
}, offset, nil
|
||||||
@@ -197,6 +260,141 @@ type PumpTradeEvent struct {
|
|||||||
MayhemMode bool
|
MayhemMode bool
|
||||||
CashbackFeeBasisPoints uint64
|
CashbackFeeBasisPoints uint64
|
||||||
Cashback 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 {
|
type PumpTradeFeeArg struct {
|
||||||
@@ -218,6 +416,212 @@ type PumpTradeArgs struct {
|
|||||||
Amount2 uint64
|
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) {
|
func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if tx.Err == nil || tx.Err.UnKnown != "" {
|
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])
|
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
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
|
||||||
user := result.accountList[instruction.Accounts[6]]
|
layout, ok := pumpTradeLayout(instruction)
|
||||||
ataUserIdx := instruction.Accounts[5]
|
if !ok {
|
||||||
userIndex := instruction.Accounts[6]
|
return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction account layout, offset, %d, %d", offset[0], offset[1])
|
||||||
mint := result.accountList[instruction.Accounts[2]]
|
}
|
||||||
|
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
|
var args PumpTradeArgs
|
||||||
err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args)
|
err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -252,30 +662,27 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions
|
|||||||
}
|
}
|
||||||
var event string
|
var event string
|
||||||
var (
|
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"
|
event = "buy_failed"
|
||||||
solAmount = args.Amount1
|
quoteAmount = args.Amount1
|
||||||
tokenAmount = args.Amount2
|
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"
|
event = "buy_failed"
|
||||||
solAmount = args.Amount2
|
quoteAmount = args.Amount2
|
||||||
tokenAmount = args.Amount1
|
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"
|
event = "sell_failed"
|
||||||
solAmount = args.Amount2
|
quoteAmount = args.Amount2
|
||||||
tokenAmount = args.Amount1
|
tokenAmount = args.Amount1
|
||||||
} else {
|
} else {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction discriminator, offset, %d, %d", offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction discriminator, offset, %d, %d", offset[0], offset[1])
|
||||||
}
|
}
|
||||||
var baseTokenProgram solana.PublicKey
|
baseTokenProgram := pumpAccount(result, instruction, layout.BaseTokenProgram)
|
||||||
|
|
||||||
if event == "buy_failed" {
|
|
||||||
baseTokenProgram = result.accountList[instruction.Accounts[8]]
|
|
||||||
} else {
|
|
||||||
baseTokenProgram = result.accountList[instruction.Accounts[9]]
|
|
||||||
}
|
|
||||||
if !user.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
|
if !user.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
|
||||||
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, mint)
|
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, mint)
|
||||||
//&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount
|
//&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount
|
||||||
@@ -287,34 +694,50 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions
|
|||||||
}
|
}
|
||||||
|
|
||||||
userBase := getAccountBalanceAfterTx(result, ataUserIdx)
|
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]
|
bcIdx := instruction.Accounts[layout.Pool]
|
||||||
bcAtaIndex := instruction.Accounts[4]
|
bcAtaIndex := instruction.Accounts[layout.BasePoolToken]
|
||||||
solReserves, _ := GetSolAfterTx(result, bcIdx)
|
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)
|
tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex)
|
||||||
swaps := []Swap{
|
swaps := []Swap{
|
||||||
{
|
{
|
||||||
Program: SolProgramPump,
|
Program: SolProgramPump,
|
||||||
Event: event,
|
Event: event,
|
||||||
Pool: result.accountList[instruction.Accounts[3]],
|
Pool: pumpAccount(result, instruction, layout.Pool),
|
||||||
BaseMint: mint,
|
BaseMint: mint,
|
||||||
QuoteMint: solana.PublicKey{},
|
QuoteMint: quoteMint,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
QuoteTokenProgram: solana.PublicKey{},
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
BaseMintDecimals: 6,
|
BaseMintDecimals: 6,
|
||||||
QuoteMintDecimals: 9,
|
QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
|
||||||
User: user,
|
User: user,
|
||||||
BaseAmount: decimal.NewFromUint64(tokenAmount),
|
BaseAmount: decimal.NewFromUint64(tokenAmount),
|
||||||
QuoteAmount: decimal.NewFromUint64(solAmount),
|
QuoteAmount: decimal.NewFromUint64(quoteAmount),
|
||||||
BaseReserve: tokenReserves,
|
BaseReserve: tokenReserves,
|
||||||
QuoteReserve: decimal.NewFromUint64(solReserves),
|
QuoteReserve: quoteReserves,
|
||||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
|
Mayhem: isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
|
||||||
UserBaseBalance: userBase,
|
UserBaseBalance: userBase,
|
||||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
UserQuoteBalance: userQuote,
|
||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
|
||||||
|
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||||
|
normalizePumpQuoteSideMint(&swaps[0])
|
||||||
|
}
|
||||||
return swaps, offset, nil
|
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 entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
var programIndex = instruction.ProgramIDIndex
|
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
|
feeEventProgramIndex := 0
|
||||||
for i, b := range result.accountList {
|
for i, b := range result.accountList {
|
||||||
@@ -337,6 +764,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
completeEvent CompleteEvent
|
completeEvent CompleteEvent
|
||||||
completed bool
|
completed bool
|
||||||
newoffset [2]uint
|
newoffset [2]uint
|
||||||
|
tradeFound bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
@@ -357,6 +785,9 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
|
|
||||||
for innerIndex, innerInstr := range inners {
|
for innerIndex, innerInstr := range inners {
|
||||||
if innerInstr.ProgramIDIndex == feeEventProgramIndex && bytes.Equal(innerInstr.Data[:8], pumpGetFeesDiscriminator[:]) {
|
if innerInstr.ProgramIDIndex == feeEventProgramIndex && bytes.Equal(innerInstr.Data[:8], pumpGetFeesDiscriminator[:]) {
|
||||||
|
if tradeFound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
err = agbinary.NewBorshDecoder(innerInstr.Data[8:]).Decode(&tradeFeeArg)
|
err = agbinary.NewBorshDecoder(innerInstr.Data[8:]).Decode(&tradeFeeArg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("pump get fees event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("pump get fees event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
@@ -365,7 +796,10 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
}
|
}
|
||||||
if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) {
|
if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) {
|
||||||
if bytes.Equal(innerInstr.Data[8:16], pumpTradeEventDiscriminator[8:16]) {
|
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 {
|
if offset[1] == 0 {
|
||||||
newoffset = [2]uint{offset[0] + 1, offset[1]}
|
newoffset = [2]uint{offset[0] + 1, offset[1]}
|
||||||
} else {
|
} else {
|
||||||
@@ -374,39 +808,59 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
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 {
|
if !tradeEvent.IsBuy {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else if bytes.Equal(innerInstr.Data[8:16], pumpCompleteEventDiscriminator[:]) {
|
} else if bytes.Equal(innerInstr.Data[8:16], pumpCompleteEventDiscriminator[:]) {
|
||||||
|
if !tradeFound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&completeEvent)
|
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 {
|
if offset[1] == 0 {
|
||||||
newoffset = [2]uint{offset[0] + 1, offset[1]}
|
newoffset = [2]uint{offset[0] + 1, offset[1]}
|
||||||
} else {
|
} else {
|
||||||
newoffset = [2]uint{offset[0], prefixLen + uint(innerIndex) + 1}
|
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
|
completed = true
|
||||||
break
|
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])
|
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]}
|
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 := ""
|
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 {
|
if tradeEvent.IsBuy {
|
||||||
event = "buy"
|
event = "buy"
|
||||||
baseTokenProgram = result.accountList[instruction.Accounts[8]]
|
|
||||||
} else {
|
} else {
|
||||||
event = "sell"
|
event = "sell"
|
||||||
baseTokenProgram = result.accountList[instruction.Accounts[9]]
|
|
||||||
}
|
}
|
||||||
if _, exists := tx.Token[tradeEvent.Mint]; !exists {
|
if _, exists := tx.Token[tradeEvent.Mint]; !exists {
|
||||||
tx.Token[tradeEvent.Mint] = TokenMeta{
|
tx.Token[tradeEvent.Mint] = TokenMeta{
|
||||||
@@ -418,8 +872,8 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
|
|
||||||
var user = tradeEvent.User
|
var user = tradeEvent.User
|
||||||
|
|
||||||
ataUserIdx := instruction.Accounts[5]
|
ataUserIdx := instruction.Accounts[layout.BaseUserToken]
|
||||||
userIndex := instruction.Accounts[6]
|
userIndex := instruction.Accounts[layout.User]
|
||||||
if !tradeEvent.User.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
|
if !tradeEvent.User.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
|
||||||
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, tradeEvent.Mint)
|
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, tradeEvent.Mint)
|
||||||
//&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount
|
//&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount
|
||||||
@@ -431,14 +885,20 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
}
|
}
|
||||||
|
|
||||||
userBase := getAccountBalanceAfterTx(result, ataUserIdx)
|
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
|
quoteAmount := pumpQuoteAmount(tradeEvent)
|
||||||
if tradeEvent.IsBuy && bytes.Equal(instruction.Data[:8], pumpBuyV2Discriminator[:]) {
|
if tradeEvent.IsBuy && pumpInstructionIsExactQuoteIn(instruction.Data) && !layout.IsV2 {
|
||||||
fee := tradeEvent.Fee + tradeEvent.CreatorFee
|
fee := tradeEvent.Fee + tradeEvent.CreatorFee
|
||||||
solAmount = tradeFeeArg.TradeSize
|
quoteAmount = tradeFeeArg.TradeSize
|
||||||
if solAmount > fee {
|
if quoteAmount > fee {
|
||||||
solAmount = solAmount - fee
|
quoteAmount = quoteAmount - fee
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isCashbackCoin := tradeEvent.CashbackFeeBasisPoints > 0 || tradeEvent.Cashback > 0
|
isCashbackCoin := tradeEvent.CashbackFeeBasisPoints > 0 || tradeEvent.Cashback > 0
|
||||||
@@ -446,44 +906,48 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
{
|
{
|
||||||
Program: SolProgramPump,
|
Program: SolProgramPump,
|
||||||
Event: event,
|
Event: event,
|
||||||
Pool: result.accountList[instruction.Accounts[3]],
|
Pool: pumpAccount(result, instruction, layout.Pool),
|
||||||
BaseMint: tradeEvent.Mint,
|
BaseMint: tradeEvent.Mint,
|
||||||
QuoteMint: solana.PublicKey{},
|
QuoteMint: quoteMint,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
QuoteTokenProgram: solana.PublicKey{},
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
Creator: tradeEvent.Creator,
|
Creator: tradeEvent.Creator,
|
||||||
BaseMintDecimals: 6,
|
BaseMintDecimals: 6,
|
||||||
QuoteMintDecimals: 9,
|
QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
|
||||||
User: user,
|
User: user,
|
||||||
BaseAmount: decimal.NewFromUint64(tradeEvent.TokenAmount),
|
BaseAmount: decimal.NewFromUint64(tradeEvent.TokenAmount),
|
||||||
QuoteAmount: decimal.NewFromUint64(solAmount),
|
QuoteAmount: decimal.NewFromUint64(quoteAmount),
|
||||||
BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves),
|
BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves),
|
||||||
QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves),
|
QuoteReserve: decimal.NewFromUint64(pumpQuoteReserve(tradeEvent)),
|
||||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
|
Mayhem: tradeEvent.MayhemMode || isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
|
||||||
UserBaseBalance: userBase,
|
UserBaseBalance: userBase,
|
||||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
UserQuoteBalance: userQuote,
|
||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
Cashback: isCashbackCoin,
|
Cashback: isCashbackCoin,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
|
||||||
|
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||||
|
normalizePumpQuoteSideMint(&swaps[0])
|
||||||
|
}
|
||||||
if completed {
|
if completed {
|
||||||
swaps = append(swaps, Swap{
|
swaps = append(swaps, Swap{
|
||||||
Program: SolProgramPump,
|
Program: SolProgramPump,
|
||||||
Event: "complete",
|
Event: "complete",
|
||||||
Pool: result.accountList[instruction.Accounts[3]],
|
Pool: pumpAccount(result, instruction, layout.Pool),
|
||||||
BaseMint: tradeEvent.Mint,
|
BaseMint: tradeEvent.Mint,
|
||||||
QuoteMint: solana.PublicKey{},
|
QuoteMint: quoteMint,
|
||||||
BaseTokenProgram: result.accountList[instruction.Accounts[8]],
|
BaseTokenProgram: baseTokenProgram,
|
||||||
QuoteTokenProgram: solana.PublicKey{},
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
Creator: tradeEvent.Creator,
|
Creator: tradeEvent.Creator,
|
||||||
BaseMintDecimals: 6,
|
BaseMintDecimals: 6,
|
||||||
QuoteMintDecimals: 9,
|
QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
|
||||||
User: user,
|
User: user,
|
||||||
BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves),
|
BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves),
|
||||||
QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves),
|
QuoteReserve: decimal.NewFromUint64(pumpQuoteReserve(tradeEvent)),
|
||||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
|
Mayhem: tradeEvent.MayhemMode || isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
|
||||||
UserBaseBalance: userBase,
|
UserBaseBalance: userBase,
|
||||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
UserQuoteBalance: userQuote,
|
||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -505,11 +969,74 @@ type MigrateEvent struct {
|
|||||||
Pool solana.PublicKey
|
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) {
|
func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
programIndex := instr.ProgramIDIndex
|
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
|
ammprogramIdx := 0
|
||||||
for i, b := range result.accountList {
|
for i, b := range result.accountList {
|
||||||
if b.Equals(pumpAmmProgram) {
|
if b.Equals(pumpAmmProgram) {
|
||||||
@@ -566,20 +1093,45 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction
|
|||||||
|
|
||||||
offset = [2]uint{newoffset[0], newoffset[1]}
|
offset = [2]uint{newoffset[0], newoffset[1]}
|
||||||
// verify migrate by checking create pool and migrate event
|
// verify migrate by checking create pool and migrate event
|
||||||
userIndex := instr.Accounts[5]
|
userIndex := instr.Accounts[layout.User]
|
||||||
ataBondingCurveAccountIndex := instr.Accounts[4]
|
ataBondingCurveAccountIndex := instr.Accounts[layout.BasePoolToken]
|
||||||
bc, err := getTokenBalanceAfterTx(result, ataBondingCurveAccountIndex)
|
bc, err := getTokenBalanceAfterTx(result, ataBondingCurveAccountIndex)
|
||||||
if err != nil || bc == nil {
|
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])
|
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
|
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
|
var userBase decimal.Decimal
|
||||||
if result.accountList[userIndex].Equals(pumpMigrationAccount) {
|
if result.accountList[userIndex].Equals(pumpMigrationAccount) {
|
||||||
userBase = decimal.Zero
|
userBase = decimal.Zero
|
||||||
} else {
|
} else {
|
||||||
userBase = GetTokenBalanceAfterTx(result, userIndex, baseTokenProgram, migrateEvent.Mint)
|
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 {
|
if _, exists := tx.Token[migrateEvent.Mint]; !exists {
|
||||||
tx.Token[migrateEvent.Mint] = TokenMeta{
|
tx.Token[migrateEvent.Mint] = TokenMeta{
|
||||||
@@ -594,22 +1146,22 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction
|
|||||||
Event: "migrate",
|
Event: "migrate",
|
||||||
Pool: migrateEvent.BondingCurve,
|
Pool: migrateEvent.BondingCurve,
|
||||||
BaseMint: migrateEvent.Mint,
|
BaseMint: migrateEvent.Mint,
|
||||||
QuoteMint: solana.PublicKey{},
|
QuoteMint: quoteMint,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
QuoteTokenProgram: solana.PublicKey{},
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
Creator: createEvent.Creator,
|
Creator: createEvent.Creator,
|
||||||
BaseMintDecimals: 6,
|
BaseMintDecimals: 6,
|
||||||
QuoteMintDecimals: 9,
|
QuoteMintDecimals: quoteDecimals,
|
||||||
User: migrateEvent.User,
|
User: migrateEvent.User,
|
||||||
//BaseAmount: decimal.Decimal{},
|
//BaseAmount: decimal.Decimal{},
|
||||||
//QuoteAmount: decimal.Decimal{},
|
//QuoteAmount: decimal.Decimal{},
|
||||||
BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount),
|
BaseReserve: baseReserve,
|
||||||
QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount),
|
QuoteReserve: quoteReserve,
|
||||||
Mayhem: createEvent.IsMayhemMode,
|
Mayhem: createEvent.IsMayhemMode,
|
||||||
MigrateTopProgram: pumpAmmProgram,
|
MigrateTopProgram: pumpAmmProgram,
|
||||||
MigrateToPool: migrateEvent.Pool,
|
MigrateToPool: migrateEvent.Pool,
|
||||||
UserBaseBalance: userBase,
|
UserBaseBalance: userBase,
|
||||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
UserQuoteBalance: userQuote,
|
||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -618,20 +1170,20 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction
|
|||||||
Event: "create",
|
Event: "create",
|
||||||
Pool: migrateEvent.Pool,
|
Pool: migrateEvent.Pool,
|
||||||
BaseMint: migrateEvent.Mint,
|
BaseMint: migrateEvent.Mint,
|
||||||
QuoteMint: wSolMint,
|
QuoteMint: quoteMint,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
QuoteTokenProgram: solana.TokenProgramID,
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
Creator: createEvent.Creator,
|
Creator: createEvent.Creator,
|
||||||
BaseMintDecimals: 6,
|
BaseMintDecimals: 6,
|
||||||
QuoteMintDecimals: 9,
|
QuoteMintDecimals: quoteDecimals,
|
||||||
User: migrateEvent.User,
|
User: migrateEvent.User,
|
||||||
BaseAmount: decimal.NewFromUint64(migrateEvent.MintAmount),
|
BaseAmount: baseAmount,
|
||||||
QuoteAmount: decimal.NewFromUint64(migrateEvent.SolAmount),
|
QuoteAmount: quoteAmount,
|
||||||
BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount),
|
BaseReserve: baseReserve,
|
||||||
QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount),
|
QuoteReserve: quoteReserve,
|
||||||
Mayhem: createEvent.IsMayhemMode,
|
Mayhem: createEvent.IsMayhemMode,
|
||||||
UserBaseBalance: userBase,
|
UserBaseBalance: userBase,
|
||||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
UserQuoteBalance: userQuote,
|
||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
249
pump_test.go
249
pump_test.go
@@ -1,6 +1,7 @@
|
|||||||
package pump_parser
|
package pump_parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -11,6 +12,31 @@ import (
|
|||||||
"github.com/mr-tron/base58"
|
"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) {
|
func TestTradeEvent(t *testing.T) {
|
||||||
hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e"
|
hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e"
|
||||||
d, err := hex.DecodeString(hexData)
|
d, err := hex.DecodeString(hexData)
|
||||||
@@ -18,13 +44,21 @@ func TestTradeEvent(t *testing.T) {
|
|||||||
t.Errorf("Failed to decode base64 data: %v", err)
|
t.Errorf("Failed to decode base64 data: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var tradeEvent PumpTradeEvent
|
var tradeEvent legacyPumpTradeEvent
|
||||||
|
|
||||||
err = agbinary.NewBorshDecoder(d[16:]).Decode(&tradeEvent)
|
err = agbinary.NewBorshDecoder(d[16:]).Decode(&tradeEvent)
|
||||||
if err != nil {
|
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)
|
t.Logf("Trade Event: %+v", tradeEvent)
|
||||||
|
|
||||||
xx, err := base58.Decode("3Bxs48EzTZB4tzRd")
|
xx, err := base58.Decode("3Bxs48EzTZB4tzRd")
|
||||||
@@ -43,3 +77,212 @@ func TestCal(t *testing.T) {
|
|||||||
|
|
||||||
fmt.Println(solana.MustPublicKeyFromBase58("BM9CcyErJcu2mjrFvUsRRrD3snGeHDDVirJLvL6EjvMN").IsOnCurve())
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
242
pumpamm.go
242
pumpamm.go
@@ -182,6 +182,9 @@ func pumpAmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ammCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func ammCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
|
if len(instruction.Accounts) < 15 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
@@ -261,7 +264,23 @@ type PumpSwapArgs struct {
|
|||||||
Amount2 uint64
|
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) {
|
func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
if tx.Err == nil || tx.Err.UnKnown != "" {
|
if tx.Err == nil || tx.Err.UnKnown != "" {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
||||||
}
|
}
|
||||||
@@ -361,31 +380,36 @@ func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions Inn
|
|||||||
}
|
}
|
||||||
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
|
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
|
||||||
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
|
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramPumpAMM,
|
||||||
Program: SolProgramPumpAMM,
|
Event: event,
|
||||||
Event: event,
|
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
|
||||||
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
|
BaseMint: baseMint,
|
||||||
BaseMint: baseMint,
|
QuoteMint: quoteMint,
|
||||||
QuoteMint: quoteMint,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
QuoteTokenProgram: quoteTokenProgram,
|
BaseMintDecimals: baseMintDecimals,
|
||||||
BaseMintDecimals: baseMintDecimals,
|
QuoteMintDecimals: quoteMintDecimals,
|
||||||
QuoteMintDecimals: quoteMintDecimals,
|
User: eventUser,
|
||||||
User: eventUser,
|
BaseAmount: decimal.NewFromUint64(tokenAmount),
|
||||||
BaseAmount: decimal.NewFromUint64(tokenAmount),
|
QuoteAmount: decimal.NewFromUint64(quoteAmount),
|
||||||
QuoteAmount: decimal.NewFromUint64(quoteAmount),
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
||||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
if swapMode, fixedAmount, limitAmount, ok := pumpAmmSwapAmountInfoFromArgs(args); ok {
|
||||||
}, offset, nil
|
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) {
|
func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
if tx.Err == nil || tx.Err.UnKnown != "" {
|
if tx.Err == nil || tx.Err.UnKnown != "" {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
||||||
}
|
}
|
||||||
@@ -479,28 +503,30 @@ func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
}
|
}
|
||||||
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
|
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
|
||||||
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
|
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramPumpAMM,
|
||||||
Program: SolProgramPumpAMM,
|
Event: event,
|
||||||
Event: event,
|
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
|
||||||
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
|
BaseMint: baseMint,
|
||||||
BaseMint: baseMint,
|
QuoteMint: quoteMint,
|
||||||
QuoteMint: quoteMint,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
QuoteTokenProgram: quoteTokenProgram,
|
BaseMintDecimals: baseMintDecimals,
|
||||||
BaseMintDecimals: baseMintDecimals,
|
QuoteMintDecimals: quoteMintDecimals,
|
||||||
QuoteMintDecimals: quoteMintDecimals,
|
User: eventUser,
|
||||||
User: eventUser,
|
BaseAmount: decimal.NewFromUint64(tokenAmount),
|
||||||
BaseAmount: decimal.NewFromUint64(tokenAmount),
|
QuoteAmount: decimal.NewFromUint64(quoteAmount),
|
||||||
QuoteAmount: decimal.NewFromUint64(quoteAmount),
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
||||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
if swapMode, fixedAmount, limitAmount, ok := pumpAmmSwapAmountInfoFromArgs(args); ok {
|
||||||
}, offset, nil
|
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) {
|
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 entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
|
return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
|
||||||
@@ -599,36 +628,55 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
|||||||
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
|
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
|
||||||
}
|
}
|
||||||
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
|
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
|
||||||
return []Swap{
|
quoteAmount := decimal.NewFromUint64(event.UserQuoteAmountIn)
|
||||||
{
|
if event.IxName == "buy" {
|
||||||
Program: SolProgramPumpAMM,
|
quoteAmount = decimal.NewFromUint64(event.QuoteAmountIn)
|
||||||
Event: "buy",
|
}
|
||||||
Pool: event.Pool,
|
swap := Swap{
|
||||||
BaseMint: baseMint,
|
Program: SolProgramPumpAMM,
|
||||||
QuoteMint: quoteMint,
|
Event: "buy",
|
||||||
BaseTokenProgram: baseTokenProgram,
|
Pool: event.Pool,
|
||||||
QuoteTokenProgram: quoteTokenProgram,
|
BaseMint: baseMint,
|
||||||
Creator: event.CoinCreator,
|
QuoteMint: quoteMint,
|
||||||
BaseMintDecimals: baseMintDecimals,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
QuoteMintDecimals: quoteMintDecimals,
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
User: eventUser,
|
Creator: event.CoinCreator,
|
||||||
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
|
BaseMintDecimals: baseMintDecimals,
|
||||||
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountIn),
|
QuoteMintDecimals: quoteMintDecimals,
|
||||||
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
|
User: eventUser,
|
||||||
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
|
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
|
||||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
QuoteAmount: quoteAmount,
|
||||||
Cashback: isCashbackCoin,
|
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
|
||||||
UserBaseBalance: userBase,
|
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
|
||||||
UserQuoteBalance: userQuote,
|
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
||||||
EntryContract: entryContract,
|
Cashback: isCashbackCoin,
|
||||||
},
|
UserBaseBalance: userBase,
|
||||||
}, offset, nil
|
UserQuoteBalance: userQuote,
|
||||||
|
EntryContract: entryContract,
|
||||||
|
}
|
||||||
|
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) {
|
func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -722,36 +770,43 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
|||||||
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
|
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
|
||||||
}
|
}
|
||||||
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
|
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramPumpAMM,
|
||||||
Program: SolProgramPumpAMM,
|
Event: "sell",
|
||||||
Event: "sell",
|
Pool: event.Pool,
|
||||||
Pool: event.Pool,
|
BaseMint: baseMint,
|
||||||
BaseMint: baseMint,
|
QuoteMint: quoteMint,
|
||||||
QuoteMint: quoteMint,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
QuoteTokenProgram: quoteTokenProgram,
|
Creator: event.CoinCreator,
|
||||||
Creator: event.CoinCreator,
|
BaseMintDecimals: baseMintDecimals,
|
||||||
BaseMintDecimals: baseMintDecimals,
|
QuoteMintDecimals: quoteMintDecimals,
|
||||||
QuoteMintDecimals: quoteMintDecimals,
|
User: eventUser,
|
||||||
User: eventUser,
|
BaseAmount: decimal.NewFromUint64(event.BaseAmountIn),
|
||||||
BaseAmount: decimal.NewFromUint64(event.BaseAmountIn),
|
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountOut),
|
||||||
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountOut),
|
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn),
|
||||||
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn),
|
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut),
|
||||||
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut),
|
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
||||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
Cashback: isCashbackCoin,
|
||||||
Cashback: isCashbackCoin,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
swap.SetSwapAmountInfo(
|
||||||
}, offset, nil
|
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) {
|
func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
|
if len(instruction.Accounts) < 11 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -850,6 +905,9 @@ func withdrawParse(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
|||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
|
if len(instruction.Accounts) < 11 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
197
rawtx.go
197
rawtx.go
@@ -1,8 +1,12 @@
|
|||||||
package pump_parser
|
package pump_parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
bin "github.com/gagliardetto/binary"
|
bin "github.com/gagliardetto/binary"
|
||||||
@@ -105,10 +109,11 @@ func (tx *RawTx) GetBlockTime() *pgtype.Timestamptz {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Instruction struct {
|
type Instruction struct {
|
||||||
Accounts []int `json:"accounts"`
|
Accounts []int `json:"accounts"`
|
||||||
Data solana.Base58 `json:"data"`
|
Data solana.Base58 `json:"data"`
|
||||||
ProgramIDIndex int `json:"programIdIndex"`
|
ProgramIDIndex int `json:"programIdIndex"`
|
||||||
StackHeight *int `json:"stackHeight"`
|
StackHeight *int `json:"stackHeight"`
|
||||||
|
LogEvents []solana.Base64 `json:"logEvents,omitempty"`
|
||||||
}
|
}
|
||||||
type InnerInstructions struct {
|
type InnerInstructions struct {
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
@@ -180,6 +185,11 @@ type Transaction struct {
|
|||||||
Signatures []solana.Signature `json:"signatures"`
|
Signatures []solana.Signature `json:"signatures"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RawTxConvertOptions struct {
|
||||||
|
IgnoreLogMessages bool
|
||||||
|
ParseLogEvents bool
|
||||||
|
}
|
||||||
|
|
||||||
func (tx *Transaction) UnmarshalJSON(data []byte) error {
|
func (tx *Transaction) UnmarshalJSON(data []byte) error {
|
||||||
if len(data) == 0 || (len(data) == 4 && string(data) == "null") {
|
if len(data) == 0 || (len(data) == 4 && string(data) == "null") {
|
||||||
// TODO: is this an error?
|
// TODO: is this an error?
|
||||||
@@ -308,7 +318,8 @@ func marshalRpcTransactionErr(err any) string {
|
|||||||
return string(e)
|
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)
|
created := int64(0)
|
||||||
if blockTime != nil {
|
if blockTime != nil {
|
||||||
created = int64(*blockTime)
|
created = int64(*blockTime)
|
||||||
@@ -523,6 +534,8 @@ func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, s
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyRawTxConvertLogOptions(sTx, option)
|
||||||
|
|
||||||
return sTx, nil
|
return sTx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,7 +846,8 @@ func isAccountOwner(account, owner, mint solana.PublicKey) (bool, error) {
|
|||||||
return account == ata, nil
|
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{
|
sTx := &RawTx{
|
||||||
BlockTime: created,
|
BlockTime: created,
|
||||||
Slot: y.Slot,
|
Slot: y.Slot,
|
||||||
@@ -863,6 +877,9 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT
|
|||||||
//Version: nil,
|
//Version: nil,
|
||||||
}
|
}
|
||||||
meta := y.Transaction.GetMeta()
|
meta := y.Transaction.GetMeta()
|
||||||
|
if meta == nil {
|
||||||
|
return nil, errors.New("meta can not parser")
|
||||||
|
}
|
||||||
yTx := y.Transaction.Transaction
|
yTx := y.Transaction.Transaction
|
||||||
|
|
||||||
if meta.Err != nil && len(meta.Err.GetErr()) > 0 {
|
if meta.Err != nil && len(meta.Err.GetErr()) > 0 {
|
||||||
@@ -872,7 +889,9 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT
|
|||||||
}
|
}
|
||||||
sTx.Meta.Fee = meta.Fee
|
sTx.Meta.Fee = meta.Fee
|
||||||
//sTx.Meta.InnerInstructions = meta.InnerInstructions
|
//sTx.Meta.InnerInstructions = meta.InnerInstructions
|
||||||
sTx.Meta.ComputeUnitsConsumed = *meta.ComputeUnitsConsumed
|
if meta.ComputeUnitsConsumed != nil {
|
||||||
|
sTx.Meta.ComputeUnitsConsumed = *meta.ComputeUnitsConsumed
|
||||||
|
}
|
||||||
for _, innerInstr := range meta.InnerInstructions {
|
for _, innerInstr := range meta.InnerInstructions {
|
||||||
var instrs []Instruction
|
var instrs []Instruction
|
||||||
for _, instr := range innerInstr.Instructions {
|
for _, instr := range innerInstr.Instructions {
|
||||||
@@ -1000,6 +1019,8 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyRawTxConvertLogOptions(sTx, option)
|
||||||
|
|
||||||
// resolve the lookups
|
// resolve the lookups
|
||||||
//{
|
//{
|
||||||
// if sTx.Transaction.Message.IsVersioned() {
|
// if sTx.Transaction.Message.IsVersioned() {
|
||||||
@@ -1019,6 +1040,168 @@ func newInt16(x uint16) *int {
|
|||||||
return &y
|
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 {
|
func newInt(x *uint32) *int {
|
||||||
if x == nil {
|
if x == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
1812
rawtx_binary.go
Normal file
1812
rawtx_binary.go
Normal file
File diff suppressed because it is too large
Load Diff
434
rawtx_binary_test.go
Normal file
434
rawtx_binary_test.go
Normal 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
|
||||||
|
}
|
||||||
122
raydiumclmm.go
122
raydiumclmm.go
@@ -1,12 +1,27 @@
|
|||||||
package pump_parser
|
package pump_parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gagliardetto/solana-go"
|
"github.com/gagliardetto/solana-go"
|
||||||
"github.com/shopspring/decimal"
|
"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) {
|
func raydiumClmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumClmmProgramID) {
|
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])
|
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 {
|
switch discriminator {
|
||||||
case raydiumClmmIncreaseLiquidityDiscriminator:
|
case raydiumClmmIncreaseLiquidityDiscriminator:
|
||||||
accountMin = 12
|
accountMin = 12
|
||||||
market = tx.rawTx.accountList[instruction.Accounts[2]]
|
|
||||||
vault0 = instruction.Accounts[9]
|
|
||||||
vault1 = instruction.Accounts[10]
|
|
||||||
case raydiumClmmIncreaseLiquidityV2Discriminator:
|
case raydiumClmmIncreaseLiquidityV2Discriminator:
|
||||||
accountMin = 15
|
accountMin = 15
|
||||||
market = tx.rawTx.accountList[instruction.Accounts[2]]
|
|
||||||
vault0 = instruction.Accounts[9]
|
|
||||||
vault1 = instruction.Accounts[10]
|
|
||||||
//token0 = tx.rawTx.accountList[instruction.Accounts[13]]
|
|
||||||
//token1 = tx.rawTx.accountList[instruction.Accounts[14]]
|
|
||||||
case raydiumClmmOpenPositionDiscriminator:
|
case raydiumClmmOpenPositionDiscriminator:
|
||||||
accountMin = 19
|
accountMin = 19
|
||||||
market = tx.rawTx.accountList[instruction.Accounts[5]]
|
|
||||||
vault0 = instruction.Accounts[12]
|
|
||||||
vault1 = instruction.Accounts[13]
|
|
||||||
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
|
||||||
case raydiumClmmOpenPositionV2Discriminator:
|
case raydiumClmmOpenPositionV2Discriminator:
|
||||||
accountMin = 22
|
accountMin = 22
|
||||||
market = tx.rawTx.accountList[instruction.Accounts[5]]
|
|
||||||
vault0 = instruction.Accounts[12]
|
|
||||||
vault1 = instruction.Accounts[13]
|
|
||||||
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
|
||||||
//token0 = tx.rawTx.accountList[instruction.Accounts[20]]
|
|
||||||
//token1 = tx.rawTx.accountList[instruction.Accounts[21]]
|
|
||||||
case raydiumClmmOpenPositionWithToken22NftDiscriminator:
|
case raydiumClmmOpenPositionWithToken22NftDiscriminator:
|
||||||
accountMin = 20
|
accountMin = 20
|
||||||
market = tx.rawTx.accountList[instruction.Accounts[4]]
|
|
||||||
vault0 = instruction.Accounts[11]
|
|
||||||
vault1 = instruction.Accounts[12]
|
|
||||||
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
|
||||||
//token0 = tx.rawTx.accountList[instruction.Accounts[18]]
|
|
||||||
//token1 = tx.rawTx.accountList[instruction.Accounts[19]]
|
|
||||||
default:
|
default:
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
|
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
|
||||||
}
|
}
|
||||||
@@ -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])
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for raydiumClmm add liquidity instruction, offset, %d, %d", offset[0], offset[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch discriminator {
|
||||||
|
case raydiumClmmIncreaseLiquidityDiscriminator, raydiumClmmIncreaseLiquidityV2Discriminator:
|
||||||
|
market = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||||
|
vault0 = instruction.Accounts[9]
|
||||||
|
vault1 = instruction.Accounts[10]
|
||||||
|
case raydiumClmmOpenPositionDiscriminator, raydiumClmmOpenPositionV2Discriminator:
|
||||||
|
market = tx.rawTx.accountList[instruction.Accounts[5]]
|
||||||
|
vault0 = instruction.Accounts[12]
|
||||||
|
vault1 = instruction.Accounts[13]
|
||||||
|
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||||
|
case raydiumClmmOpenPositionWithToken22NftDiscriminator:
|
||||||
|
market = tx.rawTx.accountList[instruction.Accounts[4]]
|
||||||
|
vault0 = instruction.Accounts[11]
|
||||||
|
vault1 = instruction.Accounts[12]
|
||||||
|
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||||
|
}
|
||||||
|
|
||||||
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
|
||||||
@@ -182,6 +190,8 @@ func raydiumClmmDecreaseLiquidityParser(tx *Tx, instruction Instruction, innerIn
|
|||||||
accountMin = 14
|
accountMin = 14
|
||||||
} else if discriminator == raydiumClmmDecreaseLiquidityV2Discriminator {
|
} else if discriminator == raydiumClmmDecreaseLiquidityV2Discriminator {
|
||||||
accountMin = 16
|
accountMin = 16
|
||||||
|
} else {
|
||||||
|
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
|
||||||
}
|
}
|
||||||
if len(instruction.Accounts) < accountMin {
|
if len(instruction.Accounts) < accountMin {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for decrease liquidity instruction")
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for decrease liquidity instruction")
|
||||||
@@ -278,25 +288,27 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
userTokenOutAccount int
|
userTokenOutAccount int
|
||||||
)
|
)
|
||||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
amountSpecified, otherAmountThreshold, swapMode, err := decodeRaydiumClmmSwapArgs(instruction.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, increaseOffset(offset), err
|
||||||
|
}
|
||||||
if discriminator == raydiumClmmSwapDiscriminator {
|
if discriminator == raydiumClmmSwapDiscriminator {
|
||||||
accountMin = 9
|
accountMin = 9
|
||||||
pool = tx.rawTx.accountList[instruction.Accounts[2]]
|
|
||||||
userTokenInAccount = instruction.Accounts[3]
|
|
||||||
userTokenOutAccount = instruction.Accounts[4]
|
|
||||||
tokenInVault = instruction.Accounts[5]
|
|
||||||
tokenOutVault = instruction.Accounts[6]
|
|
||||||
} else if discriminator == raydiumClmmSwapV2Discriminator {
|
} else if discriminator == raydiumClmmSwapV2Discriminator {
|
||||||
accountMin = 13
|
accountMin = 13
|
||||||
pool = tx.rawTx.accountList[instruction.Accounts[2]]
|
} else {
|
||||||
userTokenInAccount = instruction.Accounts[3]
|
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
|
||||||
userTokenOutAccount = instruction.Accounts[4]
|
|
||||||
tokenInVault = instruction.Accounts[5]
|
|
||||||
tokenOutVault = instruction.Accounts[6]
|
|
||||||
}
|
}
|
||||||
if len(instruction.Accounts) < accountMin {
|
if len(instruction.Accounts) < accountMin {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction")
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pool = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||||
|
userTokenInAccount = instruction.Accounts[3]
|
||||||
|
userTokenOutAccount = instruction.Accounts[4]
|
||||||
|
tokenInVault = instruction.Accounts[5]
|
||||||
|
tokenOutVault = instruction.Accounts[6]
|
||||||
|
|
||||||
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, tokenInVault)
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, tokenInVault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to get tokenIn vault balance after tx: %w", err)
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get tokenIn vault balance after tx: %w", err)
|
||||||
@@ -350,26 +362,26 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
|
|
||||||
offset[1] += 2
|
offset[1] += 2
|
||||||
|
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramRaydiumCLMM,
|
||||||
Program: SolProgramRaydiumCLMM,
|
Event: "sell",
|
||||||
Event: "sell",
|
Pool: pool,
|
||||||
Pool: pool,
|
BaseMint: baseMint,
|
||||||
BaseMint: baseMint,
|
QuoteMint: quoteMint,
|
||||||
QuoteMint: quoteMint,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
QuoteTokenProgram: quoteTokenProgram,
|
BaseMintDecimals: baseMintDecimals,
|
||||||
BaseMintDecimals: baseMintDecimals,
|
QuoteMintDecimals: quoteMintDecimals,
|
||||||
QuoteMintDecimals: quoteMintDecimals,
|
User: tx.rawTx.accountList[instruction.Accounts[0]],
|
||||||
User: tx.rawTx.accountList[instruction.Accounts[0]],
|
BaseAmount: baseAmount,
|
||||||
BaseAmount: baseAmount,
|
QuoteAmount: quoteAmount,
|
||||||
QuoteAmount: quoteAmount,
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
|
||||||
}, offset, nil
|
return []Swap{swap}, offset, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,20 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
agbinary "github.com/gagliardetto/binary"
|
||||||
"github.com/shopspring/decimal"
|
"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) {
|
func raydiumCPmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumCPmmProgramID) {
|
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])
|
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]
|
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]]
|
market := tx.rawTx.accountList[instruction.Accounts[3]]
|
||||||
// Get token accounts from instruction
|
// Get token accounts from instruction
|
||||||
tokenIn := tx.rawTx.accountList[instruction.Accounts[4]]
|
tokenIn := tx.rawTx.accountList[instruction.Accounts[4]]
|
||||||
@@ -384,25 +419,26 @@ func raydiumCPmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions")
|
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions")
|
||||||
}
|
}
|
||||||
offset[1] += 2
|
offset[1] += 2
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramRaydiumCPMM,
|
||||||
Program: SolProgramRaydiumCPMM,
|
Event: "sell",
|
||||||
Event: "sell",
|
Pool: market,
|
||||||
Pool: market,
|
BaseMint: inputTokenMint,
|
||||||
BaseMint: inputTokenMint,
|
QuoteMint: outputTokenMint,
|
||||||
QuoteMint: outputTokenMint,
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
||||||
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
||||||
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
||||||
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
||||||
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
BaseAmount: baseAmount,
|
||||||
BaseAmount: baseAmount,
|
QuoteAmount: quoteAmount,
|
||||||
QuoteAmount: quoteAmount,
|
User: user,
|
||||||
User: user,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||||
}, offset, nil
|
|
||||||
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
|
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
|
||||||
var programName string
|
var programName string
|
||||||
if platformConfig.Equals(bonkPlatformConfig) {
|
if platformConfig.Equals(bonkPlatformConfig) {
|
||||||
@@ -375,6 +383,26 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
|
|||||||
} else {
|
} else {
|
||||||
programName = SolProgramRaydiumLaunchLab
|
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]
|
var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
user := tx.rawTx.accountList[instruction.Accounts[0]]
|
user := tx.rawTx.accountList[instruction.Accounts[0]]
|
||||||
pool := tx.rawTx.accountList[instruction.Accounts[4]]
|
pool := tx.rawTx.accountList[instruction.Accounts[4]]
|
||||||
@@ -447,7 +475,7 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
|
|||||||
userBase := getAccountBalanceAfterTx(tx.rawTx, userBaseIdx)
|
userBase := getAccountBalanceAfterTx(tx.rawTx, userBaseIdx)
|
||||||
userQuote := getAccountBalanceAfterTx(tx.rawTx, userQuoteIdx)
|
userQuote := getAccountBalanceAfterTx(tx.rawTx, userQuoteIdx)
|
||||||
|
|
||||||
return []Swap{{
|
swap := Swap{
|
||||||
Program: programName,
|
Program: programName,
|
||||||
Event: event,
|
Event: event,
|
||||||
Pool: pool,
|
Pool: pool,
|
||||||
@@ -466,5 +494,8 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
|
|||||||
UserBaseBalance: userBase,
|
UserBaseBalance: userBase,
|
||||||
UserQuoteBalance: userQuote,
|
UserQuoteBalance: userQuote,
|
||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
}}, offset, nil
|
}
|
||||||
|
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||||
|
|
||||||
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
|
|||||||
116
raydiumv4.go
116
raydiumv4.go
@@ -1,11 +1,26 @@
|
|||||||
package pump_parser
|
package pump_parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
"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) {
|
func raydiumV4Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumV4Program) {
|
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])
|
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]
|
vaultQuoteIdx = instruction.Accounts[6]
|
||||||
}
|
}
|
||||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
amountSpecified, otherAmountThreshold, swapMode, err := decodeRaydiumV4SwapArgs(instruction.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, increaseOffset(offset), err
|
||||||
|
}
|
||||||
|
|
||||||
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
|
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
|
||||||
|
|
||||||
@@ -376,37 +395,44 @@ func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions Inne
|
|||||||
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
|
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
|
||||||
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
|
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
|
||||||
|
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramRaydiumV4,
|
||||||
Program: SolProgramRaydiumV4,
|
Event: event,
|
||||||
Event: event,
|
Pool: ammAccount,
|
||||||
Pool: ammAccount,
|
BaseMint: baseTokenbalance.MintAccount,
|
||||||
BaseMint: baseTokenbalance.MintAccount,
|
QuoteMint: quoteTokenbalance.MintAccount,
|
||||||
QuoteMint: quoteTokenbalance.MintAccount,
|
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
|
||||||
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
|
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
|
||||||
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
|
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
|
||||||
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
|
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
|
||||||
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
|
User: user,
|
||||||
User: user,
|
BaseAmount: baseAmount,
|
||||||
BaseAmount: baseAmount,
|
QuoteAmount: quoteAmount,
|
||||||
QuoteAmount: quoteAmount,
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
Mayhem: false,
|
||||||
Mayhem: false,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
|
||||||
}, offset, nil
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
accountsLen := len(instruction.Accounts)
|
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])
|
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]
|
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]]
|
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
|
||||||
user := tx.rawTx.accountList[instruction.Accounts[7]]
|
user := tx.rawTx.accountList[instruction.Accounts[7]]
|
||||||
userSourceTokenAccount := tx.rawTx.accountList[instruction.Accounts[5]]
|
userSourceTokenAccount := tx.rawTx.accountList[instruction.Accounts[5]]
|
||||||
@@ -472,26 +498,26 @@ func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
|
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
|
||||||
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
|
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
|
||||||
|
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramRaydiumV4,
|
||||||
Program: SolProgramRaydiumV4,
|
Event: event,
|
||||||
Event: event,
|
Pool: ammAccount,
|
||||||
Pool: ammAccount,
|
BaseMint: baseTokenbalance.MintAccount,
|
||||||
BaseMint: baseTokenbalance.MintAccount,
|
QuoteMint: quoteTokenbalance.MintAccount,
|
||||||
QuoteMint: quoteTokenbalance.MintAccount,
|
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
|
||||||
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
|
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
|
||||||
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
|
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
|
||||||
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
|
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
|
||||||
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
|
User: user,
|
||||||
User: user,
|
BaseAmount: baseAmount,
|
||||||
BaseAmount: baseAmount,
|
QuoteAmount: quoteAmount,
|
||||||
QuoteAmount: quoteAmount,
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
Mayhem: false,
|
||||||
Mayhem: false,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
|
||||||
}, offset, nil
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
|
|||||||
144
raydiumv4_test.go
Normal file
144
raydiumv4_test.go
Normal 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
230
swap_amounts.go
Normal 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
387
swap_amounts_oracle_test.go
Normal 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
206
swap_amounts_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,9 @@ func systemParser(tx *Tx, instruction Instruction, _ InnerInstructions, offset [
|
|||||||
}
|
}
|
||||||
|
|
||||||
decode := instruction.Data
|
decode := instruction.Data
|
||||||
|
if len(decode) < 4 {
|
||||||
|
return increaseOffset(offset), nil
|
||||||
|
}
|
||||||
discriminator := binary.LittleEndian.Uint32(decode[0:4])
|
discriminator := binary.LittleEndian.Uint32(decode[0:4])
|
||||||
|
|
||||||
switch discriminator {
|
switch discriminator {
|
||||||
@@ -29,6 +32,9 @@ func TransferParser(result *RawTx, instruction Instruction, offset [2]uint, tx *
|
|||||||
if len(decodeData) < 8 {
|
if len(decodeData) < 8 {
|
||||||
return increaseOffset(offset), nil
|
return increaseOffset(offset), nil
|
||||||
}
|
}
|
||||||
|
if len(instruction.Accounts) < 2 || len(result.Transaction.Message.Instructions[offset[0]].Accounts) < 1 {
|
||||||
|
return increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
var lamports uint64 = binary.LittleEndian.Uint64(decodeData)
|
var lamports uint64 = binary.LittleEndian.Uint64(decodeData)
|
||||||
|
|
||||||
from := result.accountList[result.Transaction.Message.Instructions[offset[0]].Accounts[0]]
|
from := result.accountList[result.Transaction.Message.Instructions[offset[0]].Accounts[0]]
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
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
35
tx.go
35
tx.go
@@ -31,6 +31,18 @@ type Swap struct {
|
|||||||
BaseAmount decimal.Decimal
|
BaseAmount decimal.Decimal
|
||||||
QuoteAmount 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
|
BaseReserve decimal.Decimal
|
||||||
QuoteReserve decimal.Decimal
|
QuoteReserve decimal.Decimal
|
||||||
Mayhem bool
|
Mayhem bool
|
||||||
@@ -48,23 +60,22 @@ type Swap struct {
|
|||||||
AfterSOLBalance decimal.Decimal
|
AfterSOLBalance decimal.Decimal
|
||||||
|
|
||||||
//For meteora dlmm
|
//For meteora dlmm
|
||||||
ActiveBinId int32
|
ActiveBinId int32
|
||||||
StartBinId int32
|
StartBinId int32
|
||||||
EndBinId int32
|
EndBinId int32
|
||||||
RemoveBp int32
|
RemoveBp int32
|
||||||
BinChanges []DlmmBinLiquidityChange
|
|
||||||
PositionAccount solana.PublicKey
|
PositionAccount solana.PublicKey
|
||||||
|
FeeAmount decimal.Decimal
|
||||||
|
FeeBps string
|
||||||
|
LpFeeAmount decimal.Decimal
|
||||||
|
FeeSide string
|
||||||
|
FeeMint solana.PublicKey
|
||||||
|
FeeTokenProgram solana.PublicKey
|
||||||
|
FeeMintDecimals uint8
|
||||||
|
|
||||||
ConsumeUnit uint64
|
ConsumeUnit uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
type DlmmBinLiquidityChange struct {
|
|
||||||
BinId int32
|
|
||||||
AmountX decimal.Decimal
|
|
||||||
AmountY decimal.Decimal
|
|
||||||
BpsToRemove uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
type platformInfo struct {
|
type platformInfo struct {
|
||||||
Platform string
|
Platform string
|
||||||
PlatformFee decimal.Decimal
|
PlatformFee decimal.Decimal
|
||||||
|
|||||||
2249
tx_binary.go
Normal file
2249
tx_binary.go
Normal file
File diff suppressed because it is too large
Load Diff
146
tx_binary_realdata_test.go
Normal file
146
tx_binary_realdata_test.go
Normal 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
1138
tx_binary_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user