Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc03798ff6 | ||
|
|
44eecac087 | ||
|
|
9f17ffce61 | ||
|
|
e4eaddec4e | ||
|
|
9454c3f6c7 | ||
|
|
39bfeb085f | ||
|
|
10885d5e08 | ||
|
|
2406f6d087 | ||
|
|
8b608889cb | ||
|
|
8d4aad1932 | ||
|
|
5cd3a97d81 | ||
|
|
0a4aabc67f | ||
|
|
d46e8b651c | ||
|
|
43659ea4e4 | ||
|
|
6414e6a25f | ||
|
|
273e87b8ad | ||
|
|
bb858c643e | ||
|
|
a620df5837 | ||
|
|
36da96eeaf | ||
|
|
a765fafddd | ||
|
|
738e417167 | ||
|
|
51f1511c8f | ||
|
|
7dfe003e5b | ||
|
|
fe94888b14 | ||
|
|
1dd843c393 |
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.
|
||||||
@@ -68,7 +68,7 @@ Interpretation:
|
|||||||
- Positive: execution is better than the user limit
|
- Positive: execution is better than the user limit
|
||||||
- Zero: execution lands exactly on 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`)
|
- `10000`: user limit is effectively unbounded on the constrained side (for example `min_out = 0`)
|
||||||
- Negative: this usually indicates an incorrect parser-side mapping or inconsistent source data
|
- 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:
|
This definition makes `SlippageBps` a bounded "remaining headroom to the user's limit" metric for successful swaps:
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ func main() {
|
|||||||
const rpcURL = "https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"
|
const rpcURL = "https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"
|
||||||
txHash := os.Getenv("TX_HASH")
|
txHash := os.Getenv("TX_HASH")
|
||||||
if txHash == "" {
|
if txHash == "" {
|
||||||
txHash = "2AhpL5KhVmG3D38CwMzrHuRyTucEQ43GzBXL2mo5WiugdZMVmK1dtX8brGe3sxvvFDY6iSSviJTvqCtr4UL3Pc7J"
|
txHash = "24wP3rk2ZfSVDB5YGyEQbhuy1jRXKZYkzXDRrDwwoKgzD6G4Kyyh4vmnir9ye98uLVKA5bBMj5Fq4cwgbDxp2Gie"
|
||||||
}
|
}
|
||||||
|
|
||||||
if txHash == "" {
|
if txHash == "" {
|
||||||
|
|||||||
21
codex.md
Normal file
21
codex.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Codex Notes
|
||||||
|
|
||||||
|
## Tx Binary enum synchronization
|
||||||
|
|
||||||
|
When adding or renaming transaction-facing enum values, keep the binary format definitions in sync.
|
||||||
|
|
||||||
|
This includes, but is not limited to:
|
||||||
|
|
||||||
|
- tx events
|
||||||
|
- programs
|
||||||
|
- platforms
|
||||||
|
- MEV agents
|
||||||
|
- swap modes, amount sides, limit types, and fee sides
|
||||||
|
|
||||||
|
Checklist:
|
||||||
|
|
||||||
|
1. Add the public constant in the normal source location, such as `enum.go`.
|
||||||
|
2. Add any address mapping in `consts.go` when the enum is account-derived, such as platform or MEV agent detection.
|
||||||
|
3. Append the new value to the matching versioned enum list in `tx_binary.go` under `txBinaryEnumTables`.
|
||||||
|
4. Do not reorder or insert into existing `tx_binary.go` enum lists unless the binary version is intentionally changed; append to preserve existing numeric IDs.
|
||||||
|
5. Add or update tx-binary round-trip coverage so encoding and decoding the new enum value is exercised.
|
||||||
35
consts.go
35
consts.go
@@ -116,6 +116,20 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("HZTmLyC683y74TW3HtGbNX5orxjm2sPuZBEYwwSgAM8v"): MevAgentBlocxRoute,
|
solana.MustPublicKeyFromBase58("HZTmLyC683y74TW3HtGbNX5orxjm2sPuZBEYwwSgAM8v"): MevAgentBlocxRoute,
|
||||||
solana.MustPublicKeyFromBase58("FogxVNs6Mm2w9rnGL1vkARSwJxvLE8mujTv3LK8RnUhF"): MevAgentBlocxRoute,
|
solana.MustPublicKeyFromBase58("FogxVNs6Mm2w9rnGL1vkARSwJxvLE8mujTv3LK8RnUhF"): MevAgentBlocxRoute,
|
||||||
solana.MustPublicKeyFromBase58("3UQUKjhMKaY2S6bjcQD6yHB7utcZt5bfarRCmctpRtUd"): MevAgentBlocxRoute,
|
solana.MustPublicKeyFromBase58("3UQUKjhMKaY2S6bjcQD6yHB7utcZt5bfarRCmctpRtUd"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLx7XBqSg3LUPVf1bRgCnkJmgVZR8QEgDJBPqcRLHvp"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLx8KeZxinPwy6kkUgyzMLeqb2ARNsWjADG1dhSsVba"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLxADBknoNj8WAGw2W6GBYeq848Xx6ajhaymV1YvrHm"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLxAc88vRBwvcUQJEgcxNfBLvHPikY4csNsUmPeWea2"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLxQ88oCiTsL8Xj4YWekKi1hjrgmbE3J3FFZ2xZHR3h"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLxS7NoLuynNRJ4mCnEE2YbtwJFttYsEyp2ME7rp2yt"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLxW6mCov7VEbrKc3S9tcBRcfSzRnLCbNp3Dfn3SJG5"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLxXSGXs4mYPTC5okZXed1qzvjNwNJ48QJ82hT2V7w7"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLxYi3vojbbB7hVzVDVTdBLVPhp7GJ3ZB3BwdK5sFXi"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLxhLPgBXtUpX4b1bH3HatuMGMSKT9GnwtuCGiMSAqe"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLxpY1mniuFW4PgkNA4JiNxoeKHFszryi6tNgyZAiAA"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLxuETxd2tgWxBALNwPzAfHhsik4BzD3nrEBCiPNZQD"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLxuL2gK5FW7xfahvwLrxLyW76vcCpNsKQY2CmnE6kV"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("bLxv4Hnub7nDJWHs8s17o9bGU65Bnx6Yqp2fqtMgHmm"): MevAgentBlocxRoute,
|
||||||
solana.MustPublicKeyFromBase58("TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq"): MevAgentNozomi,
|
solana.MustPublicKeyFromBase58("TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq"): MevAgentNozomi,
|
||||||
solana.MustPublicKeyFromBase58("noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4"): MevAgentNozomi,
|
solana.MustPublicKeyFromBase58("noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4"): MevAgentNozomi,
|
||||||
solana.MustPublicKeyFromBase58("noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE"): MevAgentNozomi,
|
solana.MustPublicKeyFromBase58("noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE"): MevAgentNozomi,
|
||||||
@@ -186,6 +200,12 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas,
|
solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas,
|
||||||
solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas,
|
solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas,
|
||||||
solana.MustPublicKeyFromBase58("soyasF3QPWPAKKmgA3GjfWax1kmTT1aoqSGxPzVLNUQ"): MevAgentSoyas,
|
solana.MustPublicKeyFromBase58("soyasF3QPWPAKKmgA3GjfWax1kmTT1aoqSGxPzVLNUQ"): MevAgentSoyas,
|
||||||
|
solana.MustPublicKeyFromBase58("soyasi59njacMUPvo3TM5paHjeK8pYSdovXgFi32gRt"): MevAgentSoyas,
|
||||||
|
solana.MustPublicKeyFromBase58("soyasQYhJxv8uZgWDxhg72td6piAf7XTkoyWHtSATEz"): MevAgentSoyas,
|
||||||
|
solana.MustPublicKeyFromBase58("soyastP66xyYC8XADXZjdMM5BAVGD2YRvz8dwtLsqb8"): MevAgentSoyas,
|
||||||
|
solana.MustPublicKeyFromBase58("soyasvdgUJWYcUCzDxpmjUnNjH7KamXLXTzLwFvdVPE"): MevAgentSoyas,
|
||||||
|
solana.MustPublicKeyFromBase58("soyasvxAunisNxaoRxkKGjNir7KmbwYnr37JmefkX9G"): MevAgentSoyas,
|
||||||
|
solana.MustPublicKeyFromBase58("soyas5doVFUwH8s5zK8gEvCL5KR5ogDmf52LsrJEZ9h"): MevAgentSoyas,
|
||||||
solana.MustPublicKeyFromBase58("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium,
|
solana.MustPublicKeyFromBase58("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium,
|
||||||
solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium,
|
solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium,
|
||||||
solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium,
|
solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium,
|
||||||
@@ -200,6 +220,8 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("b1ooMDLjzz4QqecNsJ8bBXzJTzfAPDCP3CxijTS2K93"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("b1ooMDLjzz4QqecNsJ8bBXzJTzfAPDCP3CxijTS2K93"): MevAgentAstralane,
|
||||||
@@ -335,7 +357,10 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("bgDETv6tnt9mwYqAKebLXY5B5o6akiKJmAdU7Gd9G7H"): MevAgentNozomi,
|
solana.MustPublicKeyFromBase58("bgDETv6tnt9mwYqAKebLXY5B5o6akiKJmAdU7Gd9G7H"): MevAgentNozomi,
|
||||||
solana.MustPublicKeyFromBase58("bgH7YhymSykyvMa3nAZpzvrn73owJHU5iB75S1aiLT9"): MevAgentNozomi,
|
solana.MustPublicKeyFromBase58("bgH7YhymSykyvMa3nAZpzvrn73owJHU5iB75S1aiLT9"): MevAgentNozomi,
|
||||||
solana.MustPublicKeyFromBase58("pfngGVVQLiVRFbLWw3Ektiv17ef9NiRZbcgdAhh4ZEW"): MevAgentNozomi,
|
solana.MustPublicKeyFromBase58("pfngGVVQLiVRFbLWw3Ektiv17ef9NiRZbcgdAhh4ZEW"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("mwGELGMgGGrNL1UibNCQeJHDE7qdPptWRYB6noUHmTj"): MevAgentNozomi,
|
||||||
solana.MustPublicKeyFromBase58("nEFs3jph8HJt7honu3k7XtGUufMnwAvSXmXcKSPxryP"): MevAgentNozomi,
|
solana.MustPublicKeyFromBase58("nEFs3jph8HJt7honu3k7XtGUufMnwAvSXmXcKSPxryP"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("nzUMeyyucuHSCZLw1aaX14d1si2mffT4PjVNdZcybot"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("n6Tc3GoTheA1pYF4MPV57KGmn3DHJCCuk3wHZmcUyp"): MevAgentNozomi,
|
||||||
solana.MustPublicKeyFromBase58("Fa1con11xLjPddfzRwRUB16sbFZggp2JeJkCeWREyR8X"): MevagentFa1con,
|
solana.MustPublicKeyFromBase58("Fa1con11xLjPddfzRwRUB16sbFZggp2JeJkCeWREyR8X"): MevagentFa1con,
|
||||||
solana.MustPublicKeyFromBase58("Fa1con11TM1RuAQzbQzYjTy4Ekfap9Lnc9fnEbQYEd6Q"): MevagentFa1con,
|
solana.MustPublicKeyFromBase58("Fa1con11TM1RuAQzbQzYjTy4Ekfap9Lnc9fnEbQYEd6Q"): MevagentFa1con,
|
||||||
solana.MustPublicKeyFromBase58("Fa1con113Bvi76nS5AzUiRDC2fqjfzkNMUNRLgQybMYt"): MevagentFa1con,
|
solana.MustPublicKeyFromBase58("Fa1con113Bvi76nS5AzUiRDC2fqjfzkNMUNRLgQybMYt"): MevagentFa1con,
|
||||||
@@ -381,6 +406,16 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("t7qUQU35sLpPydh42BcPmtEfTWW8gBe4Ry3gjwVnokJ"): MevAgentRaiden,
|
solana.MustPublicKeyFromBase58("t7qUQU35sLpPydh42BcPmtEfTWW8gBe4Ry3gjwVnokJ"): MevAgentRaiden,
|
||||||
solana.MustPublicKeyFromBase58("t8pPgarSK3TnuLbbHmoE1RCQdLxfxuPqNTyFjBKahok"): MevAgentRaiden,
|
solana.MustPublicKeyFromBase58("t8pPgarSK3TnuLbbHmoE1RCQdLxfxuPqNTyFjBKahok"): MevAgentRaiden,
|
||||||
solana.MustPublicKeyFromBase58("t96GGdw3MiaGR993XN8PSsRpKGXx56t5Wf6zcF1hBpY"): MevAgentRaiden,
|
solana.MustPublicKeyFromBase58("t96GGdw3MiaGR993XN8PSsRpKGXx56t5Wf6zcF1hBpY"): MevAgentRaiden,
|
||||||
|
solana.MustPublicKeyFromBase58("7HkiWXe5deJvzn4D6kgMUFCADwX9Z4DMrdjNSSxN6bPp"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zanrUknLZXzT9JPj968A7RfgCjp77Lx1W1xKRAtfshb"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zanHbk2UsiT3jKsKjD7UuEqS5Vgpmcd4pG9HycAAV8g"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zanNazKCXNRoKnPS9BBbFTELTpNwUDJxeKEb1JtZJer"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zan3gbFhXCjGLHhRe2vaXRDta5fCrYiYr3Dq4RLvpfU"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zan6WoE3DX5aK7FMQT1vSGsGrgZG1ngns3oCsFMnBHU"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zan8Nb9fB4zMDsuTRP9R65QZbc9v2Cjn5a4Hjwnj8D3"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zanJgoR7ALJAJ6ohoKs6aS9T71D9ZkNN9gYM5xUsi3u"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("zanAtYifQP7Bo6kStB97mJvzqSDW1toKNibWibwcKDd"): MevAgentZan,
|
||||||
|
solana.MustPublicKeyFromBase58("GWT5UjDheZzoqinLavJkYvSRH5sakW8vDRdAgrUS5ZcS"): MevAgentTunneling,
|
||||||
}
|
}
|
||||||
|
|
||||||
var entryContractAddresses = map[solana.PublicKey]string{
|
var entryContractAddresses = map[solana.PublicKey]string{
|
||||||
|
|||||||
15
enum.go
15
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 (
|
||||||
@@ -126,4 +128,17 @@ const (
|
|||||||
TxEventBuyFailed = "buy_failed"
|
TxEventBuyFailed = "buy_failed"
|
||||||
TxEventSellFailed = "sell_failed"
|
TxEventSellFailed = "sell_failed"
|
||||||
TxEventBurn = "burn"
|
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})
|
||||||
//}
|
|
||||||
md := metadata.New(map[string]string{"x-token": "5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"})
|
|
||||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
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 {
|
if err != nil {
|
||||||
return err
|
fmt.Println("EncodeTxsBinary err", 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 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
bd.Pairs[pair.Address] = *pair
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SwapGetter struct {
|
|
||||||
solana_parser.Swap
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
PositionChangeNone = int64(iota)
|
|
||||||
PositionChangeNewBuy
|
|
||||||
PositionChangeBuyMore
|
|
||||||
PositionChangeSellPart
|
|
||||||
PositionChangeSellAll
|
|
||||||
)
|
|
||||||
|
|
||||||
func (spg SwapGetter) GetLiquidityTx(tx *solana_parser.Tx, index uint64) (*Tx, error) {
|
|
||||||
if spg.BaseMint != solana.WrappedSol && spg.QuoteMint != solana.WrappedSol {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
token0 string
|
|
||||||
amount0 decimal.Decimal
|
|
||||||
amount1 decimal.Decimal
|
|
||||||
pool0 decimal.Decimal
|
|
||||||
pool1 decimal.Decimal
|
|
||||||
|
|
||||||
event string
|
|
||||||
)
|
|
||||||
|
|
||||||
if spg.BaseMint == solana.WrappedSol {
|
|
||||||
amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
|
||||||
amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
|
||||||
token0 = spg.QuoteMint.String()
|
|
||||||
pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
|
||||||
pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
|
||||||
} else {
|
|
||||||
amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
|
||||||
amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
|
||||||
token0 = spg.BaseMint.String()
|
|
||||||
pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
|
||||||
pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
|
||||||
}
|
|
||||||
if spg.Event == "deposit" || spg.Event == "add" || spg.Event == "add_liquidity" || spg.Event == "add_liquidity_one_side" {
|
|
||||||
event = "add"
|
|
||||||
} else if spg.Event == "withdraw" || spg.Event == "remove" || spg.Event == "remove_liquidity" || spg.Event == "remove_liquidity_one_side" {
|
|
||||||
event = "remove"
|
|
||||||
}
|
|
||||||
if event == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mevName, mevFee := tx.CheckMevAgent()
|
|
||||||
platformName, platformFee := tx.CheckPlatform(spg.Swap)
|
|
||||||
|
|
||||||
pairString := ""
|
|
||||||
if spg.Program == solana_parser.SolProgramPump {
|
|
||||||
pairString = spg.BaseMint.String()
|
|
||||||
} else {
|
|
||||||
pairString = spg.Pool.String()
|
|
||||||
}
|
|
||||||
t := pgtype.Timestamptz{}
|
|
||||||
_ = t.Set(time.Unix(tx.BlockAt, 0))
|
|
||||||
return &Tx{
|
|
||||||
PairAddress: pairString,
|
|
||||||
Maker: spg.User.String(),
|
|
||||||
Token0Address: token0,
|
|
||||||
Token1Address: "So11111111111111111111111111111111111111112",
|
|
||||||
Token0Amount: amount0,
|
|
||||||
Token1Amount: amount1,
|
|
||||||
Block: tx.Block,
|
|
||||||
BlockIndex: tx.BlockIndex,
|
|
||||||
Event: event,
|
|
||||||
TxHash: tx.GetTxHash(),
|
|
||||||
TxIndex: index,
|
|
||||||
BlockAt: t,
|
|
||||||
Program: spg.Program,
|
|
||||||
AfterReserve0: pool0.String(),
|
|
||||||
AfterReserve1: pool1.String(),
|
|
||||||
Platform: platformName,
|
|
||||||
PlatformFee: platformFee,
|
|
||||||
CUPrice: tx.CUPrice,
|
|
||||||
MevAgent: mevName,
|
|
||||||
MevAgentFee: mevFee,
|
|
||||||
AfterSOLBalance: spg.AfterSOLBalance,
|
|
||||||
EntryContract: spg.CheckEntryContract(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (spg SwapGetter) GetTx(tx *solana_parser.Tx, index uint64, price decimal.Decimal) Tx {
|
|
||||||
var (
|
|
||||||
token0 string
|
|
||||||
amount0 decimal.Decimal
|
|
||||||
amount1 decimal.Decimal
|
|
||||||
pool0 decimal.Decimal
|
|
||||||
pool1 decimal.Decimal
|
|
||||||
|
|
||||||
event string
|
|
||||||
)
|
|
||||||
|
|
||||||
if spg.BaseMint == solana.WrappedSol {
|
|
||||||
amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
|
||||||
amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
|
||||||
token0 = spg.QuoteMint.String()
|
|
||||||
pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
|
||||||
pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
|
||||||
if spg.Event == "buy" {
|
|
||||||
event = "sell"
|
|
||||||
} else if spg.Event == "sell" {
|
|
||||||
event = "buy"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
|
||||||
amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
|
||||||
token0 = spg.BaseMint.String()
|
|
||||||
pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
|
||||||
pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
|
||||||
event = spg.Event
|
|
||||||
}
|
|
||||||
|
|
||||||
priceUsd := decimal.Zero
|
|
||||||
if amount0.GreaterThan(priceUsd) {
|
|
||||||
priceUsd = amount1.Div(amount0).Mul(price)
|
|
||||||
}
|
|
||||||
pc := PositionChangeNone
|
|
||||||
if event == "buy" {
|
|
||||||
pc = PositionChangeNewBuy
|
|
||||||
if spg.BaseMint == solana.WrappedSol {
|
|
||||||
if spg.UserQuoteBalance.GreaterThan(spg.QuoteAmount) {
|
|
||||||
pc = PositionChangeBuyMore
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if spg.UserBaseBalance.GreaterThan(spg.BaseAmount) {
|
|
||||||
pc = PositionChangeBuyMore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if event == "sell" {
|
|
||||||
pc = PositionChangeSellPart
|
|
||||||
if spg.BaseMint == solana.WrappedSol {
|
|
||||||
if spg.UserQuoteBalance.Div(decimal.New(1, int32(spg.QuoteMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) {
|
|
||||||
pc = PositionChangeSellAll
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if spg.UserBaseBalance.Div(decimal.New(1, int32(spg.BaseMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) {
|
|
||||||
pc = PositionChangeSellAll
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mevName, mevFee := tx.CheckMevAgent()
|
|
||||||
platformName, platformFee := tx.CheckPlatform(spg.Swap)
|
|
||||||
|
|
||||||
if mevName == "" {
|
|
||||||
mevName = "none"
|
|
||||||
}
|
|
||||||
if mevName == "unknown" {
|
|
||||||
mevName = "none"
|
|
||||||
mevFee = decimal.Zero
|
|
||||||
}
|
|
||||||
pairString := ""
|
|
||||||
if spg.Program == solana_parser.SolProgramPump {
|
|
||||||
pairString = spg.BaseMint.String()
|
|
||||||
} else {
|
|
||||||
pairString = spg.Pool.String()
|
|
||||||
}
|
|
||||||
t := pgtype.Timestamptz{}
|
|
||||||
_ = t.Set(time.Unix(tx.BlockAt, 0))
|
|
||||||
|
|
||||||
return Tx{
|
|
||||||
PairAddress: pairString,
|
|
||||||
Maker: spg.User.String(),
|
|
||||||
Token0Address: token0,
|
|
||||||
Token1Address: "So11111111111111111111111111111111111111112",
|
|
||||||
Token0Amount: amount0,
|
|
||||||
Token1Amount: amount1,
|
|
||||||
PriceUsd: priceUsd,
|
|
||||||
AmountUsd: amount1.Mul(price),
|
|
||||||
Block: tx.Block,
|
|
||||||
BlockIndex: tx.BlockIndex,
|
|
||||||
Event: event,
|
|
||||||
TxHash: tx.GetTxHash(),
|
|
||||||
TxIndex: index,
|
|
||||||
BlockAt: t,
|
|
||||||
Program: spg.Program,
|
|
||||||
AfterReserve0: pool0.String(),
|
|
||||||
AfterReserve1: pool1.String(),
|
|
||||||
PositionChange: pc,
|
|
||||||
Platform: platformName,
|
|
||||||
PlatformFee: platformFee,
|
|
||||||
CUPrice: tx.CUPrice,
|
|
||||||
MevAgent: mevName,
|
|
||||||
MevAgentFee: mevFee,
|
|
||||||
AfterSOLBalance: spg.AfterSOLBalance,
|
|
||||||
EntryContract: spg.CheckEntryContract(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (spg SwapGetter) GetPair(slot uint64, _ string) (*Pair, error) {
|
|
||||||
//pump amm
|
|
||||||
if spg.Program == solana_parser.SolProgramPump {
|
|
||||||
tokenMint := spg.BaseMint.String()
|
|
||||||
return &Pair{
|
|
||||||
Address: tokenMint,
|
|
||||||
Token0: tokenMint,
|
|
||||||
Token1: "So11111111111111111111111111111111111111112",
|
|
||||||
ChainId: 900,
|
|
||||||
Reserve0: spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))),
|
|
||||||
Reserve1: spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))),
|
|
||||||
IsCreate: spg.Event == "create",
|
|
||||||
Program: spg.Program,
|
|
||||||
UpdateSlot: slot,
|
|
||||||
}, nil
|
|
||||||
} else {
|
|
||||||
var (
|
|
||||||
token0 string
|
|
||||||
amount0 decimal.Decimal
|
|
||||||
amount1 decimal.Decimal
|
|
||||||
)
|
|
||||||
if spg.BaseMint.IsZero() || spg.QuoteMint.IsZero() {
|
|
||||||
return nil, errors.New("base mint or quote mint is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if spg.BaseMint == solana.WrappedSol {
|
|
||||||
amount0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
|
||||||
amount1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
|
||||||
//decimal0 = spg.QuoteMintDecimals
|
|
||||||
token0 = spg.QuoteMint.String()
|
|
||||||
} else {
|
|
||||||
amount0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
|
||||||
amount1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
|
||||||
//decimal0 = a.BaseDecimals
|
|
||||||
token0 = spg.BaseMint.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Pair{
|
|
||||||
Address: spg.Pool.String(),
|
|
||||||
LpToken: spg.LpMint.String(),
|
|
||||||
Token0: token0,
|
|
||||||
Token1: "So11111111111111111111111111111111111111112",
|
|
||||||
ChainId: 900,
|
|
||||||
Reserve0: amount0,
|
|
||||||
Reserve1: amount1,
|
|
||||||
IsCreate: false,
|
|
||||||
Program: spg.Program,
|
|
||||||
UpdateSlot: slot,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBlockTxsFromDb(db *gorm.DB, block uint64) ([]Tx, error) {
|
|
||||||
var txs []Tx
|
|
||||||
result := db.Table("tx").Where("block = ?", block).Find(&txs)
|
|
||||||
return txs, result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBlockActionsFromDb(db *gorm.DB, block uint64) ([]Action, error) {
|
|
||||||
var txs []Action
|
|
||||||
result := db.Table("action").Where("block = ?", block).Find(&txs)
|
|
||||||
return txs, result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
type dbLog struct {
|
|
||||||
logger *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *dbLog) Printf(format string, args ...interface{}) {
|
|
||||||
l.logger.Info(fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDbLog() *dbLog {
|
|
||||||
return &dbLog{logger: slog.Default()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGorm(dsn string) *gorm.DB {
|
|
||||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
|
||||||
Logger: logger.New(newDbLog(), logger.Config{
|
|
||||||
Colorful: false,
|
|
||||||
LogLevel: logger.Warn,
|
|
||||||
SlowThreshold: time.Second * 10,
|
|
||||||
IgnoreRecordNotFoundError: true,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
func compareTxs(dbTxs []Tx, dataTxs []Tx) (diff int, missing int) {
|
|
||||||
dataByHash := make(map[string][]Tx, len(dataTxs))
|
|
||||||
for _, tx := range dataTxs {
|
|
||||||
dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)] = append(dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)], tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dbTx := range dbTxs {
|
|
||||||
candidates := dataByHash[fmt.Sprintf("%s-%d", dbTx.TxHash, dbTx.TxIndex)]
|
|
||||||
if len(candidates) == 0 {
|
|
||||||
missing++
|
|
||||||
log.Printf("missing tx: %s", txCompareString(dbTx))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
matched := false
|
|
||||||
for _, dataTx := range candidates {
|
|
||||||
if txEqualWithoutHash(dbTx, dataTx) {
|
|
||||||
matched = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !matched {
|
|
||||||
diff++
|
|
||||||
log.Printf("tx diff hash=%s, program=%s, event:%s: %s, ", dbTx.TxHash, dbTx.Program, dbTx.Event, txCompareDiffString(dbTx, candidates[0]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Printf("compare txs done: db=%d parsed=%d missing=%d diff=%d", len(dbTxs), len(dataTxs), missing, diff)
|
|
||||||
return diff, missing
|
|
||||||
}
|
|
||||||
|
|
||||||
func withinOnePercentDecimal(a decimal.Decimal, b decimal.Decimal) bool {
|
|
||||||
if a.IsZero() {
|
|
||||||
return b.IsZero()
|
|
||||||
}
|
|
||||||
diff := a.Sub(b).Abs()
|
|
||||||
threshold := a.Abs().Mul(decimal.NewFromFloat(0.03))
|
|
||||||
return diff.LessThanOrEqual(threshold)
|
|
||||||
}
|
|
||||||
|
|
||||||
func withinOnePercentStringDecimal(a string, b string) bool {
|
|
||||||
ad, errA := decimal.NewFromString(a)
|
|
||||||
bd, errB := decimal.NewFromString(b)
|
|
||||||
if errA != nil || errB != nil {
|
|
||||||
return a == b
|
|
||||||
}
|
|
||||||
return withinOnePercentDecimal(ad, bd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func txEqualWithoutHash(a Tx, b Tx) bool {
|
|
||||||
//mevMatch := a.MevAgent == b.MevAgent || (a.MevAgent == "none" && b.MevAgent == "unknown") || (a.MevAgent == "unknown" && b.MevAgent == "none")
|
|
||||||
//mevNone := a.MevAgent == "none" || a.MevAgent == "unknown"
|
|
||||||
|
|
||||||
return a.PairAddress == b.PairAddress &&
|
|
||||||
a.Token1Address == b.Token1Address &&
|
|
||||||
(a.Token0Address == "" || a.Token0Address == b.Token0Address) &&
|
|
||||||
//a.Maker == b.Maker &&
|
|
||||||
(a.Token0Address == "" || withinOnePercentDecimal(a.Token0Amount, b.Token0Amount)) &&
|
|
||||||
withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) &&
|
|
||||||
a.Block == b.Block &&
|
|
||||||
a.BlockIndex == b.BlockIndex &&
|
|
||||||
a.Event == b.Event &&
|
|
||||||
a.TxIndex == b.TxIndex &&
|
|
||||||
a.Program == b.Program &&
|
|
||||||
(a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0)) &&
|
|
||||||
(a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1)) &&
|
|
||||||
// a.PositionChange == b.PositionChange &&
|
|
||||||
(a.Platform == b.Platform || (a.Platform == "photon" && b.Platform == "fake") || (a.Platform == "trojan" && b.Platform == "fake")) &&
|
|
||||||
a.CUPrice.String() == b.CUPrice.String() // &&
|
|
||||||
//mevMatch &&
|
|
||||||
//(mevNone || a.MevAgentFee.String() == b.MevAgentFee.String()) &&
|
|
||||||
//(a.Program == solana_parser.SolProgramRaydiumV4 || a.AfterSOLBalance.String() == b.AfterSOLBalance.String())
|
|
||||||
//&&
|
|
||||||
// a.EntryContract == b.EntryContract
|
|
||||||
}
|
|
||||||
|
|
||||||
func txCompareDiffString(a Tx, b Tx) string {
|
|
||||||
var diffs []string
|
|
||||||
if a.PairAddress != b.PairAddress {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("PairAddress db=%s data=%s", a.PairAddress, b.PairAddress))
|
|
||||||
}
|
|
||||||
//if a.Maker != b.Maker {
|
|
||||||
// diffs = append(diffs, fmt.Sprintf("Maker db=%s, data=%s", a.Maker, b.Maker))
|
|
||||||
//}
|
|
||||||
if a.Token1Address != b.Token1Address {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("Token1Address db=%s data=%s", a.Token1Address, b.Token1Address))
|
|
||||||
}
|
|
||||||
if a.Token0Address != b.Token0Address {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("Token0Address db=%s data=%s", a.Token0Address, b.Token0Address))
|
|
||||||
}
|
|
||||||
if !withinOnePercentDecimal(a.Token0Amount, b.Token0Amount) {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("Token0Amount db=%s data=%s", a.Token0Amount.String(), b.Token0Amount.String()))
|
|
||||||
}
|
|
||||||
if !withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("Token1Amount db=%s data=%s", a.Token1Amount.String(), b.Token1Amount.String()))
|
|
||||||
}
|
|
||||||
if a.Block != b.Block {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block))
|
|
||||||
}
|
|
||||||
if a.BlockIndex != b.BlockIndex {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("BlockIndex db=%d data=%d", a.BlockIndex, b.BlockIndex))
|
|
||||||
}
|
|
||||||
if a.Event != b.Event {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("Event db=%s data=%s", a.Event, b.Event))
|
|
||||||
}
|
|
||||||
if a.TxIndex != b.TxIndex {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("TxIndex db=%d data=%d", a.TxIndex, b.TxIndex))
|
|
||||||
}
|
|
||||||
if a.Program != b.Program {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("Program db=%s data=%s", a.Program, b.Program))
|
|
||||||
}
|
|
||||||
if !withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0) {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("AfterReserve0 db=%s data=%s", a.AfterReserve0, b.AfterReserve0))
|
|
||||||
}
|
|
||||||
if !withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1) {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("AfterReserve1 db=%s data=%s", a.AfterReserve1, b.AfterReserve1))
|
|
||||||
}
|
|
||||||
//if a.PositionChange != b.PositionChange {
|
|
||||||
// diffs = append(diffs, fmt.Sprintf("PositionChange db=%d data=%d", a.PositionChange, b.PositionChange))
|
|
||||||
//}
|
|
||||||
if a.Platform != b.Platform {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("Platform db=%s data=%s", a.Platform, b.Platform))
|
|
||||||
}
|
|
||||||
if a.CUPrice.String() != b.CUPrice.String() {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("CUPrice db=%s data=%s", a.CUPrice.String(), b.CUPrice.String()))
|
|
||||||
}
|
|
||||||
//if a.MevAgent != b.MevAgent {
|
|
||||||
// diffs = append(diffs, fmt.Sprintf("MevAgent db=%s data=%s", a.MevAgent, b.MevAgent))
|
|
||||||
//}
|
|
||||||
//if a.MevAgentFee.String() != b.MevAgentFee.String() {
|
|
||||||
// diffs = append(diffs, fmt.Sprintf("MevAgentFee db=%s data=%s", a.MevAgentFee.String(), b.MevAgentFee.String()))
|
|
||||||
//}
|
|
||||||
//if a.AfterSOLBalance.String() != b.AfterSOLBalance.String() {
|
|
||||||
// diffs = append(diffs, fmt.Sprintf("AfterSOLBalance db=%s data=%s", a.AfterSOLBalance.String(), b.AfterSOLBalance.String()))
|
|
||||||
//}
|
|
||||||
//if a.EntryContract != b.EntryContract {
|
|
||||||
// diffs = append(diffs, fmt.Sprintf("EntryContract db=%s data=%s", a.EntryContract, b.EntryContract))
|
|
||||||
//}
|
|
||||||
return strings.Join(diffs, "; ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func compareActions(dbActions []Action, dataActions []Action) (diff, missing int) {
|
|
||||||
dataByHash := make(map[string][]Action, len(dataActions))
|
|
||||||
for _, action := range dataActions {
|
|
||||||
dataByHash[action.TxHash] = append(dataByHash[action.TxHash], action)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dbAction := range dbActions {
|
|
||||||
candidates := dataByHash[dbAction.TxHash]
|
|
||||||
if len(candidates) == 0 {
|
|
||||||
missing++
|
|
||||||
log.Printf("missing action: %s", actionCompareString(dbAction))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
matched := false
|
|
||||||
for _, dataAction := range candidates {
|
|
||||||
if actionEqualWithoutHash(dbAction, dataAction) {
|
|
||||||
matched = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !matched {
|
|
||||||
diff++
|
|
||||||
log.Printf("action diff hash=%s: %s", dbAction.TxHash, actionCompareDiffString(dbAction, candidates[0]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Printf("compare actions done: db=%d parsed=%d missing=%d diff=%d", len(dbActions), len(dataActions), missing, diff)
|
|
||||||
return diff, missing
|
|
||||||
}
|
|
||||||
|
|
||||||
func actionEqualWithoutHash(a Action, b Action) bool {
|
|
||||||
return a.Maker == b.Maker &&
|
|
||||||
a.Token == b.Token &&
|
|
||||||
a.Pair == b.Pair &&
|
|
||||||
a.Action == b.Action &&
|
|
||||||
a.Block == b.Block
|
|
||||||
}
|
|
||||||
|
|
||||||
func actionCompareDiffString(a Action, b Action) string {
|
|
||||||
var diffs []string
|
|
||||||
if a.Maker != b.Maker {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("Maker db=%s data=%s", a.Maker, b.Maker))
|
|
||||||
}
|
|
||||||
if a.Token != b.Token {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("Token db=%s data=%s", a.Token, b.Token))
|
|
||||||
}
|
|
||||||
if a.Pair != b.Pair {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("Pair db=%s data=%s", a.Pair, b.Pair))
|
|
||||||
}
|
|
||||||
if a.Action != b.Action {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("Action db=%s data=%s", a.Action, b.Action))
|
|
||||||
}
|
|
||||||
if a.Block != b.Block {
|
|
||||||
diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block))
|
|
||||||
}
|
|
||||||
return strings.Join(diffs, "; ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func actionCompareString(action Action) string {
|
|
||||||
return fmt.Sprintf("Maker=%s Token=%s Pair=%s Action=%s Block=%d TxHash=%s", action.Maker, action.Token, action.Pair, action.Action, action.Block, action.TxHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func txCompareString(tx Tx) string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"tx.Program=%s TxHash=%s PairAddress=%s Token1Address=%s Token0Amount=%s Token1Amount=%s Block=%d BlockIndex=%d Event=%s TxIndex=%d AfterReserve0=%s AfterReserve1=%s PositionChange=%d Platform=%s CUPrice=%s MevAgent=%s MevAgentFee=%s AfterSOLBalance=%s EntryContract=%s",
|
|
||||||
tx.Program,
|
|
||||||
tx.TxHash,
|
|
||||||
tx.PairAddress,
|
|
||||||
tx.Token1Address,
|
|
||||||
tx.Token0Amount.String(),
|
|
||||||
tx.Token1Amount.String(),
|
|
||||||
tx.Block,
|
|
||||||
tx.BlockIndex,
|
|
||||||
tx.Event,
|
|
||||||
tx.TxIndex,
|
|
||||||
tx.AfterReserve0,
|
|
||||||
tx.AfterReserve1,
|
|
||||||
tx.PositionChange,
|
|
||||||
tx.Platform,
|
|
||||||
tx.CUPrice.String(),
|
|
||||||
tx.MevAgent,
|
|
||||||
tx.MevAgentFee.String(),
|
|
||||||
tx.AfterSOLBalance.String(),
|
|
||||||
tx.EntryContract,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
7
meta.go
7
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}
|
||||||
@@ -132,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 (
|
||||||
|
|||||||
@@ -2006,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
|
||||||
|
|||||||
@@ -222,6 +222,50 @@ func TestDlmmDecodeLbPairCreateEvent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestMeteoraDlmmInitializeParserUsesLbPairCreateEvent(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
202
metaorapool.go
202
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"
|
||||||
@@ -20,6 +22,14 @@ type metaoraPoolSwapArgs struct {
|
|||||||
MinimumOutAmount 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}
|
||||||
@@ -731,6 +741,10 @@ 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
|
var args metaoraPoolSwapArgs
|
||||||
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {
|
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to decode meteora pools swap args: %w", err)
|
return nil, increaseOffset(offset), fmt.Errorf("failed to decode meteora pools swap args: %w", err)
|
||||||
@@ -855,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")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -883,6 +900,9 @@ 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(
|
swaps[0].SetSwapAmountInfo(
|
||||||
SwapModeExactIn,
|
SwapModeExactIn,
|
||||||
decimal.NewFromUint64(args.InAmount),
|
decimal.NewFromUint64(args.InAmount),
|
||||||
@@ -890,3 +910,185 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
)
|
)
|
||||||
return swaps, offset, nil
|
return swaps, offset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actualLimitAmount := swapAmountForSide(baseAmount, quoteAmount, limitSide)
|
||||||
|
if swapEvent, ok := metaoraPoolSwapEventFromInstruction(instruction); ok {
|
||||||
|
actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount)
|
||||||
|
} else if swapEvent, ok := metaoraPoolSwapEventForOffset(tx, swapOffset); ok {
|
||||||
|
actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount)
|
||||||
|
}
|
||||||
|
swaps[0].SetSwapAmountInfoDetailed(
|
||||||
|
SwapModeExactIn,
|
||||||
|
decimal.NewFromUint64(args.InAmount),
|
||||||
|
fixedSide,
|
||||||
|
swapMintForSide(baseMint, quoteMint, fixedSide),
|
||||||
|
SwapLimitTypeMinOut,
|
||||||
|
decimal.NewFromUint64(args.MinimumOutAmount),
|
||||||
|
limitSide,
|
||||||
|
swapMintForSide(baseMint, quoteMint, limitSide),
|
||||||
|
actualLimitAmount,
|
||||||
|
)
|
||||||
|
return swaps, offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolSwapEventFromInstruction(instruction Instruction) (metaoraPoolSwapEvent, bool) {
|
||||||
|
for _, event := range instruction.LogEvents {
|
||||||
|
if swapEvent, ok := metaoraPoolDecodeSwapEventData(event); ok {
|
||||||
|
return swapEvent, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolSwapEventForOffset(tx *Tx, offset [2]uint) (metaoraPoolSwapEvent, bool) {
|
||||||
|
if tx == nil || tx.rawTx == nil {
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
occurrence := metaoraPoolSwapInstructionOccurrence(tx.rawTx, offset)
|
||||||
|
if occurrence == 0 {
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
return metaoraPoolSwapEventFromLogs(tx.rawTx.Meta.LogMessages, occurrence)
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolSwapInstructionOccurrence(rawTx *RawTx, offset [2]uint) int {
|
||||||
|
if rawTx == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
accountList := rawTx.getAccountList()
|
||||||
|
innerByOuter := make(map[int]InnerInstructions, len(rawTx.Meta.InnerInstructions))
|
||||||
|
for _, inner := range rawTx.Meta.InnerInstructions {
|
||||||
|
innerByOuter[inner.Index] = inner
|
||||||
|
}
|
||||||
|
|
||||||
|
occurrence := 0
|
||||||
|
for i, instruction := range rawTx.Transaction.Message.Instructions {
|
||||||
|
if uint(i) == offset[0] && offset[1] == 0 {
|
||||||
|
if metaoraPoolIsSwapInstruction(accountList, instruction) {
|
||||||
|
return occurrence + 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if metaoraPoolIsSwapInstruction(accountList, instruction) {
|
||||||
|
occurrence++
|
||||||
|
}
|
||||||
|
|
||||||
|
inner := innerByOuter[i]
|
||||||
|
for j, instruction := range inner.Instructions {
|
||||||
|
innerOffset := uint(j + 1)
|
||||||
|
if uint(i) == offset[0] && offset[1] == innerOffset {
|
||||||
|
if metaoraPoolIsSwapInstruction(accountList, instruction) {
|
||||||
|
return occurrence + 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if metaoraPoolIsSwapInstruction(accountList, instruction) {
|
||||||
|
occurrence++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolIsSwapInstruction(accountList []solana.PublicKey, instruction Instruction) bool {
|
||||||
|
if instruction.ProgramIDIndex < 0 || instruction.ProgramIDIndex >= len(accountList) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !accountList[instruction.ProgramIDIndex].Equals(metaoraPoolProgramID) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return len(instruction.Data) >= 8 && bytes.Equal(instruction.Data[:8], metaoraPoolSwapDiscriminator[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolSwapEventFromLogs(logMessages []string, occurrence int) (metaoraPoolSwapEvent, bool) {
|
||||||
|
if occurrence <= 0 {
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type frame struct {
|
||||||
|
program string
|
||||||
|
sawSwap bool
|
||||||
|
}
|
||||||
|
|
||||||
|
targetProgram := metaoraPoolProgramID.String()
|
||||||
|
var stack []frame
|
||||||
|
seen := 0
|
||||||
|
for _, logMessage := range logMessages {
|
||||||
|
if program, ok := metaoraPoolLogInvokeProgram(logMessage); ok {
|
||||||
|
stack = append(stack, frame{program: program})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stack) > 0 && stack[len(stack)-1].program == targetProgram {
|
||||||
|
if logMessage == "Program log: Instruction: Swap" {
|
||||||
|
stack[len(stack)-1].sawSwap = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if stack[len(stack)-1].sawSwap && strings.HasPrefix(logMessage, "Program data: ") {
|
||||||
|
event, ok := metaoraPoolDecodeSwapEventLog(strings.TrimSpace(strings.TrimPrefix(logMessage, "Program data: ")))
|
||||||
|
if ok {
|
||||||
|
seen++
|
||||||
|
if seen == occurrence {
|
||||||
|
return event, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if program, ok := metaoraPoolLogFinishedProgram(logMessage); ok {
|
||||||
|
for i := len(stack) - 1; i >= 0; i-- {
|
||||||
|
if stack[i].program == program {
|
||||||
|
stack = stack[:i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolLogInvokeProgram(logMessage string) (string, bool) {
|
||||||
|
if !strings.HasPrefix(logMessage, "Program ") || !strings.Contains(logMessage, " invoke [") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
rest := strings.TrimPrefix(logMessage, "Program ")
|
||||||
|
program, _, ok := strings.Cut(rest, " ")
|
||||||
|
return program, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolLogFinishedProgram(logMessage string) (string, bool) {
|
||||||
|
if !strings.HasPrefix(logMessage, "Program ") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(logMessage, " success") && !strings.Contains(logMessage, " failed:") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
rest := strings.TrimPrefix(logMessage, "Program ")
|
||||||
|
program, _, ok := strings.Cut(rest, " ")
|
||||||
|
return program, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolDecodeSwapEventLog(encoded string) (metaoraPoolSwapEvent, bool) {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
return metaoraPoolDecodeSwapEventData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolDecodeSwapEventData(data []byte) (metaoraPoolSwapEvent, bool) {
|
||||||
|
if len(data) < 48 {
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
if !bytes.Equal(data[:8], metaoraPoolSwapEventDiscriminator[:]) {
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
body := data[8:]
|
||||||
|
return metaoraPoolSwapEvent{
|
||||||
|
InAmount: binary.LittleEndian.Uint64(body[0:8]),
|
||||||
|
OutAmount: binary.LittleEndian.Uint64(body[8:16]),
|
||||||
|
TradeFee: binary.LittleEndian.Uint64(body[16:24]),
|
||||||
|
ProtocolFee: binary.LittleEndian.Uint64(body[24:32]),
|
||||||
|
HostFee: binary.LittleEndian.Uint64(body[32:40]),
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|||||||
152
metaorapool_test.go
Normal file
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
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
@@ -193,6 +199,7 @@ func meteoraDammSwapAmountInfo(event string, params *struct {
|
|||||||
Amount1 uint64
|
Amount1 uint64
|
||||||
SwapMode uint8
|
SwapMode uint8
|
||||||
}) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
|
}) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
|
||||||
|
_ = event
|
||||||
if params == nil {
|
if params == nil {
|
||||||
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
|
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
|
||||||
}
|
}
|
||||||
@@ -203,21 +210,14 @@ func meteoraDammSwapAmountInfo(event string, params *struct {
|
|||||||
// - ExactIn / PartialFill: amount0=amount_in, amount1=minimum_amount_out
|
// - ExactIn / PartialFill: amount0=amount_in, amount1=minimum_amount_out
|
||||||
// - ExactOut: amount0=amount_out, amount1=maximum_amount_in
|
// - ExactOut: amount0=amount_out, amount1=maximum_amount_in
|
||||||
//
|
//
|
||||||
// The emitted event is normalized as token A <-> token B:
|
// `SetSwapAmountInfo` derives sides from the normalized buy/sell event, so
|
||||||
// - `sell` means A -> B, so A is the input side and B is the output side
|
// the instruction parameters should stay in raw IDL order here.
|
||||||
// - `buy` means B -> A, so B is the input side and A is the output side
|
|
||||||
switch params.SwapMode {
|
switch params.SwapMode {
|
||||||
case 0, 1: // ExactIn / PartialFill
|
case 0, 1: // ExactIn / PartialFill
|
||||||
swapMode = SwapModeExactIn
|
swapMode = SwapModeExactIn
|
||||||
if event == TxEventSell {
|
|
||||||
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
|
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
|
||||||
}
|
|
||||||
return swapMode, decimal.NewFromUint64(params.Amount1), decimal.NewFromUint64(params.Amount0), true
|
|
||||||
case 2: // ExactOut
|
case 2: // ExactOut
|
||||||
swapMode = SwapModeExactOut
|
swapMode = SwapModeExactOut
|
||||||
if event == TxEventSell {
|
|
||||||
return swapMode, decimal.NewFromUint64(params.Amount1), decimal.NewFromUint64(params.Amount0), true
|
|
||||||
}
|
|
||||||
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
|
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
|
||||||
default:
|
default:
|
||||||
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
|
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
|
||||||
@@ -445,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]]
|
||||||
|
|||||||
@@ -263,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 {
|
||||||
@@ -281,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")
|
||||||
@@ -370,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 {
|
||||||
@@ -388,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")
|
||||||
@@ -475,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 {
|
||||||
@@ -493,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")
|
||||||
@@ -577,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 {
|
||||||
@@ -595,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")
|
||||||
@@ -679,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 {
|
||||||
@@ -697,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")
|
||||||
@@ -784,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) {
|
||||||
@@ -792,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) {
|
||||||
@@ -894,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) {
|
||||||
@@ -902,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) {
|
||||||
@@ -1011,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]) {
|
||||||
@@ -1019,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]) {
|
||||||
@@ -1087,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]) {
|
||||||
@@ -1095,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]) {
|
||||||
@@ -1152,6 +1152,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
|
|||||||
limitMint,
|
limitMint,
|
||||||
actualLimitAmount,
|
actualLimitAmount,
|
||||||
)
|
)
|
||||||
|
swaps[0].SlippageBps = decimal.Zero
|
||||||
return swaps, offset, nil
|
return swaps, offset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1220,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]) {
|
||||||
@@ -1228,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]) {
|
||||||
@@ -1294,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]) {
|
||||||
@@ -1302,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]) {
|
||||||
@@ -1359,5 +1360,6 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
|||||||
limitMint,
|
limitMint,
|
||||||
actualLimitAmount,
|
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")
|
||||||
|
}
|
||||||
698
pump.go
698
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 {
|
||||||
@@ -220,17 +418,198 @@ type PumpTradeArgs struct {
|
|||||||
|
|
||||||
func pumpTradeAmountInfoFromArgs(args PumpTradeArgs) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
|
func pumpTradeAmountInfoFromArgs(args PumpTradeArgs) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
|
||||||
switch {
|
switch {
|
||||||
case bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]):
|
case bytes.Equal(args.Discriminator[:], pumpBuyExactSolInDiscriminator[:]),
|
||||||
|
bytes.Equal(args.Discriminator[:], pumpBuyExactQuoteInV2Discriminator[:]):
|
||||||
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
|
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
|
||||||
case bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]):
|
case bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]),
|
||||||
|
bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]):
|
||||||
return SwapModeExactOut, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
|
return SwapModeExactOut, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
|
||||||
case bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]):
|
case bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]),
|
||||||
|
bytes.Equal(args.Discriminator[:], pumpSellV2Discriminator[:]):
|
||||||
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
|
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
|
||||||
default:
|
default:
|
||||||
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
|
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) {
|
func normalizePumpQuoteSideMint(s *Swap) {
|
||||||
if s.FixedAmountSide == SwapAmountSideQuote && s.FixedMint.IsZero() {
|
if s.FixedAmountSide == SwapAmountSideQuote && s.FixedMint.IsZero() {
|
||||||
s.FixedMint = wSolMint
|
s.FixedMint = wSolMint
|
||||||
@@ -266,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 {
|
||||||
@@ -277,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
|
||||||
@@ -312,31 +694,43 @@ 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]
|
||||||
|
quoteReserves := decimal.Zero
|
||||||
|
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
|
||||||
|
quoteReserves = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuotePoolToken])
|
||||||
|
} else {
|
||||||
solReserves, _ := GetSolAfterTx(result, bcIdx)
|
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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -352,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 {
|
||||||
@@ -366,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]
|
||||||
@@ -386,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])
|
||||||
@@ -394,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 {
|
||||||
@@ -403,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{
|
||||||
@@ -447,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
|
||||||
@@ -460,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
|
||||||
@@ -475,51 +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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
var args PumpTradeArgs
|
|
||||||
if err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args); err == nil {
|
|
||||||
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
|
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
|
||||||
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||||
normalizePumpQuoteSideMint(&swaps[0])
|
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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -541,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) {
|
||||||
@@ -602,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{
|
||||||
@@ -630,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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -654,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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
210
pump_test.go
210
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"
|
||||||
@@ -76,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
27
pumpamm.go
27
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
|
||||||
@@ -275,6 +278,9 @@ func pumpAmmSwapAmountInfoFromArgs(args PumpSwapArgs) (swapMode SwapMode, fixedA
|
|||||||
}
|
}
|
||||||
|
|
||||||
func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
if tx.Err == nil || tx.Err.UnKnown != "" {
|
if tx.Err == nil || tx.Err.UnKnown != "" {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
||||||
}
|
}
|
||||||
@@ -401,6 +407,9 @@ func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions Inn
|
|||||||
}
|
}
|
||||||
|
|
||||||
func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
if tx.Err == nil || tx.Err.UnKnown != "" {
|
if tx.Err == nil || tx.Err.UnKnown != "" {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
||||||
}
|
}
|
||||||
@@ -525,6 +534,9 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
|||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
|
return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
|
||||||
@@ -616,6 +628,10 @@ 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
|
||||||
|
quoteAmount := decimal.NewFromUint64(event.UserQuoteAmountIn)
|
||||||
|
if event.IxName == "buy" {
|
||||||
|
quoteAmount = decimal.NewFromUint64(event.QuoteAmountIn)
|
||||||
|
}
|
||||||
swap := Swap{
|
swap := Swap{
|
||||||
Program: SolProgramPumpAMM,
|
Program: SolProgramPumpAMM,
|
||||||
Event: "buy",
|
Event: "buy",
|
||||||
@@ -629,7 +645,7 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
|||||||
QuoteMintDecimals: quoteMintDecimals,
|
QuoteMintDecimals: quoteMintDecimals,
|
||||||
User: eventUser,
|
User: eventUser,
|
||||||
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
|
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
|
||||||
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountIn),
|
QuoteAmount: quoteAmount,
|
||||||
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
|
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
|
||||||
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
|
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
|
||||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
||||||
@@ -658,6 +674,9 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
|||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -785,6 +804,9 @@ func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
|||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
|
if len(instruction.Accounts) < 11 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -883,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 {
|
||||||
|
|||||||
185
rawtx.go
185
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"
|
||||||
@@ -109,6 +113,7 @@ type Instruction struct {
|
|||||||
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 {
|
||||||
@@ -1002,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() {
|
||||||
@@ -1021,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
|
||||||
|
}
|
||||||
@@ -108,38 +108,14 @@ func raydiumClmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
switch discriminator {
|
switch discriminator {
|
||||||
case raydiumClmmIncreaseLiquidityDiscriminator:
|
case raydiumClmmIncreaseLiquidityDiscriminator:
|
||||||
accountMin = 12
|
accountMin = 12
|
||||||
market = tx.rawTx.accountList[instruction.Accounts[2]]
|
|
||||||
vault0 = instruction.Accounts[9]
|
|
||||||
vault1 = instruction.Accounts[10]
|
|
||||||
case raydiumClmmIncreaseLiquidityV2Discriminator:
|
case raydiumClmmIncreaseLiquidityV2Discriminator:
|
||||||
accountMin = 15
|
accountMin = 15
|
||||||
market = tx.rawTx.accountList[instruction.Accounts[2]]
|
|
||||||
vault0 = instruction.Accounts[9]
|
|
||||||
vault1 = instruction.Accounts[10]
|
|
||||||
//token0 = tx.rawTx.accountList[instruction.Accounts[13]]
|
|
||||||
//token1 = tx.rawTx.accountList[instruction.Accounts[14]]
|
|
||||||
case raydiumClmmOpenPositionDiscriminator:
|
case raydiumClmmOpenPositionDiscriminator:
|
||||||
accountMin = 19
|
accountMin = 19
|
||||||
market = tx.rawTx.accountList[instruction.Accounts[5]]
|
|
||||||
vault0 = instruction.Accounts[12]
|
|
||||||
vault1 = instruction.Accounts[13]
|
|
||||||
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
|
||||||
case raydiumClmmOpenPositionV2Discriminator:
|
case raydiumClmmOpenPositionV2Discriminator:
|
||||||
accountMin = 22
|
accountMin = 22
|
||||||
market = tx.rawTx.accountList[instruction.Accounts[5]]
|
|
||||||
vault0 = instruction.Accounts[12]
|
|
||||||
vault1 = instruction.Accounts[13]
|
|
||||||
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
|
||||||
//token0 = tx.rawTx.accountList[instruction.Accounts[20]]
|
|
||||||
//token1 = tx.rawTx.accountList[instruction.Accounts[21]]
|
|
||||||
case raydiumClmmOpenPositionWithToken22NftDiscriminator:
|
case raydiumClmmOpenPositionWithToken22NftDiscriminator:
|
||||||
accountMin = 20
|
accountMin = 20
|
||||||
market = tx.rawTx.accountList[instruction.Accounts[4]]
|
|
||||||
vault0 = instruction.Accounts[11]
|
|
||||||
vault1 = instruction.Accounts[12]
|
|
||||||
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
|
||||||
//token0 = tx.rawTx.accountList[instruction.Accounts[18]]
|
|
||||||
//token1 = tx.rawTx.accountList[instruction.Accounts[19]]
|
|
||||||
default:
|
default:
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
|
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
|
||||||
}
|
}
|
||||||
@@ -148,6 +124,23 @@ func raydiumClmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for raydiumClmm add liquidity instruction, offset, %d, %d", offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for raydiumClmm add liquidity instruction, offset, %d, %d", offset[0], offset[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch discriminator {
|
||||||
|
case raydiumClmmIncreaseLiquidityDiscriminator, raydiumClmmIncreaseLiquidityV2Discriminator:
|
||||||
|
market = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||||
|
vault0 = instruction.Accounts[9]
|
||||||
|
vault1 = instruction.Accounts[10]
|
||||||
|
case raydiumClmmOpenPositionDiscriminator, raydiumClmmOpenPositionV2Discriminator:
|
||||||
|
market = tx.rawTx.accountList[instruction.Accounts[5]]
|
||||||
|
vault0 = instruction.Accounts[12]
|
||||||
|
vault1 = instruction.Accounts[13]
|
||||||
|
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||||
|
case raydiumClmmOpenPositionWithToken22NftDiscriminator:
|
||||||
|
market = tx.rawTx.accountList[instruction.Accounts[4]]
|
||||||
|
vault0 = instruction.Accounts[11]
|
||||||
|
vault1 = instruction.Accounts[12]
|
||||||
|
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||||
|
}
|
||||||
|
|
||||||
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
|
||||||
@@ -197,6 +190,8 @@ func raydiumClmmDecreaseLiquidityParser(tx *Tx, instruction Instruction, innerIn
|
|||||||
accountMin = 14
|
accountMin = 14
|
||||||
} else if discriminator == raydiumClmmDecreaseLiquidityV2Discriminator {
|
} else if discriminator == raydiumClmmDecreaseLiquidityV2Discriminator {
|
||||||
accountMin = 16
|
accountMin = 16
|
||||||
|
} else {
|
||||||
|
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
|
||||||
}
|
}
|
||||||
if len(instruction.Accounts) < accountMin {
|
if len(instruction.Accounts) < accountMin {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for decrease liquidity instruction")
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for decrease liquidity instruction")
|
||||||
@@ -299,23 +294,21 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
}
|
}
|
||||||
if discriminator == raydiumClmmSwapDiscriminator {
|
if discriminator == raydiumClmmSwapDiscriminator {
|
||||||
accountMin = 9
|
accountMin = 9
|
||||||
pool = tx.rawTx.accountList[instruction.Accounts[2]]
|
|
||||||
userTokenInAccount = instruction.Accounts[3]
|
|
||||||
userTokenOutAccount = instruction.Accounts[4]
|
|
||||||
tokenInVault = instruction.Accounts[5]
|
|
||||||
tokenOutVault = instruction.Accounts[6]
|
|
||||||
} else if discriminator == raydiumClmmSwapV2Discriminator {
|
} else if discriminator == raydiumClmmSwapV2Discriminator {
|
||||||
accountMin = 13
|
accountMin = 13
|
||||||
pool = tx.rawTx.accountList[instruction.Accounts[2]]
|
} else {
|
||||||
userTokenInAccount = instruction.Accounts[3]
|
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
|
||||||
userTokenOutAccount = instruction.Accounts[4]
|
|
||||||
tokenInVault = instruction.Accounts[5]
|
|
||||||
tokenOutVault = instruction.Accounts[6]
|
|
||||||
}
|
}
|
||||||
if len(instruction.Accounts) < accountMin {
|
if len(instruction.Accounts) < accountMin {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction")
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pool = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||||
|
userTokenInAccount = instruction.Accounts[3]
|
||||||
|
userTokenOutAccount = instruction.Accounts[4]
|
||||||
|
tokenInVault = instruction.Accounts[5]
|
||||||
|
tokenOutVault = instruction.Accounts[6]
|
||||||
|
|
||||||
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, tokenInVault)
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, tokenInVault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to get tokenIn vault balance after tx: %w", err)
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get tokenIn vault balance after tx: %w", err)
|
||||||
|
|||||||
@@ -373,6 +373,9 @@ type raydiumLaunchLabSwapArgs struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
|
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
|
||||||
var programName string
|
var programName string
|
||||||
if platformConfig.Equals(bonkPlatformConfig) {
|
if platformConfig.Equals(bonkPlatformConfig) {
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ import (
|
|||||||
|
|
||||||
var maxSlippageBps = decimal.NewFromInt(10000)
|
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 SwapMode uint8
|
||||||
type SwapAmountSide uint8
|
type SwapAmountSide uint8
|
||||||
type SwapLimitType uint8
|
type SwapLimitType uint8
|
||||||
@@ -141,29 +151,36 @@ func limitSwapAmountType(swapMode SwapMode) SwapLimitType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func calculateLimitSlippageBps(limitType SwapLimitType, limitAmount, actualAmount decimal.Decimal) decimal.Decimal {
|
func calculateLimitSlippageBps(limitType SwapLimitType, limitAmount, actualAmount decimal.Decimal) decimal.Decimal {
|
||||||
|
var value decimal.Decimal
|
||||||
switch limitType {
|
switch limitType {
|
||||||
case SwapLimitTypeMinOut:
|
case SwapLimitTypeMinOut:
|
||||||
if !actualAmount.IsPositive() {
|
if !actualAmount.IsPositive() {
|
||||||
if !limitAmount.IsPositive() {
|
if !limitAmount.IsPositive() {
|
||||||
return maxSlippageBps
|
value = maxSlippageBps
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return maxSlippageBps.Neg()
|
value = maxSlippageBps.Neg()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if !limitAmount.IsPositive() {
|
if !limitAmount.IsPositive() {
|
||||||
return maxSlippageBps
|
value = maxSlippageBps
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return actualAmount.Sub(limitAmount).Mul(maxSlippageBps).Div(actualAmount)
|
value = actualAmount.Sub(limitAmount).Mul(maxSlippageBps).Div(actualAmount)
|
||||||
case SwapLimitTypeMaxIn:
|
case SwapLimitTypeMaxIn:
|
||||||
if !limitAmount.IsPositive() {
|
if !limitAmount.IsPositive() {
|
||||||
if !actualAmount.IsPositive() {
|
if !actualAmount.IsPositive() {
|
||||||
return maxSlippageBps
|
value = maxSlippageBps
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return maxSlippageBps.Neg()
|
value = maxSlippageBps.Neg()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return limitAmount.Sub(actualAmount).Mul(maxSlippageBps).Div(limitAmount)
|
value = limitAmount.Sub(actualAmount).Mul(maxSlippageBps).Div(limitAmount)
|
||||||
default:
|
default:
|
||||||
return decimal.Zero
|
value = decimal.Zero
|
||||||
}
|
}
|
||||||
|
return normalizeSlippageBps(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Swap) SetSwapAmountInfoDetailed(
|
func (s *Swap) SetSwapAmountInfoDetailed(
|
||||||
|
|||||||
@@ -79,6 +79,38 @@ func TestSetSwapAmountInfoExactInZeroLimitUsesMaxSlippage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestMeteoraDammSwapAmountInfo(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -116,6 +148,18 @@ func TestMeteoraDammSwapAmountInfo(t *testing.T) {
|
|||||||
wantFixed: 101,
|
wantFixed: 101,
|
||||||
wantLimit: 96,
|
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",
|
name: "buy exact out uses amount0 as target output and amount1 as max input",
|
||||||
event: TxEventBuy,
|
event: TxEventBuy,
|
||||||
@@ -128,6 +172,18 @@ func TestMeteoraDammSwapAmountInfo(t *testing.T) {
|
|||||||
wantFixed: 120,
|
wantFixed: 120,
|
||||||
wantLimit: 130,
|
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 {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -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
221
tx_binary.go
221
tx_binary.go
@@ -1,6 +1,7 @@
|
|||||||
package pump_parser
|
package pump_parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -11,11 +12,13 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gagliardetto/solana-go"
|
"github.com/gagliardetto/solana-go"
|
||||||
|
"github.com/mr-tron/base58"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
txBinarySchemaVersionCurrent uint16 = 3
|
txBinarySchemaVersionV3 uint16 = 3
|
||||||
|
txBinarySchemaVersionCurrent uint16 = 4
|
||||||
txBinaryEnumVersionV1 uint16 = 1
|
txBinaryEnumVersionV1 uint16 = 1
|
||||||
|
|
||||||
txBinarySOLScale int32 = 9
|
txBinarySOLScale int32 = 9
|
||||||
@@ -25,6 +28,10 @@ const (
|
|||||||
var txBinaryMagic = [4]byte{'P', 'T', 'X', 'B'}
|
var txBinaryMagic = [4]byte{'P', 'T', 'X', 'B'}
|
||||||
var txsBinaryMagic = [4]byte{'P', 'T', 'X', 'S'}
|
var txsBinaryMagic = [4]byte{'P', 'T', 'X', 'S'}
|
||||||
|
|
||||||
|
func txBinarySchemaVersionSupported(version uint16) bool {
|
||||||
|
return version >= txBinarySchemaVersionV3 && version <= txBinarySchemaVersionCurrent
|
||||||
|
}
|
||||||
|
|
||||||
type TxBinary struct {
|
type TxBinary struct {
|
||||||
SchemaVersion uint16
|
SchemaVersion uint16
|
||||||
EnumVersion uint16
|
EnumVersion uint16
|
||||||
@@ -32,6 +39,7 @@ type TxBinary struct {
|
|||||||
Signer uint32
|
Signer uint32
|
||||||
Block uint64
|
Block uint64
|
||||||
BlockIndex uint64
|
BlockIndex uint64
|
||||||
|
BlockAt int64
|
||||||
TxHash *[64]byte
|
TxHash *[64]byte
|
||||||
CuFee uint64
|
CuFee uint64
|
||||||
Swaps []SwapBinary
|
Swaps []SwapBinary
|
||||||
@@ -79,8 +87,8 @@ type SwapBinary struct {
|
|||||||
ActualLimitAmountSide SwapAmountSide
|
ActualLimitAmountSide SwapAmountSide
|
||||||
SlippageBps uint64
|
SlippageBps uint64
|
||||||
|
|
||||||
BaseReserve uint64
|
BaseReserve float64
|
||||||
QuoteReserve uint64
|
QuoteReserve float64
|
||||||
Mayhem bool
|
Mayhem bool
|
||||||
Cashback bool
|
Cashback bool
|
||||||
|
|
||||||
@@ -107,6 +115,18 @@ type TxsBinaryReaderSource interface {
|
|||||||
OpenTxsBinaryReader() (io.ReadCloser, error)
|
OpenTxsBinaryReader() (io.ReadCloser, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TxsBinaryBatchHeaderContext struct {
|
||||||
|
SourceIndex int
|
||||||
|
BatchIndex int
|
||||||
|
Reader *bufio.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
type TxsBinaryBatchHeaderFunc func(ctx *TxsBinaryBatchHeaderContext) (skip bool, err error)
|
||||||
|
|
||||||
|
type TxsBinaryMergeOptions struct {
|
||||||
|
BatchHeaderFunc TxsBinaryBatchHeaderFunc
|
||||||
|
}
|
||||||
|
|
||||||
type PlatformBinary struct {
|
type PlatformBinary struct {
|
||||||
Platform string
|
Platform string
|
||||||
PlatformFee uint64
|
PlatformFee uint64
|
||||||
@@ -173,7 +193,7 @@ func NewTxsBinary(txs []Tx) (*TxsBinary, error) {
|
|||||||
for i, tx := range txPtrs {
|
for i, tx := range txPtrs {
|
||||||
binaryTx, err := newTxBinaryWithAddressTable(tx, addressTable, addressIndex)
|
binaryTx, err := newTxBinaryWithAddressTable(tx, addressTable, addressIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("tx[%d]: %w", i, err)
|
return nil, fmt.Errorf("tx[%d], %s: %w", i, base58.Encode(tx.TxHash[:]), err)
|
||||||
}
|
}
|
||||||
out.Txs = append(out.Txs, *binaryTx)
|
out.Txs = append(out.Txs, *binaryTx)
|
||||||
}
|
}
|
||||||
@@ -190,6 +210,7 @@ func newTxBinaryWithAddressTable(tx *Tx, addressTable []solana.PublicKey, addres
|
|||||||
EnumVersion: txBinaryEnumVersionV1,
|
EnumVersion: txBinaryEnumVersionV1,
|
||||||
Block: tx.Block,
|
Block: tx.Block,
|
||||||
BlockIndex: tx.BlockIndex,
|
BlockIndex: tx.BlockIndex,
|
||||||
|
BlockAt: tx.BlockAt,
|
||||||
CuLimit: tx.CuLimit,
|
CuLimit: tx.CuLimit,
|
||||||
ComputeUnitsConsumed: tx.ComputeUnitsConsumed,
|
ComputeUnitsConsumed: tx.ComputeUnitsConsumed,
|
||||||
}
|
}
|
||||||
@@ -307,24 +328,32 @@ func DecodeTxsBinaryReader(r io.Reader) iter.Seq2[*Tx, error] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func MergeTxsBinaryBytes(encodedBatches [][]byte) ([]byte, error) {
|
func MergeTxsBinaryBytes(encodedBatches [][]byte) ([]byte, error) {
|
||||||
|
return MergeTxsBinaryBytesWithOptions(encodedBatches, TxsBinaryMergeOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func MergeTxsBinaryBytesWithOptions(encodedBatches [][]byte, opts TxsBinaryMergeOptions) ([]byte, error) {
|
||||||
sources := make([]TxsBinaryReaderSource, 0, len(encodedBatches))
|
sources := make([]TxsBinaryReaderSource, 0, len(encodedBatches))
|
||||||
for _, encoded := range encodedBatches {
|
for _, encoded := range encodedBatches {
|
||||||
sources = append(sources, txBinaryBytesSource{data: encoded})
|
sources = append(sources, txBinaryBytesSource{data: encoded})
|
||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
if err := MergeTxsBinarySourcesToWriter(sources, &out); err != nil {
|
if err := MergeTxsBinarySourcesToWriterWithOptions(sources, &out, opts); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return out.Bytes(), nil
|
return out.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MergeTxsBinarySourcesToWriter(sources []TxsBinaryReaderSource, w io.Writer) error {
|
func MergeTxsBinarySourcesToWriter(sources []TxsBinaryReaderSource, w io.Writer) error {
|
||||||
|
return MergeTxsBinarySourcesToWriterWithOptions(sources, w, TxsBinaryMergeOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func MergeTxsBinarySourcesToWriterWithOptions(sources []TxsBinaryReaderSource, w io.Writer, opts TxsBinaryMergeOptions) error {
|
||||||
if w == nil {
|
if w == nil {
|
||||||
return fmt.Errorf("txs binary writer is nil")
|
return fmt.Errorf("txs binary writer is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
plan, err := txBinaryBuildMergePlan(sources)
|
plan, err := txBinaryBuildMergePlan(sources, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -343,9 +372,22 @@ func MergeTxsBinarySourcesToWriter(sources []TxsBinaryReaderSource, w io.Writer)
|
|||||||
return fmt.Errorf("source[%d]: open reader: %w", sourceIndex, err)
|
return fmt.Errorf("source[%d]: open reader: %w", sourceIndex, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dec := txBinaryStreamDecoder{reader: reader}
|
bufferedReader := bufio.NewReader(reader)
|
||||||
|
dec := txBinaryStreamDecoder{reader: bufferedReader}
|
||||||
batchIndex := 0
|
batchIndex := 0
|
||||||
for {
|
for {
|
||||||
|
skipBatch, err := txBinaryApplyMergeBatchHeader(bufferedReader, opts, sourceIndex, batchIndex)
|
||||||
|
if err != nil {
|
||||||
|
closeErr := reader.Close()
|
||||||
|
if err == io.EOF {
|
||||||
|
if closeErr != nil {
|
||||||
|
return fmt.Errorf("source[%d]: close reader: %w", sourceIndex, closeErr)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return fmt.Errorf("source[%d].batch[%d]: %w", sourceIndex, batchIndex, err)
|
||||||
|
}
|
||||||
|
|
||||||
header, err := dec.readTxsBinaryHeaderOrEOF()
|
header, err := dec.readTxsBinaryHeaderOrEOF()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeErr := reader.Close()
|
closeErr := reader.Close()
|
||||||
@@ -368,11 +410,16 @@ func MergeTxsBinarySourcesToWriter(sources []TxsBinaryReaderSource, w io.Writer)
|
|||||||
reader.Close()
|
reader.Close()
|
||||||
return fmt.Errorf("source[%d].batch[%d].tx[%d]: %w", sourceIndex, batchIndex, txIndex, err)
|
return fmt.Errorf("source[%d].batch[%d].tx[%d]: %w", sourceIndex, batchIndex, txIndex, err)
|
||||||
}
|
}
|
||||||
|
if skipBatch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err := txBinaryRemapTxAddressTable(&tx, header.addressTable, plan.addressTable, plan.addressIndex); err != nil {
|
if err := txBinaryRemapTxAddressTable(&tx, header.addressTable, plan.addressTable, plan.addressIndex); err != nil {
|
||||||
reader.Close()
|
reader.Close()
|
||||||
return fmt.Errorf("source[%d].batch[%d].tx[%d]: %w", sourceIndex, batchIndex, txIndex, err)
|
return fmt.Errorf("source[%d].batch[%d].tx[%d]: %w", sourceIndex, batchIndex, txIndex, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tx.SchemaVersion = plan.schemaVersion
|
||||||
|
tx.EnumVersion = plan.enumVersion
|
||||||
bodyBytes, err := txBinaryMarshalTxBody(&tx, plan.enumTable)
|
bodyBytes, err := txBinaryMarshalTxBody(&tx, plan.enumTable)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reader.Close()
|
reader.Close()
|
||||||
@@ -394,7 +441,7 @@ func (tx *TxBinary) MarshalBinary() ([]byte, error) {
|
|||||||
if tx == nil {
|
if tx == nil {
|
||||||
return nil, fmt.Errorf("tx binary is nil")
|
return nil, fmt.Errorf("tx binary is nil")
|
||||||
}
|
}
|
||||||
if tx.SchemaVersion != txBinarySchemaVersionCurrent {
|
if !txBinarySchemaVersionSupported(tx.SchemaVersion) {
|
||||||
return nil, fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion)
|
return nil, fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,7 +469,7 @@ func (txs *TxsBinary) MarshalBinary() ([]byte, error) {
|
|||||||
if txs == nil {
|
if txs == nil {
|
||||||
return nil, fmt.Errorf("txs binary is nil")
|
return nil, fmt.Errorf("txs binary is nil")
|
||||||
}
|
}
|
||||||
if txs.SchemaVersion != txBinarySchemaVersionCurrent {
|
if !txBinarySchemaVersionSupported(txs.SchemaVersion) {
|
||||||
return nil, fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion)
|
return nil, fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,8 +487,11 @@ func (txs *TxsBinary) MarshalBinary() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
enc.writeUint32(uint32(len(txs.Txs)))
|
enc.writeUint32(uint32(len(txs.Txs)))
|
||||||
for i := range txs.Txs {
|
for i := range txs.Txs {
|
||||||
if err := enc.writeTxBinaryBody(&txs.Txs[i], enumTable); err != nil {
|
tx := txs.Txs[i]
|
||||||
return nil, fmt.Errorf("tx[%d]: %w", i, err)
|
tx.SchemaVersion = txs.SchemaVersion
|
||||||
|
tx.EnumVersion = txs.EnumVersion
|
||||||
|
if err := enc.writeTxBinaryBody(&tx, enumTable); err != nil {
|
||||||
|
return nil, fmt.Errorf("tx[%d], %s: %w", i, base58.Encode(tx.TxHash[:]), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return enc.bytes(), nil
|
return enc.bytes(), nil
|
||||||
@@ -482,7 +532,7 @@ func (tx *TxBinary) UnmarshalBinary(data []byte) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if tx.SchemaVersion != txBinarySchemaVersionCurrent {
|
if !txBinarySchemaVersionSupported(tx.SchemaVersion) {
|
||||||
return fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion)
|
return fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,7 +572,7 @@ func (txs *TxsBinary) UnmarshalBinary(data []byte) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if txs.SchemaVersion != txBinarySchemaVersionCurrent {
|
if !txBinarySchemaVersionSupported(txs.SchemaVersion) {
|
||||||
return fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion)
|
return fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,6 +625,7 @@ func (tx *TxBinary) ToTx() (*Tx, error) {
|
|||||||
Signer: signer,
|
Signer: signer,
|
||||||
Block: tx.Block,
|
Block: tx.Block,
|
||||||
BlockIndex: tx.BlockIndex,
|
BlockIndex: tx.BlockIndex,
|
||||||
|
BlockAt: tx.BlockAt,
|
||||||
CuFee: decimal.NewFromUint64(tx.CuFee),
|
CuFee: decimal.NewFromUint64(tx.CuFee),
|
||||||
CUPrice: decimal.NewFromUint64(tx.CUPrice).Shift(-txBinaryCUPriceScale),
|
CUPrice: decimal.NewFromUint64(tx.CUPrice).Shift(-txBinaryCUPriceScale),
|
||||||
BeforeSolBalance: txBinaryFloat64ToDecimal(tx.BeforeSolBalance, txBinarySOLScale),
|
BeforeSolBalance: txBinaryFloat64ToDecimal(tx.BeforeSolBalance, txBinarySOLScale),
|
||||||
@@ -691,7 +742,7 @@ func newSwapBinary(swap Swap, index int, addressIndex *txBinaryAddressIndex) (Sw
|
|||||||
|
|
||||||
out := SwapBinary{
|
out := SwapBinary{
|
||||||
Program: swap.Program,
|
Program: swap.Program,
|
||||||
Event: swap.Event,
|
Event: txBinaryCanonicalEvent(swap.Event),
|
||||||
TxIndex: int32(swap.TxIndex),
|
TxIndex: int32(swap.TxIndex),
|
||||||
InstrIdx: swap.InstrIdx,
|
InstrIdx: swap.InstrIdx,
|
||||||
InnerIdx: swap.InnerIdx,
|
InnerIdx: swap.InnerIdx,
|
||||||
@@ -740,10 +791,10 @@ func newSwapBinary(swap Swap, index int, addressIndex *txBinaryAddressIndex) (Sw
|
|||||||
if out.SlippageBps, err = txBinaryRoundedDecimalToUint64(swap.SlippageBps, fmt.Sprintf("swap[%d].slippage_bps", index)); err != nil {
|
if out.SlippageBps, err = txBinaryRoundedDecimalToUint64(swap.SlippageBps, fmt.Sprintf("swap[%d].slippage_bps", index)); err != nil {
|
||||||
return SwapBinary{}, err
|
return SwapBinary{}, err
|
||||||
}
|
}
|
||||||
if out.BaseReserve, err = txBinaryDecimalToUint64(swap.BaseReserve, fmt.Sprintf("swap[%d].base_reserve", index)); err != nil {
|
if out.BaseReserve, err = txBinaryDecimalToFloat64Raw(swap.BaseReserve, fmt.Sprintf("swap[%d].base_reserve", index)); err != nil {
|
||||||
return SwapBinary{}, err
|
return SwapBinary{}, err
|
||||||
}
|
}
|
||||||
if out.QuoteReserve, err = txBinaryDecimalToUint64(swap.QuoteReserve, fmt.Sprintf("swap[%d].quote_reserve", index)); err != nil {
|
if out.QuoteReserve, err = txBinaryDecimalToFloat64Raw(swap.QuoteReserve, fmt.Sprintf("swap[%d].quote_reserve", index)); err != nil {
|
||||||
return SwapBinary{}, err
|
return SwapBinary{}, err
|
||||||
}
|
}
|
||||||
if out.UserBaseBalance, err = txBinaryDecimalToUint64(swap.UserBaseBalance, fmt.Sprintf("swap[%d].user_base_balance", index)); err != nil {
|
if out.UserBaseBalance, err = txBinaryDecimalToUint64(swap.UserBaseBalance, fmt.Sprintf("swap[%d].user_base_balance", index)); err != nil {
|
||||||
@@ -841,8 +892,8 @@ func (swap SwapBinary) toSwap(addressTable []solana.PublicKey, index int) (Swap,
|
|||||||
ActualLimitAmount: decimal.NewFromUint64(swap.ActualLimitAmount),
|
ActualLimitAmount: decimal.NewFromUint64(swap.ActualLimitAmount),
|
||||||
ActualLimitAmountSide: swap.ActualLimitAmountSide,
|
ActualLimitAmountSide: swap.ActualLimitAmountSide,
|
||||||
SlippageBps: decimal.NewFromUint64(swap.SlippageBps),
|
SlippageBps: decimal.NewFromUint64(swap.SlippageBps),
|
||||||
BaseReserve: decimal.NewFromUint64(swap.BaseReserve),
|
BaseReserve: txBinaryFloat64ToDecimalRaw(swap.BaseReserve),
|
||||||
QuoteReserve: decimal.NewFromUint64(swap.QuoteReserve),
|
QuoteReserve: txBinaryFloat64ToDecimalRaw(swap.QuoteReserve),
|
||||||
Mayhem: swap.Mayhem,
|
Mayhem: swap.Mayhem,
|
||||||
Cashback: swap.Cashback,
|
Cashback: swap.Cashback,
|
||||||
UserBaseBalance: decimal.NewFromUint64(swap.UserBaseBalance),
|
UserBaseBalance: decimal.NewFromUint64(swap.UserBaseBalance),
|
||||||
@@ -881,6 +932,17 @@ func txBinaryPlatformsFromTx(platforms map[string]platformInfo) ([]PlatformBinar
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func txBinaryCanonicalEvent(event string) string {
|
||||||
|
switch event {
|
||||||
|
case "add_liquidity_on_side":
|
||||||
|
return TxEventAddLiquidityOneSide
|
||||||
|
case "remove_liquidity_on_side":
|
||||||
|
return TxEventRemoveLiquidityOneSide
|
||||||
|
default:
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func txBinaryMevAgentsFromTx(mevAgents map[string]mevInfo) ([]MevAgentBinary, error) {
|
func txBinaryMevAgentsFromTx(mevAgents map[string]mevInfo) ([]MevAgentBinary, error) {
|
||||||
if len(mevAgents) == 0 {
|
if len(mevAgents) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -1026,6 +1088,14 @@ func txBinaryDecimalToFloat64(value decimal.Decimal, scale int32, field string)
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func txBinaryDecimalToFloat64Raw(value decimal.Decimal, field string) (float64, error) {
|
||||||
|
f, exact := value.Float64()
|
||||||
|
if !exact && math.IsInf(f, 0) {
|
||||||
|
return 0, fmt.Errorf("%s cannot be represented as float64: %s", field, value.String())
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
func txBinaryFloat64ToDecimal(value float64, scale int32) decimal.Decimal {
|
func txBinaryFloat64ToDecimal(value float64, scale int32) decimal.Decimal {
|
||||||
formatted := strconv.FormatFloat(value, 'f', int(scale), 64)
|
formatted := strconv.FormatFloat(value, 'f', int(scale), 64)
|
||||||
out, err := decimal.NewFromString(formatted)
|
out, err := decimal.NewFromString(formatted)
|
||||||
@@ -1035,6 +1105,15 @@ func txBinaryFloat64ToDecimal(value float64, scale int32) decimal.Decimal {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func txBinaryFloat64ToDecimalRaw(value float64) decimal.Decimal {
|
||||||
|
formatted := strconv.FormatFloat(value, 'f', -1, 64)
|
||||||
|
out, err := decimal.NewFromString(formatted)
|
||||||
|
if err != nil {
|
||||||
|
return decimal.Zero
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
type txBinaryEncoder struct {
|
type txBinaryEncoder struct {
|
||||||
buf bytes.Buffer
|
buf bytes.Buffer
|
||||||
}
|
}
|
||||||
@@ -1100,6 +1179,9 @@ func (enc *txBinaryEncoder) writeTxBinaryBody(tx *TxBinary, enumTable *txBinaryE
|
|||||||
enc.writeUint32(tx.Signer)
|
enc.writeUint32(tx.Signer)
|
||||||
enc.writeUint64(tx.Block)
|
enc.writeUint64(tx.Block)
|
||||||
enc.writeUint64(tx.BlockIndex)
|
enc.writeUint64(tx.BlockIndex)
|
||||||
|
if tx.SchemaVersion >= txBinarySchemaVersionCurrent {
|
||||||
|
enc.writeUint64(uint64(tx.BlockAt))
|
||||||
|
}
|
||||||
enc.writeBool(tx.TxHash != nil)
|
enc.writeBool(tx.TxHash != nil)
|
||||||
if tx.TxHash != nil {
|
if tx.TxHash != nil {
|
||||||
enc.writeBytes(tx.TxHash[:])
|
enc.writeBytes(tx.TxHash[:])
|
||||||
@@ -1125,7 +1207,7 @@ func (enc *txBinaryEncoder) writeTxBinaryBody(tx *TxBinary, enumTable *txBinaryE
|
|||||||
func (enc *txBinaryEncoder) writePlatformEntries(entries []PlatformBinary, enumTable *txBinaryEnumTable) error {
|
func (enc *txBinaryEncoder) writePlatformEntries(entries []PlatformBinary, enumTable *txBinaryEnumTable) error {
|
||||||
enc.writeUint32(uint32(len(entries)))
|
enc.writeUint32(uint32(len(entries)))
|
||||||
for i, entry := range entries {
|
for i, entry := range entries {
|
||||||
enumID, err := enumTable.platforms.id(entry.Platform)
|
enumID, err := enumTable.platforms.idOrFallback(entry.Platform, PlatformNone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("platform[%d]: %w", i, err)
|
return fmt.Errorf("platform[%d]: %w", i, err)
|
||||||
}
|
}
|
||||||
@@ -1138,7 +1220,7 @@ func (enc *txBinaryEncoder) writePlatformEntries(entries []PlatformBinary, enumT
|
|||||||
func (enc *txBinaryEncoder) writeMevAgentEntries(entries []MevAgentBinary, enumTable *txBinaryEnumTable) error {
|
func (enc *txBinaryEncoder) writeMevAgentEntries(entries []MevAgentBinary, enumTable *txBinaryEnumTable) error {
|
||||||
enc.writeUint32(uint32(len(entries)))
|
enc.writeUint32(uint32(len(entries)))
|
||||||
for i, entry := range entries {
|
for i, entry := range entries {
|
||||||
enumID, err := enumTable.mevAgents.id(entry.MevAgent)
|
enumID, err := enumTable.mevAgents.idOrFallback(entry.MevAgent, MevAgentUnknown)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("mev_agent[%d]: %w", i, err)
|
return fmt.Errorf("mev_agent[%d]: %w", i, err)
|
||||||
}
|
}
|
||||||
@@ -1187,8 +1269,8 @@ func (enc *txBinaryEncoder) writeSwaps(swaps []SwapBinary, enumTable *txBinaryEn
|
|||||||
enc.writeUint64(swap.ActualLimitAmount)
|
enc.writeUint64(swap.ActualLimitAmount)
|
||||||
enc.writeUint8(uint8(swap.ActualLimitAmountSide))
|
enc.writeUint8(uint8(swap.ActualLimitAmountSide))
|
||||||
enc.writeUint64(swap.SlippageBps)
|
enc.writeUint64(swap.SlippageBps)
|
||||||
enc.writeUint64(swap.BaseReserve)
|
enc.writeFloat64(swap.BaseReserve)
|
||||||
enc.writeUint64(swap.QuoteReserve)
|
enc.writeFloat64(swap.QuoteReserve)
|
||||||
enc.writeBool(swap.Mayhem)
|
enc.writeBool(swap.Mayhem)
|
||||||
enc.writeBool(swap.Cashback)
|
enc.writeBool(swap.Cashback)
|
||||||
enc.writeUint64(swap.UserBaseBalance)
|
enc.writeUint64(swap.UserBaseBalance)
|
||||||
@@ -1408,7 +1490,7 @@ func (dec *txBinaryStreamDecoder) readTxsBinaryHeader() (*txsBinaryHeader, error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if schemaVersion != txBinarySchemaVersionCurrent {
|
if !txBinarySchemaVersionSupported(schemaVersion) {
|
||||||
return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion)
|
return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1465,7 +1547,7 @@ func (dec *txBinaryStreamDecoder) readTxsBinaryHeaderOrEOF() (*txsBinaryHeader,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if schemaVersion != txBinarySchemaVersionCurrent {
|
if !txBinarySchemaVersionSupported(schemaVersion) {
|
||||||
return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion)
|
return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1526,7 +1608,7 @@ func txBinaryReadPlatformEntries(dec txBinaryBodyReader, enumTable *txBinaryEnum
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
platform, err := enumTable.platforms.value(enumID)
|
platform, err := enumTable.platforms.valueOrFallback(enumID, PlatformNone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("platform[%d]: %w", i, err)
|
return nil, fmt.Errorf("platform[%d]: %w", i, err)
|
||||||
}
|
}
|
||||||
@@ -1553,7 +1635,7 @@ func txBinaryReadMevAgentEntries(dec txBinaryBodyReader, enumTable *txBinaryEnum
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
mevAgent, err := enumTable.mevAgents.value(enumID)
|
mevAgent, err := enumTable.mevAgents.valueOrFallback(enumID, MevAgentUnknown)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("mev_agent[%d]: %w", i, err)
|
return nil, fmt.Errorf("mev_agent[%d]: %w", i, err)
|
||||||
}
|
}
|
||||||
@@ -1683,10 +1765,10 @@ func txBinaryReadSwaps(dec txBinaryBodyReader, enumTable *txBinaryEnumTable) ([]
|
|||||||
if swap.SlippageBps, err = dec.readUint64(); err != nil {
|
if swap.SlippageBps, err = dec.readUint64(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if swap.BaseReserve, err = dec.readUint64(); err != nil {
|
if swap.BaseReserve, err = dec.readFloat64(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if swap.QuoteReserve, err = dec.readUint64(); err != nil {
|
if swap.QuoteReserve, err = dec.readFloat64(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if swap.Mayhem, err = dec.readBool(); err != nil {
|
if swap.Mayhem, err = dec.readBool(); err != nil {
|
||||||
@@ -1733,6 +1815,13 @@ func txBinaryReadTxBody(dec txBinaryBodyReader, tx *TxBinary, enumTable *txBinar
|
|||||||
if tx.BlockIndex, err = dec.readUint64(); err != nil {
|
if tx.BlockIndex, err = dec.readUint64(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if tx.SchemaVersion >= txBinarySchemaVersionCurrent {
|
||||||
|
blockAt, err := dec.readUint64()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tx.BlockAt = int64(blockAt)
|
||||||
|
}
|
||||||
|
|
||||||
hasTxHash, err := dec.readBool()
|
hasTxHash, err := dec.readBool()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1780,7 +1869,7 @@ func txBinaryReadTxBody(dec txBinaryBodyReader, tx *TxBinary, enumTable *txBinar
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func txBinaryBuildMergePlan(sources []TxsBinaryReaderSource) (*txsBinaryMergePlan, error) {
|
func txBinaryBuildMergePlan(sources []TxsBinaryReaderSource, opts TxsBinaryMergeOptions) (*txsBinaryMergePlan, error) {
|
||||||
if len(sources) == 0 {
|
if len(sources) == 0 {
|
||||||
return nil, fmt.Errorf("txs binary sources are empty")
|
return nil, fmt.Errorf("txs binary sources are empty")
|
||||||
}
|
}
|
||||||
@@ -1788,7 +1877,9 @@ func txBinaryBuildMergePlan(sources []TxsBinaryReaderSource) (*txsBinaryMergePla
|
|||||||
builder := txBinaryAddressTableBuilder{
|
builder := txBinaryAddressTableBuilder{
|
||||||
index: make(map[solana.PublicKey]struct{}),
|
index: make(map[solana.PublicKey]struct{}),
|
||||||
}
|
}
|
||||||
plan := &txsBinaryMergePlan{}
|
plan := &txsBinaryMergePlan{
|
||||||
|
schemaVersion: txBinarySchemaVersionCurrent,
|
||||||
|
}
|
||||||
hasBatch := false
|
hasBatch := false
|
||||||
|
|
||||||
for sourceIndex, source := range sources {
|
for sourceIndex, source := range sources {
|
||||||
@@ -1801,9 +1892,22 @@ func txBinaryBuildMergePlan(sources []TxsBinaryReaderSource) (*txsBinaryMergePla
|
|||||||
return nil, fmt.Errorf("source[%d]: open reader: %w", sourceIndex, err)
|
return nil, fmt.Errorf("source[%d]: open reader: %w", sourceIndex, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dec := txBinaryStreamDecoder{reader: reader}
|
bufferedReader := bufio.NewReader(reader)
|
||||||
|
dec := txBinaryStreamDecoder{reader: bufferedReader}
|
||||||
batchIndex := 0
|
batchIndex := 0
|
||||||
for {
|
for {
|
||||||
|
skipBatch, err := txBinaryApplyMergeBatchHeader(bufferedReader, opts, sourceIndex, batchIndex)
|
||||||
|
if err != nil {
|
||||||
|
closeErr := reader.Close()
|
||||||
|
if err == io.EOF {
|
||||||
|
if closeErr != nil {
|
||||||
|
return nil, fmt.Errorf("source[%d]: close reader: %w", sourceIndex, closeErr)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("source[%d].batch[%d]: %w", sourceIndex, batchIndex, err)
|
||||||
|
}
|
||||||
|
|
||||||
header, err := dec.readTxsBinaryHeaderOrEOF()
|
header, err := dec.readTxsBinaryHeaderOrEOF()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeErr := reader.Close()
|
closeErr := reader.Close()
|
||||||
@@ -1817,15 +1921,10 @@ func txBinaryBuildMergePlan(sources []TxsBinaryReaderSource) (*txsBinaryMergePla
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !hasBatch {
|
if !hasBatch {
|
||||||
plan.schemaVersion = header.schemaVersion
|
|
||||||
plan.enumVersion = header.enumVersion
|
plan.enumVersion = header.enumVersion
|
||||||
plan.enumTable = header.enumTable
|
plan.enumTable = header.enumTable
|
||||||
hasBatch = true
|
hasBatch = true
|
||||||
} else {
|
} else {
|
||||||
if header.schemaVersion != plan.schemaVersion {
|
|
||||||
reader.Close()
|
|
||||||
return nil, fmt.Errorf("source[%d].batch[%d]: schema version mismatch: got %d want %d", sourceIndex, batchIndex, header.schemaVersion, plan.schemaVersion)
|
|
||||||
}
|
|
||||||
if header.enumVersion != plan.enumVersion {
|
if header.enumVersion != plan.enumVersion {
|
||||||
reader.Close()
|
reader.Close()
|
||||||
return nil, fmt.Errorf("source[%d].batch[%d]: enum version mismatch: got %d want %d", sourceIndex, batchIndex, header.enumVersion, plan.enumVersion)
|
return nil, fmt.Errorf("source[%d].batch[%d]: enum version mismatch: got %d want %d", sourceIndex, batchIndex, header.enumVersion, plan.enumVersion)
|
||||||
@@ -1833,17 +1932,21 @@ func txBinaryBuildMergePlan(sources []TxsBinaryReaderSource) (*txsBinaryMergePla
|
|||||||
}
|
}
|
||||||
|
|
||||||
for addressIndex, address := range header.addressTable {
|
for addressIndex, address := range header.addressTable {
|
||||||
|
if !skipBatch {
|
||||||
if err := builder.add(address); err != nil {
|
if err := builder.add(address); err != nil {
|
||||||
reader.Close()
|
reader.Close()
|
||||||
return nil, fmt.Errorf("source[%d].batch[%d].address[%d]: %w", sourceIndex, batchIndex, addressIndex, err)
|
return nil, fmt.Errorf("source[%d].batch[%d].address[%d]: %w", sourceIndex, batchIndex, addressIndex, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !skipBatch {
|
||||||
if uint64(plan.txCount)+uint64(header.count) > uint64(math.MaxUint32) {
|
if uint64(plan.txCount)+uint64(header.count) > uint64(math.MaxUint32) {
|
||||||
reader.Close()
|
reader.Close()
|
||||||
return nil, fmt.Errorf("merged tx count exceeds uint32 capacity")
|
return nil, fmt.Errorf("merged tx count exceeds uint32 capacity")
|
||||||
}
|
}
|
||||||
plan.txCount += header.count
|
plan.txCount += header.count
|
||||||
|
}
|
||||||
|
|
||||||
for txIndex := uint32(0); txIndex < header.count; txIndex++ {
|
for txIndex := uint32(0); txIndex < header.count; txIndex++ {
|
||||||
tx := TxBinary{
|
tx := TxBinary{
|
||||||
@@ -1947,6 +2050,17 @@ func txBinaryWriteAll(w io.Writer, data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func txBinaryApplyMergeBatchHeader(reader *bufio.Reader, opts TxsBinaryMergeOptions, sourceIndex int, batchIndex int) (bool, error) {
|
||||||
|
if opts.BatchHeaderFunc == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return opts.BatchHeaderFunc(&TxsBinaryBatchHeaderContext{
|
||||||
|
SourceIndex: sourceIndex,
|
||||||
|
BatchIndex: batchIndex,
|
||||||
|
Reader: reader,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type txBinaryEnumTable struct {
|
type txBinaryEnumTable struct {
|
||||||
version uint16
|
version uint16
|
||||||
programs txBinaryEnumSet
|
programs txBinaryEnumSet
|
||||||
@@ -1990,6 +2104,18 @@ var txBinaryEnumTables = map[uint16]*txBinaryEnumTable{
|
|||||||
TxEventBuyFailed,
|
TxEventBuyFailed,
|
||||||
TxEventSellFailed,
|
TxEventSellFailed,
|
||||||
TxEventBurn,
|
TxEventBurn,
|
||||||
|
TxEventCreate,
|
||||||
|
TxEventComplete,
|
||||||
|
TxEventMigrate,
|
||||||
|
TxEventDeposit,
|
||||||
|
TxEventWithdraw,
|
||||||
|
TxEventOpen,
|
||||||
|
TxEventClose,
|
||||||
|
TxEventClaimFee,
|
||||||
|
TxEventAddLiquidity,
|
||||||
|
TxEventAddLiquidityOneSide,
|
||||||
|
TxEventRemoveLiquidity,
|
||||||
|
TxEventRemoveLiquidityOneSide,
|
||||||
},
|
},
|
||||||
"platform",
|
"platform",
|
||||||
[]string{
|
[]string{
|
||||||
@@ -2037,6 +2163,8 @@ var txBinaryEnumTables = map[uint16]*txBinaryEnumTable{
|
|||||||
MevAgentSpeedlanding,
|
MevAgentSpeedlanding,
|
||||||
MevAgentAllenhark,
|
MevAgentAllenhark,
|
||||||
MevAgentRaiden,
|
MevAgentRaiden,
|
||||||
|
MevAgentZan,
|
||||||
|
MevAgentTunneling,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -2092,9 +2220,30 @@ func (set txBinaryEnumSet) id(value string) (uint16, error) {
|
|||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (set txBinaryEnumSet) idOrFallback(value string, fallback string) (uint16, error) {
|
||||||
|
if id, ok := set.ids[value]; ok {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
id, ok := set.ids[fallback]
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("unsupported %s fallback enum value %q for versioned tx binary", set.name, fallback)
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (set txBinaryEnumSet) value(id uint16) (string, error) {
|
func (set txBinaryEnumSet) value(id uint16) (string, error) {
|
||||||
if int(id) >= len(set.values) {
|
if int(id) >= len(set.values) {
|
||||||
return "", fmt.Errorf("unknown %s enum id %d", set.name, id)
|
return "", fmt.Errorf("unknown %s enum id %d", set.name, id)
|
||||||
}
|
}
|
||||||
return set.values[id], nil
|
return set.values[id], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (set txBinaryEnumSet) valueOrFallback(id uint16, fallback string) (string, error) {
|
||||||
|
if int(id) < len(set.values) {
|
||||||
|
return set.values[id], nil
|
||||||
|
}
|
||||||
|
if _, ok := set.ids[fallback]; !ok {
|
||||||
|
return "", fmt.Errorf("unsupported %s fallback enum value %q for versioned tx binary", set.name, fallback)
|
||||||
|
}
|
||||||
|
return fallback, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func TestTxBinaryRoundTrip(t *testing.T) {
|
|||||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
Block: 123456789,
|
Block: 123456789,
|
||||||
BlockIndex: 42,
|
BlockIndex: 42,
|
||||||
|
BlockAt: 1710000000,
|
||||||
TxHash: &txHash,
|
TxHash: &txHash,
|
||||||
CuFee: decimal.NewFromInt(5000),
|
CuFee: decimal.NewFromInt(5000),
|
||||||
CUPrice: decimal.RequireFromString("0.123456"),
|
CUPrice: decimal.RequireFromString("0.123456"),
|
||||||
@@ -41,6 +42,14 @@ func TestTxBinaryRoundTrip(t *testing.T) {
|
|||||||
MevAgent: MevAgentJito,
|
MevAgent: MevAgentJito,
|
||||||
MevAgentFee: decimal.RequireFromString("0.030000000"),
|
MevAgentFee: decimal.RequireFromString("0.030000000"),
|
||||||
},
|
},
|
||||||
|
MevAgentZan: {
|
||||||
|
MevAgent: MevAgentZan,
|
||||||
|
MevAgentFee: decimal.RequireFromString("0.040000000"),
|
||||||
|
},
|
||||||
|
MevAgentTunneling: {
|
||||||
|
MevAgent: MevAgentTunneling,
|
||||||
|
MevAgentFee: decimal.RequireFromString("0.050000000"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Swaps: []Swap{
|
Swaps: []Swap{
|
||||||
{
|
{
|
||||||
@@ -110,6 +119,9 @@ func TestTxBinaryRoundTrip(t *testing.T) {
|
|||||||
if decoded.BlockIndex != original.BlockIndex {
|
if decoded.BlockIndex != original.BlockIndex {
|
||||||
t.Fatalf("BlockIndex = %d, want %d", decoded.BlockIndex, original.BlockIndex)
|
t.Fatalf("BlockIndex = %d, want %d", decoded.BlockIndex, original.BlockIndex)
|
||||||
}
|
}
|
||||||
|
if decoded.BlockAt != original.BlockAt {
|
||||||
|
t.Fatalf("BlockAt = %d, want %d", decoded.BlockAt, original.BlockAt)
|
||||||
|
}
|
||||||
if decoded.TxHash == nil {
|
if decoded.TxHash == nil {
|
||||||
t.Fatal("TxHash = nil, want non-nil")
|
t.Fatal("TxHash = nil, want non-nil")
|
||||||
}
|
}
|
||||||
@@ -146,6 +158,12 @@ func TestTxBinaryRoundTrip(t *testing.T) {
|
|||||||
if !decoded.MevAgent[MevAgentJito].MevAgentFee.Equal(original.MevAgent[MevAgentJito].MevAgentFee) {
|
if !decoded.MevAgent[MevAgentJito].MevAgentFee.Equal(original.MevAgent[MevAgentJito].MevAgentFee) {
|
||||||
t.Fatalf("MevAgent fee mismatch")
|
t.Fatalf("MevAgent fee mismatch")
|
||||||
}
|
}
|
||||||
|
if !decoded.MevAgent[MevAgentZan].MevAgentFee.Equal(original.MevAgent[MevAgentZan].MevAgentFee) {
|
||||||
|
t.Fatalf("Zan MevAgent fee mismatch")
|
||||||
|
}
|
||||||
|
if !decoded.MevAgent[MevAgentTunneling].MevAgentFee.Equal(original.MevAgent[MevAgentTunneling].MevAgentFee) {
|
||||||
|
t.Fatalf("Tunneling MevAgent fee mismatch")
|
||||||
|
}
|
||||||
if len(decoded.Swaps) != 1 {
|
if len(decoded.Swaps) != 1 {
|
||||||
t.Fatalf("Swaps len = %d, want 1", len(decoded.Swaps))
|
t.Fatalf("Swaps len = %d, want 1", len(decoded.Swaps))
|
||||||
}
|
}
|
||||||
@@ -225,11 +243,278 @@ func TestTxBinaryRejectsUnknownProgramEnum(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTxBinaryLabelEnumsFallbackToUnknown(t *testing.T) {
|
||||||
|
original := &Tx{
|
||||||
|
Signer: solana.WrappedSol,
|
||||||
|
Platform: map[string]platformInfo{
|
||||||
|
"future-platform": {
|
||||||
|
Platform: "future-platform",
|
||||||
|
PlatformFee: decimal.RequireFromString("0.010000000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MevAgent: map[string]mevInfo{
|
||||||
|
"future-mev-agent": {
|
||||||
|
MevAgent: "future-mev-agent",
|
||||||
|
MevAgentFee: decimal.RequireFromString("0.020000000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := EncodeTxBinary(original)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
decoded, err := DecodeTxBinary(encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(decoded.Platform) != 1 {
|
||||||
|
t.Fatalf("Platform len = %d, want 1", len(decoded.Platform))
|
||||||
|
}
|
||||||
|
if _, exists := decoded.Platform["future-platform"]; exists {
|
||||||
|
t.Fatalf("future platform was preserved, want fallback")
|
||||||
|
}
|
||||||
|
if !decoded.Platform[PlatformNone].PlatformFee.Equal(original.Platform["future-platform"].PlatformFee) {
|
||||||
|
t.Fatalf("PlatformNone fee = %s, want %s", decoded.Platform[PlatformNone].PlatformFee, original.Platform["future-platform"].PlatformFee)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(decoded.MevAgent) != 1 {
|
||||||
|
t.Fatalf("MevAgent len = %d, want 1", len(decoded.MevAgent))
|
||||||
|
}
|
||||||
|
if _, exists := decoded.MevAgent["future-mev-agent"]; exists {
|
||||||
|
t.Fatalf("future mev agent was preserved, want fallback")
|
||||||
|
}
|
||||||
|
if !decoded.MevAgent[MevAgentUnknown].MevAgentFee.Equal(original.MevAgent["future-mev-agent"].MevAgentFee) {
|
||||||
|
t.Fatalf("MevAgentUnknown fee = %s, want %s", decoded.MevAgent[MevAgentUnknown].MevAgentFee, original.MevAgent["future-mev-agent"].MevAgentFee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTxBinaryReadLabelEnumUnknownIDsFallback(t *testing.T) {
|
||||||
|
enumTable := txBinaryEnumTables[txBinaryEnumVersionV1]
|
||||||
|
|
||||||
|
platformFee := uint64(123)
|
||||||
|
platformEnc := txBinaryEncoder{}
|
||||||
|
platformEnc.writeUint32(1)
|
||||||
|
platformEnc.writeUint16(uint16(len(enumTable.platforms.values) + 10))
|
||||||
|
platformEnc.writeUint64(platformFee)
|
||||||
|
|
||||||
|
platformDec := txBinaryDecoder{reader: bytes.NewReader(platformEnc.bytes())}
|
||||||
|
platforms, err := txBinaryReadPlatformEntries(&platformDec, enumTable)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("txBinaryReadPlatformEntries() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(platforms) != 1 || platforms[0].Platform != PlatformNone || platforms[0].PlatformFee != platformFee {
|
||||||
|
t.Fatalf("platform fallback = %+v, want %s/%d", platforms, PlatformNone, platformFee)
|
||||||
|
}
|
||||||
|
|
||||||
|
mevFee := uint64(456)
|
||||||
|
mevEnc := txBinaryEncoder{}
|
||||||
|
mevEnc.writeUint32(1)
|
||||||
|
mevEnc.writeUint16(uint16(len(enumTable.mevAgents.values) + 10))
|
||||||
|
mevEnc.writeUint64(mevFee)
|
||||||
|
|
||||||
|
mevDec := txBinaryDecoder{reader: bytes.NewReader(mevEnc.bytes())}
|
||||||
|
mevAgents, err := txBinaryReadMevAgentEntries(&mevDec, enumTable)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("txBinaryReadMevAgentEntries() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(mevAgents) != 1 || mevAgents[0].MevAgent != MevAgentUnknown || mevAgents[0].MevAgentFee != mevFee {
|
||||||
|
t.Fatalf("mev agent fallback = %+v, want %s/%d", mevAgents, MevAgentUnknown, mevFee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTxBinaryAcceptsKnownEventEnums(t *testing.T) {
|
||||||
|
events := []string{
|
||||||
|
TxEventAddLP,
|
||||||
|
TxEventRemoveLP,
|
||||||
|
TxEventBuy,
|
||||||
|
TxEventSell,
|
||||||
|
TxEventBuyFailed,
|
||||||
|
TxEventSellFailed,
|
||||||
|
TxEventBurn,
|
||||||
|
TxEventCreate,
|
||||||
|
TxEventComplete,
|
||||||
|
TxEventMigrate,
|
||||||
|
TxEventDeposit,
|
||||||
|
TxEventWithdraw,
|
||||||
|
TxEventOpen,
|
||||||
|
TxEventClose,
|
||||||
|
TxEventClaimFee,
|
||||||
|
TxEventAddLiquidity,
|
||||||
|
TxEventAddLiquidityOneSide,
|
||||||
|
TxEventRemoveLiquidity,
|
||||||
|
TxEventRemoveLiquidityOneSide,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, event := range events {
|
||||||
|
t.Run(event, func(t *testing.T) {
|
||||||
|
txBinary := &TxBinary{
|
||||||
|
SchemaVersion: txBinarySchemaVersionCurrent,
|
||||||
|
EnumVersion: txBinaryEnumVersionV1,
|
||||||
|
AddressTable: []solana.PublicKey{
|
||||||
|
mustPubKey("11111111111111111111111111111111"),
|
||||||
|
mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
solana.TokenProgramID,
|
||||||
|
mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||||
|
mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
|
},
|
||||||
|
Swaps: []SwapBinary{
|
||||||
|
{
|
||||||
|
Program: SolProgramPump,
|
||||||
|
Event: event,
|
||||||
|
Pool: 0,
|
||||||
|
BaseMint: 1,
|
||||||
|
QuoteMint: 1,
|
||||||
|
BaseTokenProgram: 2,
|
||||||
|
QuoteTokenProgram: 2,
|
||||||
|
Creator: 3,
|
||||||
|
User: 4,
|
||||||
|
FixedMint: 1,
|
||||||
|
LimitMint: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := txBinary.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MarshalBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded TxBinary
|
||||||
|
if err := decoded.UnmarshalBinary(encoded); err != nil {
|
||||||
|
t.Fatalf("UnmarshalBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
if got := decoded.Swaps[0].Event; got != event {
|
||||||
|
t.Fatalf("decoded event = %q, want %q", got, event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTxBinaryPreservesFractionalReserves(t *testing.T) {
|
||||||
|
tx := &Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 1,
|
||||||
|
BlockIndex: 1,
|
||||||
|
CuFee: decimal.NewFromInt(1),
|
||||||
|
CUPrice: decimal.RequireFromString("0.000001"),
|
||||||
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
|
||||||
|
ComputeUnitsConsumed: 1,
|
||||||
|
CuLimit: 1,
|
||||||
|
Swaps: []Swap{
|
||||||
|
{
|
||||||
|
Program: SolProgramMeteoraPools,
|
||||||
|
Event: TxEventAddLiquidity,
|
||||||
|
Pool: mustPubKey("11111111111111111111111111111111"),
|
||||||
|
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
QuoteMint: solana.WrappedSol,
|
||||||
|
BaseTokenProgram: solana.TokenProgramID,
|
||||||
|
QuoteTokenProgram: solana.TokenProgramID,
|
||||||
|
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||||
|
BaseMintDecimals: 6,
|
||||||
|
QuoteMintDecimals: 9,
|
||||||
|
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
|
BaseAmount: decimal.NewFromInt(10),
|
||||||
|
QuoteAmount: decimal.NewFromInt(20),
|
||||||
|
SwapMode: SwapModeExactIn,
|
||||||
|
FixedAmount: decimal.NewFromInt(20),
|
||||||
|
FixedAmountSide: SwapAmountSideQuote,
|
||||||
|
FixedMint: solana.WrappedSol,
|
||||||
|
LimitAmountType: SwapLimitTypeMinOut,
|
||||||
|
LimitAmount: decimal.NewFromInt(9),
|
||||||
|
LimitAmountSide: SwapAmountSideBase,
|
||||||
|
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
ActualLimitAmount: decimal.NewFromInt(10),
|
||||||
|
ActualLimitAmountSide: SwapAmountSideBase,
|
||||||
|
BaseReserve: decimal.RequireFromString("123.4"),
|
||||||
|
QuoteReserve: decimal.RequireFromString("710079483.625409498"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := EncodeTxBinary(tx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := DecodeTxBinary(encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
if got := decoded.Swaps[0].BaseReserve.String(); got != "123.4" {
|
||||||
|
t.Fatalf("BaseReserve = %s, want 123.4", got)
|
||||||
|
}
|
||||||
|
diff := decoded.Swaps[0].QuoteReserve.Sub(decimal.RequireFromString("710079483.625409498")).Abs()
|
||||||
|
if diff.GreaterThan(decimal.RequireFromString("0.0000001")) {
|
||||||
|
t.Fatalf("QuoteReserve diff = %s, want <= 0.0000001", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTxBinaryCanonicalizesOnSideEventAlias(t *testing.T) {
|
||||||
|
tx := &Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 1,
|
||||||
|
BlockIndex: 1,
|
||||||
|
CuFee: decimal.NewFromInt(1),
|
||||||
|
CUPrice: decimal.RequireFromString("0.000001"),
|
||||||
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
|
||||||
|
ComputeUnitsConsumed: 1,
|
||||||
|
CuLimit: 1,
|
||||||
|
Swaps: []Swap{
|
||||||
|
{
|
||||||
|
Program: SolProgramOrcaWhirPool,
|
||||||
|
Event: "remove_liquidity_on_side",
|
||||||
|
Pool: mustPubKey("11111111111111111111111111111111"),
|
||||||
|
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
QuoteMint: solana.WrappedSol,
|
||||||
|
BaseTokenProgram: solana.TokenProgramID,
|
||||||
|
QuoteTokenProgram: solana.TokenProgramID,
|
||||||
|
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||||
|
BaseMintDecimals: 6,
|
||||||
|
QuoteMintDecimals: 9,
|
||||||
|
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
|
BaseAmount: decimal.NewFromInt(10),
|
||||||
|
QuoteAmount: decimal.Zero,
|
||||||
|
SwapMode: SwapModeExactIn,
|
||||||
|
FixedAmount: decimal.NewFromInt(10),
|
||||||
|
FixedAmountSide: SwapAmountSideBase,
|
||||||
|
FixedMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
LimitAmountType: SwapLimitTypeMinOut,
|
||||||
|
LimitAmount: decimal.Zero,
|
||||||
|
LimitAmountSide: SwapAmountSideQuote,
|
||||||
|
ActualLimitAmount: decimal.Zero,
|
||||||
|
ActualLimitAmountSide: SwapAmountSideQuote,
|
||||||
|
BaseReserve: decimal.RequireFromString("123.4"),
|
||||||
|
QuoteReserve: decimal.RequireFromString("456.7"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := EncodeTxBinary(tx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := DecodeTxBinary(encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
if got := decoded.Swaps[0].Event; got != TxEventRemoveLiquidityOneSide {
|
||||||
|
t.Fatalf("Event = %q, want %q", got, TxEventRemoveLiquidityOneSide)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
|
func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
|
||||||
tx1 := Tx{
|
tx1 := Tx{
|
||||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
Block: 1,
|
Block: 1,
|
||||||
BlockIndex: 1,
|
BlockIndex: 1,
|
||||||
|
BlockAt: 1710000001,
|
||||||
CuFee: decimal.NewFromInt(1000),
|
CuFee: decimal.NewFromInt(1000),
|
||||||
CUPrice: decimal.RequireFromString("0.123456"),
|
CUPrice: decimal.RequireFromString("0.123456"),
|
||||||
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
@@ -280,6 +565,7 @@ func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
|
|||||||
tx2 := tx1
|
tx2 := tx1
|
||||||
tx2.Block = 2
|
tx2.Block = 2
|
||||||
tx2.BlockIndex = 2
|
tx2.BlockIndex = 2
|
||||||
|
tx2.BlockAt = 1710000002
|
||||||
tx2.CuFee = decimal.NewFromInt(2000)
|
tx2.CuFee = decimal.NewFromInt(2000)
|
||||||
tx2.AfterSOLBalance = decimal.RequireFromString("0.700000000")
|
tx2.AfterSOLBalance = decimal.RequireFromString("0.700000000")
|
||||||
tx2.Swaps = []Swap{tx1.Swaps[0]}
|
tx2.Swaps = []Swap{tx1.Swaps[0]}
|
||||||
@@ -301,6 +587,9 @@ func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
|
|||||||
if decoded[0].Signer != tx1.Signer || decoded[1].Signer != tx2.Signer {
|
if decoded[0].Signer != tx1.Signer || decoded[1].Signer != tx2.Signer {
|
||||||
t.Fatalf("decoded signer mismatch")
|
t.Fatalf("decoded signer mismatch")
|
||||||
}
|
}
|
||||||
|
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt {
|
||||||
|
t.Fatalf("decoded block_at mismatch")
|
||||||
|
}
|
||||||
if decoded[0].Swaps[0].Pool != tx1.Swaps[0].Pool || decoded[1].Swaps[0].Pool != tx2.Swaps[0].Pool {
|
if decoded[0].Swaps[0].Pool != tx1.Swaps[0].Pool || decoded[1].Swaps[0].Pool != tx2.Swaps[0].Pool {
|
||||||
t.Fatalf("decoded shared address mismatch")
|
t.Fatalf("decoded shared address mismatch")
|
||||||
}
|
}
|
||||||
@@ -323,6 +612,7 @@ func TestDecodeTxsBinaryReader(t *testing.T) {
|
|||||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
Block: 100,
|
Block: 100,
|
||||||
BlockIndex: 7,
|
BlockIndex: 7,
|
||||||
|
BlockAt: 1710000100,
|
||||||
CuFee: decimal.NewFromInt(111),
|
CuFee: decimal.NewFromInt(111),
|
||||||
CUPrice: decimal.RequireFromString("0.123456"),
|
CUPrice: decimal.RequireFromString("0.123456"),
|
||||||
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
@@ -369,6 +659,7 @@ func TestDecodeTxsBinaryReader(t *testing.T) {
|
|||||||
tx2 := tx1
|
tx2 := tx1
|
||||||
tx2.Block = 101
|
tx2.Block = 101
|
||||||
tx2.BlockIndex = 8
|
tx2.BlockIndex = 8
|
||||||
|
tx2.BlockAt = 1710000101
|
||||||
tx2.CuFee = decimal.NewFromInt(222)
|
tx2.CuFee = decimal.NewFromInt(222)
|
||||||
tx2.AfterSOLBalance = decimal.RequireFromString("0.300000000")
|
tx2.AfterSOLBalance = decimal.RequireFromString("0.300000000")
|
||||||
tx2.Swaps = []Swap{tx1.Swaps[0]}
|
tx2.Swaps = []Swap{tx1.Swaps[0]}
|
||||||
@@ -397,6 +688,9 @@ func TestDecodeTxsBinaryReader(t *testing.T) {
|
|||||||
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
|
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
|
||||||
t.Fatalf("decoded block mismatch")
|
t.Fatalf("decoded block mismatch")
|
||||||
}
|
}
|
||||||
|
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt {
|
||||||
|
t.Fatalf("decoded block_at mismatch")
|
||||||
|
}
|
||||||
if decoded[0].Swaps[0].BaseAmount.Cmp(tx1.Swaps[0].BaseAmount) != 0 {
|
if decoded[0].Swaps[0].BaseAmount.Cmp(tx1.Swaps[0].BaseAmount) != 0 {
|
||||||
t.Fatalf("decoded tx1 swap base amount = %s, want %s", decoded[0].Swaps[0].BaseAmount, tx1.Swaps[0].BaseAmount)
|
t.Fatalf("decoded tx1 swap base amount = %s, want %s", decoded[0].Swaps[0].BaseAmount, tx1.Swaps[0].BaseAmount)
|
||||||
}
|
}
|
||||||
@@ -444,6 +738,7 @@ func TestMergeTxsBinaryBytes(t *testing.T) {
|
|||||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
Block: 11,
|
Block: 11,
|
||||||
BlockIndex: 1,
|
BlockIndex: 1,
|
||||||
|
BlockAt: 1710000011,
|
||||||
CuFee: decimal.NewFromInt(10),
|
CuFee: decimal.NewFromInt(10),
|
||||||
CUPrice: decimal.RequireFromString("0.000123"),
|
CUPrice: decimal.RequireFromString("0.000123"),
|
||||||
BeforeSolBalance: decimal.RequireFromString("1.100000000"),
|
BeforeSolBalance: decimal.RequireFromString("1.100000000"),
|
||||||
@@ -475,6 +770,7 @@ func TestMergeTxsBinaryBytes(t *testing.T) {
|
|||||||
Signer: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
Signer: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
Block: 12,
|
Block: 12,
|
||||||
BlockIndex: 2,
|
BlockIndex: 2,
|
||||||
|
BlockAt: 1710000012,
|
||||||
CuFee: decimal.NewFromInt(20),
|
CuFee: decimal.NewFromInt(20),
|
||||||
CUPrice: decimal.RequireFromString("0.000456"),
|
CUPrice: decimal.RequireFromString("0.000456"),
|
||||||
BeforeSolBalance: decimal.RequireFromString("2.200000000"),
|
BeforeSolBalance: decimal.RequireFromString("2.200000000"),
|
||||||
@@ -538,6 +834,9 @@ func TestMergeTxsBinaryBytes(t *testing.T) {
|
|||||||
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
|
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
|
||||||
t.Fatalf("decoded block mismatch")
|
t.Fatalf("decoded block mismatch")
|
||||||
}
|
}
|
||||||
|
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt {
|
||||||
|
t.Fatalf("decoded block_at mismatch")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
|
func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
|
||||||
@@ -545,6 +844,7 @@ func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
|
|||||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
Block: 21,
|
Block: 21,
|
||||||
BlockIndex: 1,
|
BlockIndex: 1,
|
||||||
|
BlockAt: 1710000021,
|
||||||
CuFee: decimal.NewFromInt(1),
|
CuFee: decimal.NewFromInt(1),
|
||||||
CUPrice: decimal.RequireFromString("0.000001"),
|
CUPrice: decimal.RequireFromString("0.000001"),
|
||||||
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
@@ -555,9 +855,11 @@ func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
|
|||||||
tx2 := tx1
|
tx2 := tx1
|
||||||
tx2.Block = 22
|
tx2.Block = 22
|
||||||
tx2.BlockIndex = 2
|
tx2.BlockIndex = 2
|
||||||
|
tx2.BlockAt = 1710000022
|
||||||
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
|
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
|
||||||
tx3 := tx1
|
tx3 := tx1
|
||||||
tx3.Block = 23
|
tx3.Block = 23
|
||||||
|
tx3.BlockAt = 1710000023
|
||||||
tx3.BlockIndex = 3
|
tx3.BlockIndex = 3
|
||||||
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
|
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
|
||||||
|
|
||||||
@@ -600,12 +902,213 @@ func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
|
|||||||
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block || decoded[2].Block != tx3.Block {
|
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block || decoded[2].Block != tx3.Block {
|
||||||
t.Fatalf("decoded block order mismatch")
|
t.Fatalf("decoded block order mismatch")
|
||||||
}
|
}
|
||||||
|
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt || decoded[2].BlockAt != tx3.BlockAt {
|
||||||
|
t.Fatalf("decoded block_at order mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) {
|
||||||
|
tx1 := Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 31,
|
||||||
|
BlockIndex: 1,
|
||||||
|
BlockAt: 1710000031,
|
||||||
|
CuFee: decimal.NewFromInt(1),
|
||||||
|
CUPrice: decimal.RequireFromString("0.000001"),
|
||||||
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
|
||||||
|
ComputeUnitsConsumed: 11,
|
||||||
|
CuLimit: 111,
|
||||||
|
}
|
||||||
|
tx2 := tx1
|
||||||
|
tx2.Block = 32
|
||||||
|
tx2.BlockIndex = 2
|
||||||
|
tx2.BlockAt = 1710000032
|
||||||
|
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
|
||||||
|
tx3 := tx1
|
||||||
|
tx3.Block = 33
|
||||||
|
tx3.BlockIndex = 3
|
||||||
|
tx3.BlockAt = 1710000033
|
||||||
|
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
|
||||||
|
|
||||||
|
batch1, err := EncodeTxsBinary([]Tx{tx1})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary(batch1) error = %v", err)
|
||||||
|
}
|
||||||
|
batch2, err := EncodeTxsBinary([]Tx{tx2})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary(batch2) error = %v", err)
|
||||||
|
}
|
||||||
|
batch3, err := EncodeTxsBinary([]Tx{tx3})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary(batch3) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
source := &testTxsBinarySource{
|
||||||
|
data: append(
|
||||||
|
append(
|
||||||
|
append([]byte{}, testBatchHeader(false)...),
|
||||||
|
batch1...,
|
||||||
|
),
|
||||||
|
append(
|
||||||
|
append(testBatchHeader(true), batch2...),
|
||||||
|
append(testBatchHeader(false), batch3...)...,
|
||||||
|
)...,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
err = MergeTxsBinarySourcesToWriterWithOptions(
|
||||||
|
[]TxsBinaryReaderSource{source},
|
||||||
|
&out,
|
||||||
|
TxsBinaryMergeOptions{
|
||||||
|
BatchHeaderFunc: func(ctx *TxsBinaryBatchHeaderContext) (bool, error) {
|
||||||
|
header := make([]byte, 5)
|
||||||
|
if _, err := io.ReadFull(ctx.Reader, header); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(header[:4], []byte("BHDR")) {
|
||||||
|
return false, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
return header[4] == 1, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MergeTxsBinarySourcesToWriterWithOptions() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := DecodeTxsBinary(out.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxsBinary(merged) error = %v", err)
|
||||||
|
}
|
||||||
|
if len(decoded) != 2 {
|
||||||
|
t.Fatalf("decoded len = %d, want 2", len(decoded))
|
||||||
|
}
|
||||||
|
if decoded[0].Block != tx1.Block || decoded[1].Block != tx3.Block {
|
||||||
|
t.Fatalf("decoded block order mismatch after skip")
|
||||||
|
}
|
||||||
|
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx3.BlockAt {
|
||||||
|
t.Fatalf("decoded block_at order mismatch after skip")
|
||||||
|
}
|
||||||
|
if source.opens != 2 {
|
||||||
|
t.Fatalf("source.opens = %d, want 2", source.opens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTxBinaryDecodeSchemaV3LeavesBlockAtZero(t *testing.T) {
|
||||||
|
original := &Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 41,
|
||||||
|
BlockIndex: 1,
|
||||||
|
BlockAt: 1710000041,
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded := mustEncodeTxBinaryV3(t, original)
|
||||||
|
decoded, err := DecodeTxBinary(encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxBinary(v3) error = %v", err)
|
||||||
|
}
|
||||||
|
if decoded.Block != original.Block || decoded.BlockIndex != original.BlockIndex {
|
||||||
|
t.Fatalf("decoded block mismatch: got (%d,%d), want (%d,%d)", decoded.Block, decoded.BlockIndex, original.Block, original.BlockIndex)
|
||||||
|
}
|
||||||
|
if decoded.BlockAt != 0 {
|
||||||
|
t.Fatalf("BlockAt = %d, want 0 for legacy v3", decoded.BlockAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeTxsBinaryBytesUpgradesSchemaV3AndPreservesV4BlockAt(t *testing.T) {
|
||||||
|
legacyTx := Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 51,
|
||||||
|
BlockIndex: 1,
|
||||||
|
BlockAt: 1710000051,
|
||||||
|
}
|
||||||
|
currentTx := Tx{
|
||||||
|
Signer: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
|
Block: 52,
|
||||||
|
BlockIndex: 2,
|
||||||
|
BlockAt: 1710000052,
|
||||||
|
}
|
||||||
|
|
||||||
|
merged, err := MergeTxsBinaryBytes([][]byte{
|
||||||
|
mustEncodeTxsBinaryV3(t, []Tx{legacyTx}),
|
||||||
|
mustEncodeTxsBinary(t, []Tx{currentTx}),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MergeTxsBinaryBytes(v3,v4) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mergedBinary TxsBinary
|
||||||
|
if err := mergedBinary.UnmarshalBinary(merged); err != nil {
|
||||||
|
t.Fatalf("UnmarshalBinary(merged) error = %v", err)
|
||||||
|
}
|
||||||
|
if mergedBinary.SchemaVersion != txBinarySchemaVersionCurrent {
|
||||||
|
t.Fatalf("merged schema version = %d, want %d", mergedBinary.SchemaVersion, txBinarySchemaVersionCurrent)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := DecodeTxsBinary(merged)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxsBinary(merged) error = %v", err)
|
||||||
|
}
|
||||||
|
if len(decoded) != 2 {
|
||||||
|
t.Fatalf("decoded len = %d, want 2", len(decoded))
|
||||||
|
}
|
||||||
|
if decoded[0].BlockAt != 0 {
|
||||||
|
t.Fatalf("legacy BlockAt = %d, want 0", decoded[0].BlockAt)
|
||||||
|
}
|
||||||
|
if decoded[1].BlockAt != currentTx.BlockAt {
|
||||||
|
t.Fatalf("current BlockAt = %d, want %d", decoded[1].BlockAt, currentTx.BlockAt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustPubKey(value string) solana.PublicKey {
|
func mustPubKey(value string) solana.PublicKey {
|
||||||
return solana.MustPublicKeyFromBase58(value)
|
return solana.MustPublicKeyFromBase58(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustEncodeTxBinaryV3(t *testing.T, tx *Tx) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
binaryTx, err := NewTxBinary(tx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
binaryTx.SchemaVersion = txBinarySchemaVersionV3
|
||||||
|
encoded, err := binaryTx.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MarshalBinary(v3) error = %v", err)
|
||||||
|
}
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustEncodeTxsBinary(t *testing.T, txs []Tx) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
encoded, err := EncodeTxsBinary(txs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustEncodeTxsBinaryV3(t *testing.T, txs []Tx) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
binaryTxs, err := NewTxsBinary(txs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewTxsBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
binaryTxs.SchemaVersion = txBinarySchemaVersionV3
|
||||||
|
for i := range binaryTxs.Txs {
|
||||||
|
binaryTxs.Txs[i].SchemaVersion = txBinarySchemaVersionV3
|
||||||
|
}
|
||||||
|
encoded, err := binaryTxs.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MarshalBinary(v3) error = %v", err)
|
||||||
|
}
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
func mustTxBinary(t *testing.T, data []byte) *TxsBinary {
|
func mustTxBinary(t *testing.T, data []byte) *TxsBinary {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -625,3 +1128,11 @@ func (s *testTxsBinarySource) OpenTxsBinaryReader() (io.ReadCloser, error) {
|
|||||||
s.opens++
|
s.opens++
|
||||||
return io.NopCloser(bytes.NewReader(s.data)), nil
|
return io.NopCloser(bytes.NewReader(s.data)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testBatchHeader(skip bool) []byte {
|
||||||
|
header := []byte("BHDR\x00")
|
||||||
|
if skip {
|
||||||
|
header[4] = 1
|
||||||
|
}
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user