Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc03798ff6 | ||
|
|
44eecac087 | ||
|
|
9f17ffce61 | ||
|
|
e4eaddec4e | ||
|
|
9454c3f6c7 | ||
|
|
39bfeb085f | ||
|
|
10885d5e08 | ||
|
|
2406f6d087 | ||
|
|
8b608889cb | ||
|
|
8d4aad1932 | ||
|
|
5cd3a97d81 | ||
|
|
0a4aabc67f | ||
|
|
d46e8b651c |
249
README.md
Normal file
249
README.md
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
# pump-parser
|
||||||
|
|
||||||
|
Solana transaction parser focused on swap, liquidity, migration, platform, MEV, compute budget, and compact binary persistence workflows.
|
||||||
|
|
||||||
|
The package works with a normalized `RawTx` representation, parses it into `Tx`, and emits one or more `Swap` records when a supported protocol action is found.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Parse Solana RPC / Yellowstone transactions into local `RawTx`.
|
||||||
|
- Extract swap and liquidity events into `Tx.Swaps`.
|
||||||
|
- Preserve transaction metadata such as slot, block time, signer, fee, CU limit, CU consumed, token balances, platform fees, and MEV agent hints.
|
||||||
|
- Encode/decode parsed transactions with the `PTXB` / `PTXS` binary formats.
|
||||||
|
- Encode/decode raw transactions with the `PRTX` / `PRTS` / `PRBS` binary formats.
|
||||||
|
- Stream decode large `PTXS` / `PRTS` payloads without loading every transaction into memory.
|
||||||
|
- Merge `PTXS` batches while remapping address tables.
|
||||||
|
|
||||||
|
## Supported Parsers
|
||||||
|
|
||||||
|
Default parser initialization enables the hot-path Pump parsers only:
|
||||||
|
|
||||||
|
- Pump
|
||||||
|
- Pump AMM
|
||||||
|
|
||||||
|
`EnableAllParsers()` additionally enables:
|
||||||
|
|
||||||
|
- Meteora DLMM
|
||||||
|
- Meteora Pools
|
||||||
|
- Meteora DAMM v2
|
||||||
|
- Meteora Bonding Curve
|
||||||
|
- Orca Whirlpool
|
||||||
|
- Raydium AMM v4
|
||||||
|
- Raydium CLMM
|
||||||
|
- Raydium CPMM
|
||||||
|
- Raydium LaunchLab
|
||||||
|
|
||||||
|
Use `InitParser(WithMeteoraDlmm())` when only Meteora DLMM should be added to the default parser set.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/thloyi/pump-parser
|
||||||
|
```
|
||||||
|
|
||||||
|
This module currently declares `go 1.25.1` in `go.mod`.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
pump_parser "github.com/thloyi/pump-parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Enable all known parser programs. Omit this call to keep the default
|
||||||
|
// Pump + Pump AMM parser set.
|
||||||
|
pump_parser.EnableAllParsers()
|
||||||
|
|
||||||
|
var rawTx *pump_parser.RawTx
|
||||||
|
// Fill rawTx from RPC, Yellowstone, JSON, or RawTx binary decoding.
|
||||||
|
|
||||||
|
tx, err := pump_parser.ParseRawTx(rawTx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("tx:", tx.GetTxHash())
|
||||||
|
for _, swap := range tx.Swaps {
|
||||||
|
fmt.Printf("%s %s base=%s quote=%s pool=%s user=%s\n",
|
||||||
|
swap.Program,
|
||||||
|
swap.Event,
|
||||||
|
swap.BaseAmount,
|
||||||
|
swap.QuoteAmount,
|
||||||
|
swap.Pool,
|
||||||
|
swap.User,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Swap Result
|
||||||
|
|
||||||
|
`ParseRawTx` returns a `Tx`. A transaction can contain zero, one, or multiple parsed swap records in `Tx.Swaps`.
|
||||||
|
|
||||||
|
Each `Swap` describes one protocol-level swap, liquidity, migration, or pool operation that the parser can normalize. The most important fields are:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `Program` | Normalized protocol name, such as `Pump`, `PumpAMM`, `MeteoraDLMM`, `RaydiumV4`, or `OrcaWhirPool`. |
|
||||||
|
| `Event` | Normalized action, such as `buy`, `sell`, `add_liquidity`, `remove_liquidity`, `create`, `complete`, or `migrate`. |
|
||||||
|
| `TxIndex` | Approximate execution order across outer and inner instructions. |
|
||||||
|
| `InstrIdx` / `InnerIdx` | Outer instruction index and inner instruction index where the swap was found. |
|
||||||
|
| `Pool` | Pool, pair, bonding curve, or market account for the parsed action. |
|
||||||
|
| `BaseMint` / `QuoteMint` | Base and quote mint accounts after parser normalization. |
|
||||||
|
| `BaseTokenProgram` / `QuoteTokenProgram` | Token program for each side, useful when Token-2022 is involved. |
|
||||||
|
| `BaseMintDecimals` / `QuoteMintDecimals` | Decimals used to interpret raw token amounts. |
|
||||||
|
| `User` | User or effective owner account for the action. If the parsed user is not on-curve, the parser may fall back to the transaction signer. |
|
||||||
|
| `BaseAmount` / `QuoteAmount` | Actual parsed base-side and quote-side amounts, stored as `decimal.Decimal`. |
|
||||||
|
| `BaseReserve` / `QuoteReserve` | Pool reserves when the protocol event or accounts expose them. |
|
||||||
|
| `UserBaseBalance` / `UserQuoteBalance` | User token balances after the transaction when available from token balance metadata. |
|
||||||
|
| `AfterSOLBalance` | User or signer SOL balance after the transaction. |
|
||||||
|
| `EntryContract` | Known router / entry contract account when detected. |
|
||||||
|
| `Mayhem` / `Cashback` | Protocol or platform-specific labels detected from the transaction path. |
|
||||||
|
|
||||||
|
Swap amount and slippage fields normalize instruction intent:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `SwapMode` | `exact_in`, `exact_out`, or empty when unknown. |
|
||||||
|
| `FixedAmount` / `FixedAmountSide` / `FixedMint` | The user-specified fixed side of the swap. For `exact_in`, this is the input amount. For `exact_out`, this is the target output amount. |
|
||||||
|
| `LimitAmountType` | `min_out` for `exact_in`, `max_in` for `exact_out`, or empty when unknown. |
|
||||||
|
| `LimitAmount` / `LimitAmountSide` / `LimitMint` | User-specified limit on the opposite side. |
|
||||||
|
| `ActualLimitAmount` / `ActualLimitAmountSide` | Actual executed amount on the limited side. |
|
||||||
|
| `SlippageBps` | Remaining headroom to the user's limit in basis points. See `SLIPPAGE_MAPPING.md` for protocol-specific derivation rules. |
|
||||||
|
|
||||||
|
Liquidity, migration, and DLMM-specific fields are populated only for protocols that expose them:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `Creator` | Creator account when exposed by pool or launch events. |
|
||||||
|
| `MigrateToPool` / `MigrateTopProgram` | Destination pool and program for migration events. |
|
||||||
|
| `LpMint` | LP token mint for liquidity operations when available. |
|
||||||
|
| `ActiveBinId` / `StartBinId` / `EndBinId` | Meteora DLMM bin identifiers. |
|
||||||
|
| `RemoveBp` | DLMM remove-liquidity basis points. |
|
||||||
|
| `PositionAccount` | DLMM position account. |
|
||||||
|
| `FeeAmount` / `LpFeeAmount` | Parsed fee amounts when the protocol exposes fee breakdowns. |
|
||||||
|
| `FeeSide` / `FeeMint` / `FeeTokenProgram` / `FeeMintDecimals` | Fee side and mint metadata. |
|
||||||
|
| `ConsumeUnit` | Per-swap compute unit value when available. |
|
||||||
|
|
||||||
|
Amounts are normalized into decimals in parser output, but the exact scale depends on the source event and parser path. For persisted binary output, `tx_binary.go` defines the conversion rules and schema version.
|
||||||
|
|
||||||
|
## Convert Transactions
|
||||||
|
|
||||||
|
RPC transactions can be converted with `FromRpcTransactionWithMeta`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
rawTx, err := pump_parser.FromRpcTransactionWithMeta(
|
||||||
|
txWithMeta,
|
||||||
|
blockTime,
|
||||||
|
slot,
|
||||||
|
indexWithinBlock,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Yellowstone transactions can be converted with `ConvertYellowstoneGrpcTransactionToSolanaTransaction`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
rawTx, err := pump_parser.ConvertYellowstoneGrpcTransactionToSolanaTransaction(
|
||||||
|
update,
|
||||||
|
createdUnix,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Both conversion paths accept optional `RawTxConvertOptions`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
rawTx, err := pump_parser.FromRpcTransactionWithMeta(
|
||||||
|
txWithMeta,
|
||||||
|
blockTime,
|
||||||
|
slot,
|
||||||
|
indexWithinBlock,
|
||||||
|
pump_parser.RawTxConvertOptions{
|
||||||
|
ParseLogEvents: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ParseLogEvents` attaches decoded program log events to matching instructions. `IgnoreLogMessages` skips log-message retention.
|
||||||
|
|
||||||
|
## Binary Formats
|
||||||
|
|
||||||
|
Parsed transaction binary:
|
||||||
|
|
||||||
|
- `EncodeTxBinary` / `DecodeTxBinary` for one parsed `Tx`.
|
||||||
|
- `EncodeTxsBinary` / `DecodeTxsBinary` for a batch of parsed `Tx`.
|
||||||
|
- `DecodeTxsBinaryReader` for streaming `PTXS` reads.
|
||||||
|
- `MergeTxsBinaryBytes` and `MergeTxsBinarySourcesToWriter` for merging `PTXS` batches.
|
||||||
|
|
||||||
|
Raw transaction binary:
|
||||||
|
|
||||||
|
- `EncodeRawTxBinary` / `DecodeRawTxBinary` for one `RawTx`.
|
||||||
|
- `EncodeRawTxsBinary` / `DecodeRawTxsBinary` for a batch of `RawTx`.
|
||||||
|
- `EncodeRawTxBlocksBinary` / `DecodeRawTxBlocksBinary` for grouped block data.
|
||||||
|
- `DecodeRawTxsBinaryReader` for streaming `PRTS` reads.
|
||||||
|
|
||||||
|
Format magic values:
|
||||||
|
|
||||||
|
- `PTXB`: one parsed `Tx`.
|
||||||
|
- `PTXS`: parsed `Tx` batch.
|
||||||
|
- `PRTX`: one raw `RawTx`.
|
||||||
|
- `PRTS`: raw `RawTx` batch.
|
||||||
|
- `PRBS`: raw block-grouped `RawTx` batch.
|
||||||
|
|
||||||
|
When adding or renaming transaction-facing enum values, update `tx_binary.go` enum tables by appending new values only. Reordering existing values changes persisted numeric IDs.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Parse a live transaction by signature:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TX_HASH=<signature> go run ./cmd/rpc_parse
|
||||||
|
```
|
||||||
|
|
||||||
|
Collect Yellowstone transactions into a `.prbs` raw-block binary file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
YELLOWSTONE_X_TOKEN=<token> go run ./cmd/collect_yellowstone_rawtx_binary \
|
||||||
|
-endpoint ams.rpc.orbitflare.com:10000 \
|
||||||
|
-duration 5m \
|
||||||
|
-output testdata/rawtx-binary/sample.prbs
|
||||||
|
```
|
||||||
|
|
||||||
|
Measure parsed `Tx` binary size from a `getBlock` JSON payload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/measure_tx_binary_block \
|
||||||
|
-file /path/to/block.json \
|
||||||
|
-slot 413539056 \
|
||||||
|
-swaps-only
|
||||||
|
```
|
||||||
|
|
||||||
|
Analyze raw transaction binary size distribution:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/analyze_rawtx_binary_size \
|
||||||
|
-file testdata/rawtx-binary/sample.prbs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Run the full test suite:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful focused checks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -run 'TestTxBinary|TestRawTxBinary' .
|
||||||
|
go test -run 'TestMetaoraPoolSwapEventFromLogsUsesMatchingInvocation|TestMetaoraPoolSwapInstructionOccurrenceIncludesInnerInstructions|TestAttachLogEventsToInstructions' .
|
||||||
|
TX_HASH=<signature> go run ./cmd/rpc_parse
|
||||||
|
```
|
||||||
|
|
||||||
|
For parser regressions, reproduce with the exact transaction signature first, then add a targeted unit test or fixture around the corrected parser path.
|
||||||
@@ -25,6 +25,9 @@ func chainLinkParser(tx *Tx, instruction Instruction, inners InnerInstructions,
|
|||||||
}
|
}
|
||||||
|
|
||||||
decode := instruction.Data
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,15 +138,20 @@ func addInnerInstructions(stats *sizeStats, values []pump_parser.InnerInstructio
|
|||||||
func addInstructions(stats *sizeStats, prefix string, values []pump_parser.Instruction) {
|
func addInstructions(stats *sizeStats, prefix string, values []pump_parser.Instruction) {
|
||||||
stats.add(prefix+".count", 4)
|
stats.add(prefix+".count", 4)
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
stats.add(prefix+".program_id_index", 2)
|
stats.add(prefix+".program_id_index", 1)
|
||||||
stats.add(prefix+".accounts.count", 4)
|
stats.add(prefix+".accounts.count", 4)
|
||||||
stats.add(prefix+".accounts.refs", uint64(len(value.Accounts))*2)
|
stats.add(prefix+".accounts.refs", uint64(len(value.Accounts)))
|
||||||
stats.add(prefix+".data.length", 4)
|
stats.add(prefix+".data.length", 4)
|
||||||
stats.add(prefix+".data.bytes", uint64(len(value.Data)))
|
stats.add(prefix+".data.bytes", uint64(len(value.Data)))
|
||||||
stats.add(prefix+".stack_height.present", 1)
|
stats.add(prefix+".stack_height.present", 1)
|
||||||
if value.StackHeight != nil {
|
if value.StackHeight != nil {
|
||||||
stats.add(prefix+".stack_height.value", 4)
|
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)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,13 +199,13 @@ func addLamportBalances(stats *sizeStats, preBalances []uint64, postBalances []u
|
|||||||
func addTokenBalances(stats *sizeStats, prefix string, values []pump_parser.RawTxTokenBalanceBinary) {
|
func addTokenBalances(stats *sizeStats, prefix string, values []pump_parser.RawTxTokenBalanceBinary) {
|
||||||
stats.add(prefix+".count", 4)
|
stats.add(prefix+".count", 4)
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
stats.add(prefix+".account_index", 2)
|
stats.add(prefix+".account_index", 1)
|
||||||
stats.add(prefix+".mint_ref", 2)
|
stats.add(prefix+".mint_ref", 1)
|
||||||
stats.add(prefix+".owner.present", 1)
|
stats.add(prefix+".owner.present", 1)
|
||||||
if value.HasOwnerAccount {
|
if value.HasOwnerAccount {
|
||||||
stats.add(prefix+".owner_ref", 2)
|
stats.add(prefix+".owner_ref", 1)
|
||||||
}
|
}
|
||||||
stats.add(prefix+".program_id_ref", 2)
|
stats.add(prefix+".program_id_ref", 1)
|
||||||
stats.add(prefix+".decimals", 1)
|
stats.add(prefix+".decimals", 1)
|
||||||
stats.add(prefix+".pre_amount.present", 1)
|
stats.add(prefix+".pre_amount.present", 1)
|
||||||
if value.HasPreAmount {
|
if value.HasPreAmount {
|
||||||
|
|||||||
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{
|
||||||
|
|||||||
2
enum.go
2
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 (
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func main() {
|
|||||||
// laserstream-mainnet-slc.helius-rpc.com:80
|
// laserstream-mainnet-slc.helius-rpc.com:80
|
||||||
|
|
||||||
ch := make(chan example.SubscriptionMessage, 1)
|
ch := make(chan example.SubscriptionMessage, 1)
|
||||||
go example.RunLoopWithReConnect(context.Background(), "", "", parser.SolProgramPump, ch)
|
go example.RunLoopWithReConnect(context.Background(), "ams.rpc.orbitflare.com:10000", "ORBIT-EPUZGQ-177605-508881", parser.SolProgramPump, ch)
|
||||||
// var tokenTxs = make(map[string]*types.Tx)
|
// var tokenTxs = make(map[string]*types.Tx)
|
||||||
// currentBlock := uint64(0)
|
// currentBlock := uint64(0)
|
||||||
for msg := range ch {
|
for msg := range ch {
|
||||||
|
|||||||
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 (
|
||||||
|
|||||||
201
metaorapool.go
201
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)
|
||||||
@@ -886,10 +900,195 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
swaps[0].SetSwapAmountInfo(
|
fixedSide := fixedSwapAmountSide(event, SwapModeExactIn)
|
||||||
|
limitSide := oppositeSwapAmountSide(fixedSide)
|
||||||
|
if fixedSide == SwapAmountSideUnknown || limitSide == SwapAmountSideUnknown {
|
||||||
|
swaps[0].SetSwapAmountInfo(
|
||||||
|
SwapModeExactIn,
|
||||||
|
decimal.NewFromUint64(args.InAmount),
|
||||||
|
decimal.NewFromUint64(args.MinimumOutAmount),
|
||||||
|
)
|
||||||
|
return swaps, offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
actualLimitAmount := swapAmountForSide(baseAmount, quoteAmount, limitSide)
|
||||||
|
if swapEvent, ok := metaoraPoolSwapEventFromInstruction(instruction); ok {
|
||||||
|
actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount)
|
||||||
|
} else if swapEvent, ok := metaoraPoolSwapEventForOffset(tx, swapOffset); ok {
|
||||||
|
actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount)
|
||||||
|
}
|
||||||
|
swaps[0].SetSwapAmountInfoDetailed(
|
||||||
SwapModeExactIn,
|
SwapModeExactIn,
|
||||||
decimal.NewFromUint64(args.InAmount),
|
decimal.NewFromUint64(args.InAmount),
|
||||||
|
fixedSide,
|
||||||
|
swapMintForSide(baseMint, quoteMint, fixedSide),
|
||||||
|
SwapLimitTypeMinOut,
|
||||||
decimal.NewFromUint64(args.MinimumOutAmount),
|
decimal.NewFromUint64(args.MinimumOutAmount),
|
||||||
|
limitSide,
|
||||||
|
swapMintForSide(baseMint, quoteMint, limitSide),
|
||||||
|
actualLimitAmount,
|
||||||
)
|
)
|
||||||
return swaps, offset, nil
|
return swaps, offset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func metaoraPoolSwapEventFromInstruction(instruction Instruction) (metaoraPoolSwapEvent, bool) {
|
||||||
|
for _, event := range instruction.LogEvents {
|
||||||
|
if swapEvent, ok := metaoraPoolDecodeSwapEventData(event); ok {
|
||||||
|
return swapEvent, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolSwapEventForOffset(tx *Tx, offset [2]uint) (metaoraPoolSwapEvent, bool) {
|
||||||
|
if tx == nil || tx.rawTx == nil {
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
occurrence := metaoraPoolSwapInstructionOccurrence(tx.rawTx, offset)
|
||||||
|
if occurrence == 0 {
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
return metaoraPoolSwapEventFromLogs(tx.rawTx.Meta.LogMessages, occurrence)
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolSwapInstructionOccurrence(rawTx *RawTx, offset [2]uint) int {
|
||||||
|
if rawTx == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
accountList := rawTx.getAccountList()
|
||||||
|
innerByOuter := make(map[int]InnerInstructions, len(rawTx.Meta.InnerInstructions))
|
||||||
|
for _, inner := range rawTx.Meta.InnerInstructions {
|
||||||
|
innerByOuter[inner.Index] = inner
|
||||||
|
}
|
||||||
|
|
||||||
|
occurrence := 0
|
||||||
|
for i, instruction := range rawTx.Transaction.Message.Instructions {
|
||||||
|
if uint(i) == offset[0] && offset[1] == 0 {
|
||||||
|
if metaoraPoolIsSwapInstruction(accountList, instruction) {
|
||||||
|
return occurrence + 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if metaoraPoolIsSwapInstruction(accountList, instruction) {
|
||||||
|
occurrence++
|
||||||
|
}
|
||||||
|
|
||||||
|
inner := innerByOuter[i]
|
||||||
|
for j, instruction := range inner.Instructions {
|
||||||
|
innerOffset := uint(j + 1)
|
||||||
|
if uint(i) == offset[0] && offset[1] == innerOffset {
|
||||||
|
if metaoraPoolIsSwapInstruction(accountList, instruction) {
|
||||||
|
return occurrence + 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if metaoraPoolIsSwapInstruction(accountList, instruction) {
|
||||||
|
occurrence++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolIsSwapInstruction(accountList []solana.PublicKey, instruction Instruction) bool {
|
||||||
|
if instruction.ProgramIDIndex < 0 || instruction.ProgramIDIndex >= len(accountList) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !accountList[instruction.ProgramIDIndex].Equals(metaoraPoolProgramID) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return len(instruction.Data) >= 8 && bytes.Equal(instruction.Data[:8], metaoraPoolSwapDiscriminator[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolSwapEventFromLogs(logMessages []string, occurrence int) (metaoraPoolSwapEvent, bool) {
|
||||||
|
if occurrence <= 0 {
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type frame struct {
|
||||||
|
program string
|
||||||
|
sawSwap bool
|
||||||
|
}
|
||||||
|
|
||||||
|
targetProgram := metaoraPoolProgramID.String()
|
||||||
|
var stack []frame
|
||||||
|
seen := 0
|
||||||
|
for _, logMessage := range logMessages {
|
||||||
|
if program, ok := metaoraPoolLogInvokeProgram(logMessage); ok {
|
||||||
|
stack = append(stack, frame{program: program})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stack) > 0 && stack[len(stack)-1].program == targetProgram {
|
||||||
|
if logMessage == "Program log: Instruction: Swap" {
|
||||||
|
stack[len(stack)-1].sawSwap = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if stack[len(stack)-1].sawSwap && strings.HasPrefix(logMessage, "Program data: ") {
|
||||||
|
event, ok := metaoraPoolDecodeSwapEventLog(strings.TrimSpace(strings.TrimPrefix(logMessage, "Program data: ")))
|
||||||
|
if ok {
|
||||||
|
seen++
|
||||||
|
if seen == occurrence {
|
||||||
|
return event, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if program, ok := metaoraPoolLogFinishedProgram(logMessage); ok {
|
||||||
|
for i := len(stack) - 1; i >= 0; i-- {
|
||||||
|
if stack[i].program == program {
|
||||||
|
stack = stack[:i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolLogInvokeProgram(logMessage string) (string, bool) {
|
||||||
|
if !strings.HasPrefix(logMessage, "Program ") || !strings.Contains(logMessage, " invoke [") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
rest := strings.TrimPrefix(logMessage, "Program ")
|
||||||
|
program, _, ok := strings.Cut(rest, " ")
|
||||||
|
return program, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolLogFinishedProgram(logMessage string) (string, bool) {
|
||||||
|
if !strings.HasPrefix(logMessage, "Program ") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(logMessage, " success") && !strings.Contains(logMessage, " failed:") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
rest := strings.TrimPrefix(logMessage, "Program ")
|
||||||
|
program, _, ok := strings.Cut(rest, " ")
|
||||||
|
return program, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolDecodeSwapEventLog(encoded string) (metaoraPoolSwapEvent, bool) {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
return metaoraPoolDecodeSwapEventData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolDecodeSwapEventData(data []byte) (metaoraPoolSwapEvent, bool) {
|
||||||
|
if len(data) < 48 {
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
if !bytes.Equal(data[:8], metaoraPoolSwapEventDiscriminator[:]) {
|
||||||
|
return metaoraPoolSwapEvent{}, false
|
||||||
|
}
|
||||||
|
body := data[8:]
|
||||||
|
return metaoraPoolSwapEvent{
|
||||||
|
InAmount: binary.LittleEndian.Uint64(body[0:8]),
|
||||||
|
OutAmount: binary.LittleEndian.Uint64(body[8:16]),
|
||||||
|
TradeFee: binary.LittleEndian.Uint64(body[16:24]),
|
||||||
|
ProtocolFee: binary.LittleEndian.Uint64(body[24:32]),
|
||||||
|
HostFee: binary.LittleEndian.Uint64(body[32:40]),
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|||||||
152
metaorapool_test.go
Normal file
152
metaorapool_test.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package pump_parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gagliardetto/solana-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMetaoraPoolSwapEventFromLogsUsesMatchingInvocation(t *testing.T) {
|
||||||
|
firstEvent := metaoraPoolSwapEventLogForTest(10, 9, 1, 0, 0)
|
||||||
|
secondEvent := metaoraPoolSwapEventLogForTest(4013522650, 135, 8043041, 2010760, 0)
|
||||||
|
|
||||||
|
logs := []string{
|
||||||
|
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
|
||||||
|
"Program log: Instruction: Swap",
|
||||||
|
"Program data: " + firstEvent,
|
||||||
|
"Program " + metaoraPoolProgramID.String() + " success",
|
||||||
|
"Program " + solana.TokenProgramID.String() + " invoke [1]",
|
||||||
|
"Program data: " + secondEvent,
|
||||||
|
"Program " + solana.TokenProgramID.String() + " success",
|
||||||
|
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
|
||||||
|
"Program log: Instruction: Swap",
|
||||||
|
"Program data: " + secondEvent,
|
||||||
|
"Program " + metaoraPoolProgramID.String() + " success",
|
||||||
|
}
|
||||||
|
|
||||||
|
event, ok := metaoraPoolSwapEventFromLogs(logs, 2)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected second swap event")
|
||||||
|
}
|
||||||
|
if event.InAmount != 4013522650 {
|
||||||
|
t.Fatalf("in amount = %d, want 4013522650", event.InAmount)
|
||||||
|
}
|
||||||
|
if event.OutAmount != 135 {
|
||||||
|
t.Fatalf("out amount = %d, want 135", event.OutAmount)
|
||||||
|
}
|
||||||
|
if event.TradeFee != 8043041 {
|
||||||
|
t.Fatalf("trade fee = %d, want 8043041", event.TradeFee)
|
||||||
|
}
|
||||||
|
if event.ProtocolFee != 2010760 {
|
||||||
|
t.Fatalf("protocol fee = %d, want 2010760", event.ProtocolFee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetaoraPoolSwapInstructionOccurrenceIncludesInnerInstructions(t *testing.T) {
|
||||||
|
rawTx := &RawTx{
|
||||||
|
Transaction: Transaction{
|
||||||
|
Message: Message{
|
||||||
|
AccountKeys: solana.PublicKeySlice{
|
||||||
|
metaoraPoolProgramID,
|
||||||
|
solana.MustPublicKeyFromBase58("BASDaPs2cdVTsvgPRfESDLZgek8tKRTfqbR2ksdgptsn"),
|
||||||
|
},
|
||||||
|
Instructions: []Instruction{
|
||||||
|
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
|
||||||
|
{ProgramIDIndex: 1, Data: []byte{1}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Meta: Meta{
|
||||||
|
InnerInstructions: []InnerInstructions{
|
||||||
|
{
|
||||||
|
Index: 1,
|
||||||
|
Instructions: []Instruction{
|
||||||
|
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := metaoraPoolSwapInstructionOccurrence(rawTx, [2]uint{1, 1}); got != 2 {
|
||||||
|
t.Fatalf("occurrence = %d, want 2", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttachLogEventsToInstructions(t *testing.T) {
|
||||||
|
swapEvent := metaoraPoolSwapEventLogForTest(4013522650, 135, 8043041, 2010760, 0)
|
||||||
|
rawTx := &RawTx{
|
||||||
|
Transaction: Transaction{
|
||||||
|
Message: Message{
|
||||||
|
AccountKeys: solana.PublicKeySlice{
|
||||||
|
metaoraPoolProgramID,
|
||||||
|
solana.TokenProgramID,
|
||||||
|
},
|
||||||
|
Instructions: []Instruction{
|
||||||
|
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Meta: Meta{
|
||||||
|
InnerInstructions: []InnerInstructions{
|
||||||
|
{
|
||||||
|
Index: 0,
|
||||||
|
Instructions: []Instruction{
|
||||||
|
{ProgramIDIndex: 1, Data: []byte{3}, StackHeight: intPtrForTest(2)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LogMessages: []string{
|
||||||
|
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
|
||||||
|
"Program log: Instruction: Swap",
|
||||||
|
"Program " + solana.TokenProgramID.String() + " invoke [2]",
|
||||||
|
"Program " + solana.TokenProgramID.String() + " success",
|
||||||
|
"Program data: " + swapEvent,
|
||||||
|
"Program " + metaoraPoolProgramID.String() + " success",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyRawTxConvertLogOptions(rawTx, RawTxConvertOptions{ParseLogEvents: true, IgnoreLogMessages: true})
|
||||||
|
|
||||||
|
if len(rawTx.Meta.LogMessages) != 0 {
|
||||||
|
t.Fatalf("log messages length = %d, want 0", len(rawTx.Meta.LogMessages))
|
||||||
|
}
|
||||||
|
if len(rawTx.Transaction.Message.Instructions[0].LogEvents) != 1 {
|
||||||
|
t.Fatalf("outer log events length = %d, want 1", len(rawTx.Transaction.Message.Instructions[0].LogEvents))
|
||||||
|
}
|
||||||
|
if len(rawTx.Meta.InnerInstructions[0].Instructions[0].LogEvents) != 0 {
|
||||||
|
t.Fatalf("inner log events length = %d, want 0", len(rawTx.Meta.InnerInstructions[0].Instructions[0].LogEvents))
|
||||||
|
}
|
||||||
|
|
||||||
|
event, ok := metaoraPoolSwapEventFromInstruction(rawTx.Transaction.Message.Instructions[0])
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected swap event from outer instruction")
|
||||||
|
}
|
||||||
|
if event.OutAmount != 135 {
|
||||||
|
t.Fatalf("out amount = %d, want 135", event.OutAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolSwapInstructionDataForTest() []byte {
|
||||||
|
data := make([]byte, 8+16)
|
||||||
|
copy(data, metaoraPoolSwapDiscriminator[:])
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaoraPoolSwapEventLogForTest(inAmount, outAmount, tradeFee, protocolFee, hostFee uint64) string {
|
||||||
|
data := make([]byte, 8+40)
|
||||||
|
copy(data, metaoraPoolSwapEventDiscriminator[:])
|
||||||
|
binary.LittleEndian.PutUint64(data[8:16], inAmount)
|
||||||
|
binary.LittleEndian.PutUint64(data[16:24], outAmount)
|
||||||
|
binary.LittleEndian.PutUint64(data[24:32], tradeFee)
|
||||||
|
binary.LittleEndian.PutUint64(data[32:40], protocolFee)
|
||||||
|
binary.LittleEndian.PutUint64(data[40:48], hostFee)
|
||||||
|
return base64.StdEncoding.EncodeToString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func intPtrForTest(value int) *int {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
@@ -83,7 +83,13 @@ type MetaoraDammInitializePoolEvent struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func meteoraDammV2InitializePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func meteoraDammV2InitializePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if len(instruction.Accounts) < 12 {
|
requiredAccounts := 12
|
||||||
|
if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializePoolWithDynamicConfig[:]) {
|
||||||
|
requiredAccounts = 13
|
||||||
|
} else if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializeCustomizablePoolDiscriminator[:]) {
|
||||||
|
requiredAccounts = 11
|
||||||
|
}
|
||||||
|
if len(instruction.Accounts) < requiredAccounts {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
||||||
}
|
}
|
||||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
@@ -439,7 +445,7 @@ func meteoraDammV2AddLiquidityParser(tx *Tx, instruction Instruction, innerInstr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func meteoraDammV2RemoveLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func meteoraDammV2RemoveLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if len(instruction.Accounts) < 8 {
|
if len(instruction.Accounts) < 9 {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
||||||
}
|
}
|
||||||
tokenAMint := tx.rawTx.accountList[instruction.Accounts[7]]
|
tokenAMint := tx.rawTx.accountList[instruction.Accounts[7]]
|
||||||
|
|||||||
661
pump.go
661
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,185 @@ 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 {
|
func pumpCompleteMatchesTradeEvent(completeEvent CompleteEvent, tradeEvent PumpTradeEvent, bondingCurve solana.PublicKey) bool {
|
||||||
if completeEvent.Mint != tradeEvent.Mint {
|
if completeEvent.Mint != tradeEvent.Mint {
|
||||||
return false
|
return false
|
||||||
@@ -279,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 {
|
||||||
@@ -290,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
|
||||||
@@ -325,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]
|
||||||
solReserves, _ := GetSolAfterTx(result, bcIdx)
|
quoteReserves := decimal.Zero
|
||||||
|
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
|
||||||
|
quoteReserves = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuotePoolToken])
|
||||||
|
} else {
|
||||||
|
solReserves, _ := GetSolAfterTx(result, bcIdx)
|
||||||
|
quoteReserves = decimal.NewFromUint64(solReserves)
|
||||||
|
}
|
||||||
tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex)
|
tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex)
|
||||||
swaps := []Swap{
|
swaps := []Swap{
|
||||||
{
|
{
|
||||||
Program: SolProgramPump,
|
Program: SolProgramPump,
|
||||||
Event: event,
|
Event: event,
|
||||||
Pool: result.accountList[instruction.Accounts[3]],
|
Pool: pumpAccount(result, instruction, layout.Pool),
|
||||||
BaseMint: mint,
|
BaseMint: mint,
|
||||||
QuoteMint: solana.PublicKey{},
|
QuoteMint: quoteMint,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
QuoteTokenProgram: solana.PublicKey{},
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
BaseMintDecimals: 6,
|
BaseMintDecimals: 6,
|
||||||
QuoteMintDecimals: 9,
|
QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
|
||||||
User: user,
|
User: user,
|
||||||
BaseAmount: decimal.NewFromUint64(tokenAmount),
|
BaseAmount: decimal.NewFromUint64(tokenAmount),
|
||||||
QuoteAmount: decimal.NewFromUint64(solAmount),
|
QuoteAmount: decimal.NewFromUint64(quoteAmount),
|
||||||
BaseReserve: tokenReserves,
|
BaseReserve: tokenReserves,
|
||||||
QuoteReserve: decimal.NewFromUint64(solReserves),
|
QuoteReserve: quoteReserves,
|
||||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
|
Mayhem: isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
|
||||||
UserBaseBalance: userBase,
|
UserBaseBalance: userBase,
|
||||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
UserQuoteBalance: userQuote,
|
||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -365,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 {
|
||||||
@@ -400,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])
|
||||||
@@ -411,7 +799,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
if tradeFound {
|
if tradeFound {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&tradeEvent)
|
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 {
|
||||||
@@ -420,7 +808,7 @@ 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 := !bytes.Equal(instruction.Data[:8], pumpSellDiscriminator[:])
|
expectedIsBuy := !pumpInstructionIsSell(instruction.Data)
|
||||||
if tradeEvent.IsBuy != expectedIsBuy {
|
if tradeEvent.IsBuy != expectedIsBuy {
|
||||||
tradeEvent = PumpTradeEvent{}
|
tradeEvent = PumpTradeEvent{}
|
||||||
continue
|
continue
|
||||||
@@ -437,7 +825,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, result.accountList[instruction.Accounts[3]]) {
|
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, pumpAccount(result, instruction, layout.Pool)) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if offset[1] == 0 {
|
if offset[1] == 0 {
|
||||||
@@ -451,7 +839,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,13 +851,16 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
}
|
}
|
||||||
|
|
||||||
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{
|
||||||
@@ -481,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
|
||||||
@@ -494,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
|
||||||
@@ -509,22 +906,22 @@ 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,
|
||||||
},
|
},
|
||||||
@@ -537,20 +934,20 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -572,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) {
|
||||||
@@ -633,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{
|
||||||
@@ -661,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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -685,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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
186
pump_test.go
186
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"
|
||||||
@@ -100,3 +101,188 @@ func TestPumpCompleteMatchesTradeEvent(t *testing.T) {
|
|||||||
t.Fatal("pumpCompleteMatchesTradeEvent() = true for mismatched user")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
21
pumpamm.go
21
pumpamm.go
@@ -182,6 +182,9 @@ func pumpAmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ammCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func ammCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
|
if len(instruction.Accounts) < 15 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
@@ -275,6 +278,9 @@ func pumpAmmSwapAmountInfoFromArgs(args PumpSwapArgs) (swapMode SwapMode, fixedA
|
|||||||
}
|
}
|
||||||
|
|
||||||
func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
if tx.Err == nil || tx.Err.UnKnown != "" {
|
if tx.Err == nil || tx.Err.UnKnown != "" {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
||||||
}
|
}
|
||||||
@@ -401,6 +407,9 @@ func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions Inn
|
|||||||
}
|
}
|
||||||
|
|
||||||
func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
if tx.Err == nil || tx.Err.UnKnown != "" {
|
if tx.Err == nil || tx.Err.UnKnown != "" {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
||||||
}
|
}
|
||||||
@@ -525,6 +534,9 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
|||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
|
return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
|
||||||
@@ -662,6 +674,9 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
|||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
|
if len(instruction.Accounts) < 13 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -789,6 +804,9 @@ func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
|||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
|
if len(instruction.Accounts) < 11 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -887,6 +905,9 @@ func withdrawParse(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
|||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
var err error
|
var err error
|
||||||
|
if len(instruction.Accounts) < 11 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
193
rawtx.go
193
rawtx.go
@@ -1,8 +1,12 @@
|
|||||||
package pump_parser
|
package pump_parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
bin "github.com/gagliardetto/binary"
|
bin "github.com/gagliardetto/binary"
|
||||||
@@ -105,10 +109,11 @@ func (tx *RawTx) GetBlockTime() *pgtype.Timestamptz {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Instruction struct {
|
type Instruction struct {
|
||||||
Accounts []int `json:"accounts"`
|
Accounts []int `json:"accounts"`
|
||||||
Data solana.Base58 `json:"data"`
|
Data solana.Base58 `json:"data"`
|
||||||
ProgramIDIndex int `json:"programIdIndex"`
|
ProgramIDIndex int `json:"programIdIndex"`
|
||||||
StackHeight *int `json:"stackHeight"`
|
StackHeight *int `json:"stackHeight"`
|
||||||
|
LogEvents []solana.Base64 `json:"logEvents,omitempty"`
|
||||||
}
|
}
|
||||||
type InnerInstructions struct {
|
type InnerInstructions struct {
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
@@ -180,6 +185,11 @@ type Transaction struct {
|
|||||||
Signatures []solana.Signature `json:"signatures"`
|
Signatures []solana.Signature `json:"signatures"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RawTxConvertOptions struct {
|
||||||
|
IgnoreLogMessages bool
|
||||||
|
ParseLogEvents bool
|
||||||
|
}
|
||||||
|
|
||||||
func (tx *Transaction) UnmarshalJSON(data []byte) error {
|
func (tx *Transaction) UnmarshalJSON(data []byte) error {
|
||||||
if len(data) == 0 || (len(data) == 4 && string(data) == "null") {
|
if len(data) == 0 || (len(data) == 4 && string(data) == "null") {
|
||||||
// TODO: is this an error?
|
// TODO: is this an error?
|
||||||
@@ -308,7 +318,8 @@ func marshalRpcTransactionErr(err any) string {
|
|||||||
return string(e)
|
return string(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, slot uint64, index int64) (*RawTx, error) {
|
func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, slot uint64, index int64, options ...RawTxConvertOptions) (*RawTx, error) {
|
||||||
|
option := rawTxConvertOption(options)
|
||||||
created := int64(0)
|
created := int64(0)
|
||||||
if blockTime != nil {
|
if blockTime != nil {
|
||||||
created = int64(*blockTime)
|
created = int64(*blockTime)
|
||||||
@@ -523,6 +534,8 @@ func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, s
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyRawTxConvertLogOptions(sTx, option)
|
||||||
|
|
||||||
return sTx, nil
|
return sTx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,7 +846,8 @@ func isAccountOwner(account, owner, mint solana.PublicKey) (bool, error) {
|
|||||||
return account == ata, nil
|
return account == ata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateTransaction, created int64) (*RawTx, error) {
|
func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateTransaction, created int64, options ...RawTxConvertOptions) (*RawTx, error) {
|
||||||
|
option := rawTxConvertOption(options)
|
||||||
sTx := &RawTx{
|
sTx := &RawTx{
|
||||||
BlockTime: created,
|
BlockTime: created,
|
||||||
Slot: y.Slot,
|
Slot: y.Slot,
|
||||||
@@ -863,6 +877,9 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT
|
|||||||
//Version: nil,
|
//Version: nil,
|
||||||
}
|
}
|
||||||
meta := y.Transaction.GetMeta()
|
meta := y.Transaction.GetMeta()
|
||||||
|
if meta == nil {
|
||||||
|
return nil, errors.New("meta can not parser")
|
||||||
|
}
|
||||||
yTx := y.Transaction.Transaction
|
yTx := y.Transaction.Transaction
|
||||||
|
|
||||||
if meta.Err != nil && len(meta.Err.GetErr()) > 0 {
|
if meta.Err != nil && len(meta.Err.GetErr()) > 0 {
|
||||||
@@ -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
|
||||||
|
|||||||
234
rawtx_binary.go
234
rawtx_binary.go
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
const rawTxBinarySchemaVersionCurrent uint16 = 7
|
const rawTxBinarySchemaVersionCurrent uint16 = 10
|
||||||
|
|
||||||
var rawTxBinaryMagic = [4]byte{'P', 'R', 'T', 'X'}
|
var rawTxBinaryMagic = [4]byte{'P', 'R', 'T', 'X'}
|
||||||
var rawTxsBinaryMagic = [4]byte{'P', 'R', 'T', 'S'}
|
var rawTxsBinaryMagic = [4]byte{'P', 'R', 'T', 'S'}
|
||||||
@@ -61,11 +61,11 @@ type RawTxMetaBinary struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RawTxTokenBalanceBinary struct {
|
type RawTxTokenBalanceBinary struct {
|
||||||
AccountIndex uint16
|
AccountIndex uint8
|
||||||
MintAccount uint16
|
MintAccount uint8
|
||||||
OwnerAccount uint16
|
OwnerAccount uint8
|
||||||
HasOwnerAccount bool
|
HasOwnerAccount bool
|
||||||
ProgramIDAccount uint16
|
ProgramIDAccount uint8
|
||||||
Decimals uint8
|
Decimals uint8
|
||||||
HasPreAmount bool
|
HasPreAmount bool
|
||||||
PreAmount string
|
PreAmount string
|
||||||
@@ -270,10 +270,17 @@ func DecodeRawTxsBinaryReader(r io.Reader) iter.Seq2[*RawTx, error] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newRawTxBinaryWithAddressTable(tx *RawTx, addressTable []solana.PublicKey, addressIndex *txBinaryAddressIndex) (*RawTxBinary, error) {
|
func newRawTxBinaryWithAddressTable(tx *RawTx, addressTable []solana.PublicKey, addressIndex *txBinaryAddressIndex) (*RawTxBinary, error) {
|
||||||
accountList := tx.getAccountList()
|
accountList, err := rawTxBinaryEffectiveAccountList(tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if uint64(len(accountList)) > uint64(math.MaxUint32) {
|
if uint64(len(accountList)) > uint64(math.MaxUint32) {
|
||||||
return nil, fmt.Errorf("account list exceeds uint32 capacity")
|
return nil, fmt.Errorf("account list exceeds uint32 capacity")
|
||||||
}
|
}
|
||||||
|
accountListIndex, err := newRawTxBinaryAccountListIndex(accountList)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if uint64(len(tx.Transaction.Message.AccountKeys)) > uint64(math.MaxUint32) {
|
if uint64(len(tx.Transaction.Message.AccountKeys)) > uint64(math.MaxUint32) {
|
||||||
return nil, fmt.Errorf("message account key count exceeds uint32 capacity")
|
return nil, fmt.Errorf("message account key count exceeds uint32 capacity")
|
||||||
}
|
}
|
||||||
@@ -299,7 +306,7 @@ func newRawTxBinaryWithAddressTable(tx *RawTx, addressTable []solana.PublicKey,
|
|||||||
out.AccountList = append(out.AccountList, ref)
|
out.AccountList = append(out.AccountList, ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
meta, err := rawTxMetaToBinary(&tx.Meta, addressIndex)
|
meta, err := rawTxMetaToBinary(&tx.Meta, accountListIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -520,7 +527,7 @@ func (tx *RawTxBinary) ToRawTx() (*RawTx, error) {
|
|||||||
IndexWithinBlock: int64(tx.IndexWithinBlock),
|
IndexWithinBlock: int64(tx.IndexWithinBlock),
|
||||||
Slot: tx.Slot,
|
Slot: tx.Slot,
|
||||||
Version: rawTxBinaryVersionValue(tx.Version),
|
Version: rawTxBinaryVersionValue(tx.Version),
|
||||||
Meta: rawTxMetaFromBinary(tx.Meta, tx.AddressTable),
|
Meta: rawTxMetaFromBinary(tx.Meta, accountList),
|
||||||
Transaction: rawTxTransactionFromBinary(tx.Transaction, tx.AddressTable),
|
Transaction: rawTxTransactionFromBinary(tx.Transaction, tx.AddressTable),
|
||||||
}
|
}
|
||||||
if tx.AccountKeyCount > 0 && tx.AccountKeyCount <= uint32(len(accountList)) {
|
if tx.AccountKeyCount > 0 && tx.AccountKeyCount <= uint32(len(accountList)) {
|
||||||
@@ -678,7 +685,7 @@ func rawTxBinaryReadTxBody(dec txBinaryBodyReader, tx *RawTxBinary, addressTable
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rawTxMetaToBinary(meta *Meta, addressIndex *txBinaryAddressIndex) (RawTxMetaBinary, error) {
|
func rawTxMetaToBinary(meta *Meta, accountListIndex map[solana.PublicKey]uint8) (RawTxMetaBinary, error) {
|
||||||
out := RawTxMetaBinary{
|
out := RawTxMetaBinary{
|
||||||
Err: cloneTransactionParsedError(meta.Err),
|
Err: cloneTransactionParsedError(meta.Err),
|
||||||
Fee: meta.Fee,
|
Fee: meta.Fee,
|
||||||
@@ -688,7 +695,7 @@ func rawTxMetaToBinary(meta *Meta, addressIndex *txBinaryAddressIndex) (RawTxMet
|
|||||||
ComputeUnitsConsumed: meta.ComputeUnitsConsumed,
|
ComputeUnitsConsumed: meta.ComputeUnitsConsumed,
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
out.TokenBalances, err = rawTxTokenBalancesToBinary(meta.PreTokenBalances, meta.PostTokenBalances, addressIndex)
|
out.TokenBalances, err = rawTxTokenBalancesToBinary(meta.PreTokenBalances, meta.PostTokenBalances, accountListIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return out, fmt.Errorf("token_balances: %w", err)
|
return out, fmt.Errorf("token_balances: %w", err)
|
||||||
}
|
}
|
||||||
@@ -715,86 +722,75 @@ func rawTxMessageToBinary(message *Message, addressIndex *txBinaryAddressIndex)
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rawTxTokenBalancesToBinary(preBalances []TokenBalance, postBalances []TokenBalance, addressIndex *txBinaryAddressIndex) ([]RawTxTokenBalanceBinary, error) {
|
func rawTxTokenBalancesToBinary(preBalances []TokenBalance, postBalances []TokenBalance, accountListIndex map[solana.PublicKey]uint8) ([]RawTxTokenBalanceBinary, error) {
|
||||||
out := make([]RawTxTokenBalanceBinary, 0, len(preBalances)+len(postBalances))
|
out := make([]RawTxTokenBalanceBinary, 0, len(preBalances)+len(postBalances))
|
||||||
byAccountIndex := make(map[uint16]int, len(preBalances)+len(postBalances))
|
preByAccountIndex := make(map[uint8]int, len(preBalances))
|
||||||
|
postSeenByAccountIndex := make(map[uint8]struct{}, len(postBalances))
|
||||||
for i, balance := range preBalances {
|
for i, balance := range preBalances {
|
||||||
encoded, err := rawTxTokenBalanceToBinary(balance, addressIndex)
|
encoded, err := rawTxTokenBalanceToBinary(balance, accountListIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("pre[%d]: %w", i, err)
|
return nil, fmt.Errorf("pre[%d]: %w", i, err)
|
||||||
}
|
}
|
||||||
if _, exists := byAccountIndex[encoded.AccountIndex]; exists {
|
if _, exists := preByAccountIndex[encoded.AccountIndex]; exists {
|
||||||
return nil, fmt.Errorf("pre[%d].account_index duplicate: %d", i, encoded.AccountIndex)
|
return nil, fmt.Errorf("pre[%d].account_index duplicate: %d", i, encoded.AccountIndex)
|
||||||
}
|
}
|
||||||
encoded.HasPreAmount = true
|
encoded.HasPreAmount = true
|
||||||
encoded.PreAmount = balance.UITokenAmount.Amount
|
encoded.PreAmount = balance.UITokenAmount.Amount
|
||||||
byAccountIndex[encoded.AccountIndex] = len(out)
|
preByAccountIndex[encoded.AccountIndex] = len(out)
|
||||||
out = append(out, encoded)
|
out = append(out, encoded)
|
||||||
}
|
}
|
||||||
for i, balance := range postBalances {
|
for i, balance := range postBalances {
|
||||||
encoded, err := rawTxTokenBalanceToBinary(balance, addressIndex)
|
encoded, err := rawTxTokenBalanceToBinary(balance, accountListIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("post[%d]: %w", i, err)
|
return nil, fmt.Errorf("post[%d]: %w", i, err)
|
||||||
}
|
}
|
||||||
if existingIndex, exists := byAccountIndex[encoded.AccountIndex]; exists {
|
if _, exists := postSeenByAccountIndex[encoded.AccountIndex]; exists {
|
||||||
if out[existingIndex].HasPostAmount {
|
return nil, fmt.Errorf("post[%d].account_index duplicate: %d", i, encoded.AccountIndex)
|
||||||
return nil, fmt.Errorf("post[%d].account_index duplicate: %d", i, encoded.AccountIndex)
|
}
|
||||||
}
|
postSeenByAccountIndex[encoded.AccountIndex] = struct{}{}
|
||||||
if !rawTxTokenBalanceBinarySameIdentity(out[existingIndex], encoded) {
|
if existingIndex, exists := preByAccountIndex[encoded.AccountIndex]; exists && rawTxTokenBalanceBinarySameIdentity(out[existingIndex], encoded) {
|
||||||
return nil, fmt.Errorf("post[%d].account_index %d identity mismatch", i, encoded.AccountIndex)
|
|
||||||
}
|
|
||||||
out[existingIndex].HasPostAmount = true
|
out[existingIndex].HasPostAmount = true
|
||||||
out[existingIndex].PostAmount = balance.UITokenAmount.Amount
|
out[existingIndex].PostAmount = balance.UITokenAmount.Amount
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
encoded.HasPostAmount = true
|
encoded.HasPostAmount = true
|
||||||
encoded.PostAmount = balance.UITokenAmount.Amount
|
encoded.PostAmount = balance.UITokenAmount.Amount
|
||||||
byAccountIndex[encoded.AccountIndex] = len(out)
|
|
||||||
out = append(out, encoded)
|
out = append(out, encoded)
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rawTxTokenBalanceToBinary(balance TokenBalance, addressIndex *txBinaryAddressIndex) (RawTxTokenBalanceBinary, error) {
|
func rawTxTokenBalanceToBinary(balance TokenBalance, accountListIndex map[solana.PublicKey]uint8) (RawTxTokenBalanceBinary, error) {
|
||||||
mintAccount, ownerAccount, programIDAccount, err := rawTxBinaryTokenBalanceAccounts(balance)
|
mintAccount, ownerAccount, programIDAccount, err := rawTxBinaryTokenBalanceAccounts(balance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return RawTxTokenBalanceBinary{}, err
|
return RawTxTokenBalanceBinary{}, err
|
||||||
}
|
}
|
||||||
mint, err := addressIndex.id(mintAccount)
|
mint, ok := accountListIndex[mintAccount]
|
||||||
if err != nil {
|
if !ok {
|
||||||
return RawTxTokenBalanceBinary{}, fmt.Errorf("mint: %w", err)
|
return RawTxTokenBalanceBinary{}, fmt.Errorf("mint account not found in account_list: %s", mintAccount)
|
||||||
}
|
}
|
||||||
programID, err := addressIndex.id(programIDAccount)
|
programID, ok := accountListIndex[programIDAccount]
|
||||||
if err != nil {
|
if !ok {
|
||||||
return RawTxTokenBalanceBinary{}, fmt.Errorf("program_id: %w", err)
|
return RawTxTokenBalanceBinary{}, fmt.Errorf("program_id account not found in account_list: %s", programIDAccount)
|
||||||
}
|
|
||||||
if mint > math.MaxUint16 {
|
|
||||||
return RawTxTokenBalanceBinary{}, fmt.Errorf("mint ref overflows uint16: %d", mint)
|
|
||||||
}
|
|
||||||
if programID > math.MaxUint16 {
|
|
||||||
return RawTxTokenBalanceBinary{}, fmt.Errorf("program_id ref overflows uint16: %d", programID)
|
|
||||||
}
|
}
|
||||||
if balance.UITokenAmount.Decimals > math.MaxUint8 {
|
if balance.UITokenAmount.Decimals > math.MaxUint8 {
|
||||||
return RawTxTokenBalanceBinary{}, fmt.Errorf("decimals overflows uint8: %d", balance.UITokenAmount.Decimals)
|
return RawTxTokenBalanceBinary{}, fmt.Errorf("decimals overflows uint8: %d", balance.UITokenAmount.Decimals)
|
||||||
}
|
}
|
||||||
if balance.AccountIndex < 0 || balance.AccountIndex > math.MaxUint16 {
|
if balance.AccountIndex < 0 || balance.AccountIndex > math.MaxUint8 {
|
||||||
return RawTxTokenBalanceBinary{}, fmt.Errorf("account_index overflows uint16: %d", balance.AccountIndex)
|
return RawTxTokenBalanceBinary{}, fmt.Errorf("account_index overflows uint8: %d", balance.AccountIndex)
|
||||||
}
|
}
|
||||||
encoded := RawTxTokenBalanceBinary{
|
encoded := RawTxTokenBalanceBinary{
|
||||||
AccountIndex: uint16(balance.AccountIndex),
|
AccountIndex: uint8(balance.AccountIndex),
|
||||||
MintAccount: uint16(mint),
|
MintAccount: mint,
|
||||||
ProgramIDAccount: uint16(programID),
|
ProgramIDAccount: programID,
|
||||||
Decimals: uint8(balance.UITokenAmount.Decimals),
|
Decimals: uint8(balance.UITokenAmount.Decimals),
|
||||||
}
|
}
|
||||||
if ownerAccount != nil {
|
if ownerAccount != nil {
|
||||||
owner, err := addressIndex.id(*ownerAccount)
|
owner, ok := accountListIndex[*ownerAccount]
|
||||||
if err != nil {
|
if !ok {
|
||||||
return RawTxTokenBalanceBinary{}, fmt.Errorf("owner: %w", err)
|
return RawTxTokenBalanceBinary{}, fmt.Errorf("owner account not found in account_list: %s", *ownerAccount)
|
||||||
}
|
}
|
||||||
if owner > math.MaxUint16 {
|
encoded.OwnerAccount = owner
|
||||||
return RawTxTokenBalanceBinary{}, fmt.Errorf("owner ref overflows uint16: %d", owner)
|
|
||||||
}
|
|
||||||
encoded.OwnerAccount = uint16(owner)
|
|
||||||
encoded.HasOwnerAccount = true
|
encoded.HasOwnerAccount = true
|
||||||
}
|
}
|
||||||
return encoded, nil
|
return encoded, nil
|
||||||
@@ -809,8 +805,8 @@ func rawTxTokenBalanceBinarySameIdentity(a, b RawTxTokenBalanceBinary) bool {
|
|||||||
a.Decimals == b.Decimals
|
a.Decimals == b.Decimals
|
||||||
}
|
}
|
||||||
|
|
||||||
func rawTxMetaFromBinary(meta RawTxMetaBinary, addressTable []solana.PublicKey) Meta {
|
func rawTxMetaFromBinary(meta RawTxMetaBinary, accountList []solana.PublicKey) Meta {
|
||||||
preTokenBalances, postTokenBalances := rawTxTokenBalancesFromBinary(meta.TokenBalances, addressTable)
|
preTokenBalances, postTokenBalances := rawTxTokenBalancesFromBinary(meta.TokenBalances, accountList)
|
||||||
return Meta{
|
return Meta{
|
||||||
Err: cloneTransactionParsedError(meta.Err),
|
Err: cloneTransactionParsedError(meta.Err),
|
||||||
Fee: meta.Fee,
|
Fee: meta.Fee,
|
||||||
@@ -838,15 +834,15 @@ func rawTxTransactionFromBinary(tx RawTxTransactionBinary, addressTable []solana
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func rawTxTokenBalancesFromBinary(balances []RawTxTokenBalanceBinary, addressTable []solana.PublicKey) ([]TokenBalance, []TokenBalance) {
|
func rawTxTokenBalancesFromBinary(balances []RawTxTokenBalanceBinary, accountList []solana.PublicKey) ([]TokenBalance, []TokenBalance) {
|
||||||
pre := make([]TokenBalance, 0, len(balances))
|
pre := make([]TokenBalance, 0, len(balances))
|
||||||
post := make([]TokenBalance, 0, len(balances))
|
post := make([]TokenBalance, 0, len(balances))
|
||||||
for _, balance := range balances {
|
for _, balance := range balances {
|
||||||
mint, _ := txBinaryAddressAt(addressTable, uint32(balance.MintAccount), "token_balance.mint")
|
mint, _ := txBinaryAddressAt(accountList, uint32(balance.MintAccount), "token_balance.mint")
|
||||||
programID, _ := txBinaryAddressAt(addressTable, uint32(balance.ProgramIDAccount), "token_balance.program_id")
|
programID, _ := txBinaryAddressAt(accountList, uint32(balance.ProgramIDAccount), "token_balance.program_id")
|
||||||
var owner *solana.PublicKey
|
var owner *solana.PublicKey
|
||||||
if balance.HasOwnerAccount {
|
if balance.HasOwnerAccount {
|
||||||
ownerKey, _ := txBinaryAddressAt(addressTable, uint32(balance.OwnerAccount), "token_balance.owner")
|
ownerKey, _ := txBinaryAddressAt(accountList, uint32(balance.OwnerAccount), "token_balance.owner")
|
||||||
owner = &ownerKey
|
owner = &ownerKey
|
||||||
}
|
}
|
||||||
if balance.HasPreAmount {
|
if balance.HasPreAmount {
|
||||||
@@ -1065,10 +1061,10 @@ func readInnerInstructions(dec txBinaryBodyReader) ([]InnerInstructions, error)
|
|||||||
func writeInstructions(enc *txBinaryEncoder, values []Instruction) error {
|
func writeInstructions(enc *txBinaryEncoder, values []Instruction) error {
|
||||||
enc.writeUint32(uint32(len(values)))
|
enc.writeUint32(uint32(len(values)))
|
||||||
for i, value := range values {
|
for i, value := range values {
|
||||||
if value.ProgramIDIndex < 0 || value.ProgramIDIndex > math.MaxUint16 {
|
if value.ProgramIDIndex < 0 || value.ProgramIDIndex > math.MaxUint8 {
|
||||||
return fmt.Errorf("[%d].program_id_index overflows uint16: %d", i, value.ProgramIDIndex)
|
return fmt.Errorf("[%d].program_id_index overflows uint8: %d", i, value.ProgramIDIndex)
|
||||||
}
|
}
|
||||||
enc.writeUint16(uint16(value.ProgramIDIndex))
|
enc.writeUint8(uint8(value.ProgramIDIndex))
|
||||||
if err := writeAccountIndexSlice(enc, value.Accounts); err != nil {
|
if err := writeAccountIndexSlice(enc, value.Accounts); err != nil {
|
||||||
return fmt.Errorf("[%d].accounts: %w", i, err)
|
return fmt.Errorf("[%d].accounts: %w", i, err)
|
||||||
}
|
}
|
||||||
@@ -1077,6 +1073,10 @@ func writeInstructions(enc *txBinaryEncoder, values []Instruction) error {
|
|||||||
if value.StackHeight != nil {
|
if value.StackHeight != nil {
|
||||||
enc.writeUint32(uint32(*value.StackHeight))
|
enc.writeUint32(uint32(*value.StackHeight))
|
||||||
}
|
}
|
||||||
|
enc.writeUint32(uint32(len(value.LogEvents)))
|
||||||
|
for _, event := range value.LogEvents {
|
||||||
|
writeByteSlice(enc, event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1088,7 +1088,7 @@ func readInstructions(dec txBinaryBodyReader) ([]Instruction, error) {
|
|||||||
}
|
}
|
||||||
out := make([]Instruction, 0, count)
|
out := make([]Instruction, 0, count)
|
||||||
for i := uint32(0); i < count; i++ {
|
for i := uint32(0); i < count; i++ {
|
||||||
programIDIndex, err := dec.readUint16()
|
programIDIndex, err := dec.readUint8()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1113,11 +1113,24 @@ func readInstructions(dec txBinaryBodyReader) ([]Instruction, error) {
|
|||||||
sh := int(rawStackHeight)
|
sh := int(rawStackHeight)
|
||||||
stackHeight = &sh
|
stackHeight = &sh
|
||||||
}
|
}
|
||||||
|
logEventCount, err := dec.readUint32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logEvents := make([]solana.Base64, 0, logEventCount)
|
||||||
|
for j := uint32(0); j < logEventCount; j++ {
|
||||||
|
eventData, err := readByteSlice(dec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logEvents = append(logEvents, solana.Base64(eventData))
|
||||||
|
}
|
||||||
out = append(out, Instruction{
|
out = append(out, Instruction{
|
||||||
ProgramIDIndex: int(programIDIndex),
|
ProgramIDIndex: int(programIDIndex),
|
||||||
Accounts: accounts,
|
Accounts: accounts,
|
||||||
Data: solana.Base58(data),
|
Data: solana.Base58(data),
|
||||||
StackHeight: stackHeight,
|
StackHeight: stackHeight,
|
||||||
|
LogEvents: logEvents,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
@@ -1126,13 +1139,13 @@ func readInstructions(dec txBinaryBodyReader) ([]Instruction, error) {
|
|||||||
func writeTokenBalances(enc *txBinaryEncoder, values []RawTxTokenBalanceBinary) error {
|
func writeTokenBalances(enc *txBinaryEncoder, values []RawTxTokenBalanceBinary) error {
|
||||||
enc.writeUint32(uint32(len(values)))
|
enc.writeUint32(uint32(len(values)))
|
||||||
for i, value := range values {
|
for i, value := range values {
|
||||||
enc.writeUint16(value.AccountIndex)
|
enc.writeUint8(value.AccountIndex)
|
||||||
enc.writeUint16(value.MintAccount)
|
enc.writeUint8(value.MintAccount)
|
||||||
enc.writeBool(value.HasOwnerAccount)
|
enc.writeBool(value.HasOwnerAccount)
|
||||||
if value.HasOwnerAccount {
|
if value.HasOwnerAccount {
|
||||||
enc.writeUint16(value.OwnerAccount)
|
enc.writeUint8(value.OwnerAccount)
|
||||||
}
|
}
|
||||||
enc.writeUint16(value.ProgramIDAccount)
|
enc.writeUint8(value.ProgramIDAccount)
|
||||||
enc.writeUint8(value.Decimals)
|
enc.writeUint8(value.Decimals)
|
||||||
enc.writeBool(value.HasPreAmount)
|
enc.writeBool(value.HasPreAmount)
|
||||||
if value.HasPreAmount {
|
if value.HasPreAmount {
|
||||||
@@ -1158,21 +1171,21 @@ func readTokenBalances(dec txBinaryBodyReader) ([]RawTxTokenBalanceBinary, error
|
|||||||
out := make([]RawTxTokenBalanceBinary, 0, count)
|
out := make([]RawTxTokenBalanceBinary, 0, count)
|
||||||
for i := uint32(0); i < count; i++ {
|
for i := uint32(0); i < count; i++ {
|
||||||
value := RawTxTokenBalanceBinary{}
|
value := RawTxTokenBalanceBinary{}
|
||||||
if value.AccountIndex, err = dec.readUint16(); err != nil {
|
if value.AccountIndex, err = dec.readUint8(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if value.MintAccount, err = dec.readUint16(); err != nil {
|
if value.MintAccount, err = dec.readUint8(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if value.HasOwnerAccount, err = dec.readBool(); err != nil {
|
if value.HasOwnerAccount, err = dec.readBool(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if value.HasOwnerAccount {
|
if value.HasOwnerAccount {
|
||||||
if value.OwnerAccount, err = dec.readUint16(); err != nil {
|
if value.OwnerAccount, err = dec.readUint8(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if value.ProgramIDAccount, err = dec.readUint16(); err != nil {
|
if value.ProgramIDAccount, err = dec.readUint8(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if value.Decimals, err = dec.readUint8(); err != nil {
|
if value.Decimals, err = dec.readUint8(); err != nil {
|
||||||
@@ -1336,10 +1349,10 @@ func readUint32Slice(dec txBinaryBodyReader) ([]uint32, error) {
|
|||||||
func writeAccountIndexSlice(enc *txBinaryEncoder, values []int) error {
|
func writeAccountIndexSlice(enc *txBinaryEncoder, values []int) error {
|
||||||
enc.writeUint32(uint32(len(values)))
|
enc.writeUint32(uint32(len(values)))
|
||||||
for i, value := range values {
|
for i, value := range values {
|
||||||
if value < 0 || value > math.MaxUint16 {
|
if value < 0 || value > math.MaxUint8 {
|
||||||
return fmt.Errorf("[%d] overflows uint16: %d", i, value)
|
return fmt.Errorf("[%d] overflows uint8: %d", i, value)
|
||||||
}
|
}
|
||||||
enc.writeUint16(uint16(value))
|
enc.writeUint8(uint8(value))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1351,7 +1364,7 @@ func readAccountIndexSlice(dec txBinaryBodyReader) ([]int, error) {
|
|||||||
}
|
}
|
||||||
out := make([]int, 0, count)
|
out := make([]int, 0, count)
|
||||||
for i := uint32(0); i < count; i++ {
|
for i := uint32(0); i < count; i++ {
|
||||||
value, err := dec.readUint16()
|
value, err := dec.readUint8()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1547,7 +1560,11 @@ func rawTxBinaryBuildAddressTable(txs []*RawTx) ([]solana.PublicKey, error) {
|
|||||||
if tx == nil {
|
if tx == nil {
|
||||||
return nil, fmt.Errorf("tx[%d] is nil", txIndex)
|
return nil, fmt.Errorf("tx[%d] is nil", txIndex)
|
||||||
}
|
}
|
||||||
for accountIndex, account := range tx.getAccountList() {
|
accountList, err := rawTxBinaryEffectiveAccountList(tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("tx[%d].account_list: %w", txIndex, err)
|
||||||
|
}
|
||||||
|
for accountIndex, account := range accountList {
|
||||||
if err := builder.add(account); err != nil {
|
if err := builder.add(account); err != nil {
|
||||||
return nil, fmt.Errorf("tx[%d].account_list[%d]: %w", txIndex, accountIndex, err)
|
return nil, fmt.Errorf("tx[%d].account_list[%d]: %w", txIndex, accountIndex, err)
|
||||||
}
|
}
|
||||||
@@ -1557,20 +1574,64 @@ func rawTxBinaryBuildAddressTable(txs []*RawTx) ([]solana.PublicKey, error) {
|
|||||||
return nil, fmt.Errorf("tx[%d].address_table_lookups[%d].account_key: %w", txIndex, lookupIndex, err)
|
return nil, fmt.Errorf("tx[%d].address_table_lookups[%d].account_key: %w", txIndex, lookupIndex, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for balanceIndex, balance := range tx.Meta.PreTokenBalances {
|
|
||||||
if err := rawTxBinaryAddTokenBalanceAddresses(&builder, balance); err != nil {
|
|
||||||
return nil, fmt.Errorf("tx[%d].pre_token_balances[%d]: %w", txIndex, balanceIndex, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for balanceIndex, balance := range tx.Meta.PostTokenBalances {
|
|
||||||
if err := rawTxBinaryAddTokenBalanceAddresses(&builder, balance); err != nil {
|
|
||||||
return nil, fmt.Errorf("tx[%d].post_token_balances[%d]: %w", txIndex, balanceIndex, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return builder.addresses, nil
|
return builder.addresses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rawTxBinaryEffectiveAccountList(tx *RawTx) ([]solana.PublicKey, error) {
|
||||||
|
accountList := append([]solana.PublicKey(nil), tx.getAccountList()...)
|
||||||
|
seen := make(map[solana.PublicKey]struct{}, len(accountList))
|
||||||
|
for _, account := range accountList {
|
||||||
|
seen[account] = struct{}{}
|
||||||
|
}
|
||||||
|
for balanceIndex, balance := range tx.Meta.PreTokenBalances {
|
||||||
|
if err := rawTxBinaryAppendTokenBalanceAccounts(&accountList, seen, balance); err != nil {
|
||||||
|
return nil, fmt.Errorf("pre_token_balances[%d]: %w", balanceIndex, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for balanceIndex, balance := range tx.Meta.PostTokenBalances {
|
||||||
|
if err := rawTxBinaryAppendTokenBalanceAccounts(&accountList, seen, balance); err != nil {
|
||||||
|
return nil, fmt.Errorf("post_token_balances[%d]: %w", balanceIndex, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accountList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rawTxBinaryAppendTokenBalanceAccounts(accountList *[]solana.PublicKey, seen map[solana.PublicKey]struct{}, balance TokenBalance) error {
|
||||||
|
mintAccount, ownerAccount, programIDAccount, err := rawTxBinaryTokenBalanceAccounts(balance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rawTxBinaryAppendAccountIfMissing(accountList, seen, mintAccount)
|
||||||
|
if ownerAccount != nil {
|
||||||
|
rawTxBinaryAppendAccountIfMissing(accountList, seen, *ownerAccount)
|
||||||
|
}
|
||||||
|
rawTxBinaryAppendAccountIfMissing(accountList, seen, programIDAccount)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rawTxBinaryAppendAccountIfMissing(accountList *[]solana.PublicKey, seen map[solana.PublicKey]struct{}, account solana.PublicKey) {
|
||||||
|
if _, exists := seen[account]; exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[account] = struct{}{}
|
||||||
|
*accountList = append(*accountList, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRawTxBinaryAccountListIndex(accountList []solana.PublicKey) (map[solana.PublicKey]uint8, error) {
|
||||||
|
out := make(map[solana.PublicKey]uint8, len(accountList))
|
||||||
|
for i, account := range accountList {
|
||||||
|
if i > math.MaxUint8 {
|
||||||
|
return nil, fmt.Errorf("account_list index overflows uint8: %d", i)
|
||||||
|
}
|
||||||
|
if _, exists := out[account]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[account] = uint8(i)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func rawTxBinarySharedBlockTime(txs []*RawTx, field string) (int64, error) {
|
func rawTxBinarySharedBlockTime(txs []*RawTx, field string) (int64, error) {
|
||||||
if len(txs) == 0 {
|
if len(txs) == 0 {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
@@ -1672,6 +1733,7 @@ func cloneInstructions(values []Instruction) []Instruction {
|
|||||||
Accounts: append([]int(nil), value.Accounts...),
|
Accounts: append([]int(nil), value.Accounts...),
|
||||||
Data: append(solana.Base58(nil), value.Data...),
|
Data: append(solana.Base58(nil), value.Data...),
|
||||||
ProgramIDIndex: value.ProgramIDIndex,
|
ProgramIDIndex: value.ProgramIDIndex,
|
||||||
|
LogEvents: cloneBase64Slice(value.LogEvents),
|
||||||
}
|
}
|
||||||
if value.StackHeight != nil {
|
if value.StackHeight != nil {
|
||||||
stackHeight := *value.StackHeight
|
stackHeight := *value.StackHeight
|
||||||
@@ -1682,6 +1744,14 @@ func cloneInstructions(values []Instruction) []Instruction {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cloneBase64Slice(values []solana.Base64) []solana.Base64 {
|
||||||
|
out := make([]solana.Base64, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
out = append(out, append(solana.Base64(nil), value...))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func rawTxBinaryVersionID(version interface{}) uint8 {
|
func rawTxBinaryVersionID(version interface{}) uint8 {
|
||||||
switch value := version.(type) {
|
switch value := version.(type) {
|
||||||
case solana.MessageVersion:
|
case solana.MessageVersion:
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
81
tx_binary.go
81
tx_binary.go
@@ -17,7 +17,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
txBinarySchemaVersionCurrent uint16 = 3
|
txBinarySchemaVersionV3 uint16 = 3
|
||||||
|
txBinarySchemaVersionCurrent uint16 = 4
|
||||||
txBinaryEnumVersionV1 uint16 = 1
|
txBinaryEnumVersionV1 uint16 = 1
|
||||||
|
|
||||||
txBinarySOLScale int32 = 9
|
txBinarySOLScale int32 = 9
|
||||||
@@ -27,6 +28,10 @@ const (
|
|||||||
var txBinaryMagic = [4]byte{'P', 'T', 'X', 'B'}
|
var txBinaryMagic = [4]byte{'P', 'T', 'X', 'B'}
|
||||||
var txsBinaryMagic = [4]byte{'P', 'T', 'X', 'S'}
|
var txsBinaryMagic = [4]byte{'P', 'T', 'X', 'S'}
|
||||||
|
|
||||||
|
func txBinarySchemaVersionSupported(version uint16) bool {
|
||||||
|
return version >= txBinarySchemaVersionV3 && version <= txBinarySchemaVersionCurrent
|
||||||
|
}
|
||||||
|
|
||||||
type TxBinary struct {
|
type TxBinary struct {
|
||||||
SchemaVersion uint16
|
SchemaVersion uint16
|
||||||
EnumVersion uint16
|
EnumVersion uint16
|
||||||
@@ -34,6 +39,7 @@ type TxBinary struct {
|
|||||||
Signer uint32
|
Signer uint32
|
||||||
Block uint64
|
Block uint64
|
||||||
BlockIndex uint64
|
BlockIndex uint64
|
||||||
|
BlockAt int64
|
||||||
TxHash *[64]byte
|
TxHash *[64]byte
|
||||||
CuFee uint64
|
CuFee uint64
|
||||||
Swaps []SwapBinary
|
Swaps []SwapBinary
|
||||||
@@ -204,6 +210,7 @@ func newTxBinaryWithAddressTable(tx *Tx, addressTable []solana.PublicKey, addres
|
|||||||
EnumVersion: txBinaryEnumVersionV1,
|
EnumVersion: txBinaryEnumVersionV1,
|
||||||
Block: tx.Block,
|
Block: tx.Block,
|
||||||
BlockIndex: tx.BlockIndex,
|
BlockIndex: tx.BlockIndex,
|
||||||
|
BlockAt: tx.BlockAt,
|
||||||
CuLimit: tx.CuLimit,
|
CuLimit: tx.CuLimit,
|
||||||
ComputeUnitsConsumed: tx.ComputeUnitsConsumed,
|
ComputeUnitsConsumed: tx.ComputeUnitsConsumed,
|
||||||
}
|
}
|
||||||
@@ -411,6 +418,8 @@ func MergeTxsBinarySourcesToWriterWithOptions(sources []TxsBinaryReaderSource, w
|
|||||||
return fmt.Errorf("source[%d].batch[%d].tx[%d]: %w", sourceIndex, batchIndex, txIndex, err)
|
return fmt.Errorf("source[%d].batch[%d].tx[%d]: %w", sourceIndex, batchIndex, txIndex, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tx.SchemaVersion = plan.schemaVersion
|
||||||
|
tx.EnumVersion = plan.enumVersion
|
||||||
bodyBytes, err := txBinaryMarshalTxBody(&tx, plan.enumTable)
|
bodyBytes, err := txBinaryMarshalTxBody(&tx, plan.enumTable)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reader.Close()
|
reader.Close()
|
||||||
@@ -432,7 +441,7 @@ func (tx *TxBinary) MarshalBinary() ([]byte, error) {
|
|||||||
if tx == nil {
|
if tx == nil {
|
||||||
return nil, fmt.Errorf("tx binary is nil")
|
return nil, fmt.Errorf("tx binary is nil")
|
||||||
}
|
}
|
||||||
if tx.SchemaVersion != txBinarySchemaVersionCurrent {
|
if !txBinarySchemaVersionSupported(tx.SchemaVersion) {
|
||||||
return nil, fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion)
|
return nil, fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,7 +469,7 @@ func (txs *TxsBinary) MarshalBinary() ([]byte, error) {
|
|||||||
if txs == nil {
|
if txs == nil {
|
||||||
return nil, fmt.Errorf("txs binary is nil")
|
return nil, fmt.Errorf("txs binary is nil")
|
||||||
}
|
}
|
||||||
if txs.SchemaVersion != txBinarySchemaVersionCurrent {
|
if !txBinarySchemaVersionSupported(txs.SchemaVersion) {
|
||||||
return nil, fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion)
|
return nil, fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,8 +487,11 @@ func (txs *TxsBinary) MarshalBinary() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
enc.writeUint32(uint32(len(txs.Txs)))
|
enc.writeUint32(uint32(len(txs.Txs)))
|
||||||
for i := range txs.Txs {
|
for i := range txs.Txs {
|
||||||
if err := enc.writeTxBinaryBody(&txs.Txs[i], enumTable); err != nil {
|
tx := txs.Txs[i]
|
||||||
return nil, fmt.Errorf("tx[%d], %s: %w", i, base58.Encode(txs.Txs[i].TxHash[:]), err)
|
tx.SchemaVersion = txs.SchemaVersion
|
||||||
|
tx.EnumVersion = txs.EnumVersion
|
||||||
|
if err := enc.writeTxBinaryBody(&tx, enumTable); err != nil {
|
||||||
|
return nil, fmt.Errorf("tx[%d], %s: %w", i, base58.Encode(tx.TxHash[:]), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return enc.bytes(), nil
|
return enc.bytes(), nil
|
||||||
@@ -520,7 +532,7 @@ func (tx *TxBinary) UnmarshalBinary(data []byte) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if tx.SchemaVersion != txBinarySchemaVersionCurrent {
|
if !txBinarySchemaVersionSupported(tx.SchemaVersion) {
|
||||||
return fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion)
|
return fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,7 +572,7 @@ func (txs *TxsBinary) UnmarshalBinary(data []byte) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if txs.SchemaVersion != txBinarySchemaVersionCurrent {
|
if !txBinarySchemaVersionSupported(txs.SchemaVersion) {
|
||||||
return fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion)
|
return fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,6 +625,7 @@ func (tx *TxBinary) ToTx() (*Tx, error) {
|
|||||||
Signer: signer,
|
Signer: signer,
|
||||||
Block: tx.Block,
|
Block: tx.Block,
|
||||||
BlockIndex: tx.BlockIndex,
|
BlockIndex: tx.BlockIndex,
|
||||||
|
BlockAt: tx.BlockAt,
|
||||||
CuFee: decimal.NewFromUint64(tx.CuFee),
|
CuFee: decimal.NewFromUint64(tx.CuFee),
|
||||||
CUPrice: decimal.NewFromUint64(tx.CUPrice).Shift(-txBinaryCUPriceScale),
|
CUPrice: decimal.NewFromUint64(tx.CUPrice).Shift(-txBinaryCUPriceScale),
|
||||||
BeforeSolBalance: txBinaryFloat64ToDecimal(tx.BeforeSolBalance, txBinarySOLScale),
|
BeforeSolBalance: txBinaryFloat64ToDecimal(tx.BeforeSolBalance, txBinarySOLScale),
|
||||||
@@ -1166,6 +1179,9 @@ func (enc *txBinaryEncoder) writeTxBinaryBody(tx *TxBinary, enumTable *txBinaryE
|
|||||||
enc.writeUint32(tx.Signer)
|
enc.writeUint32(tx.Signer)
|
||||||
enc.writeUint64(tx.Block)
|
enc.writeUint64(tx.Block)
|
||||||
enc.writeUint64(tx.BlockIndex)
|
enc.writeUint64(tx.BlockIndex)
|
||||||
|
if tx.SchemaVersion >= txBinarySchemaVersionCurrent {
|
||||||
|
enc.writeUint64(uint64(tx.BlockAt))
|
||||||
|
}
|
||||||
enc.writeBool(tx.TxHash != nil)
|
enc.writeBool(tx.TxHash != nil)
|
||||||
if tx.TxHash != nil {
|
if tx.TxHash != nil {
|
||||||
enc.writeBytes(tx.TxHash[:])
|
enc.writeBytes(tx.TxHash[:])
|
||||||
@@ -1191,7 +1207,7 @@ func (enc *txBinaryEncoder) writeTxBinaryBody(tx *TxBinary, enumTable *txBinaryE
|
|||||||
func (enc *txBinaryEncoder) writePlatformEntries(entries []PlatformBinary, enumTable *txBinaryEnumTable) error {
|
func (enc *txBinaryEncoder) writePlatformEntries(entries []PlatformBinary, enumTable *txBinaryEnumTable) error {
|
||||||
enc.writeUint32(uint32(len(entries)))
|
enc.writeUint32(uint32(len(entries)))
|
||||||
for i, entry := range entries {
|
for i, entry := range entries {
|
||||||
enumID, err := enumTable.platforms.id(entry.Platform)
|
enumID, err := enumTable.platforms.idOrFallback(entry.Platform, PlatformNone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("platform[%d]: %w", i, err)
|
return fmt.Errorf("platform[%d]: %w", i, err)
|
||||||
}
|
}
|
||||||
@@ -1204,7 +1220,7 @@ func (enc *txBinaryEncoder) writePlatformEntries(entries []PlatformBinary, enumT
|
|||||||
func (enc *txBinaryEncoder) writeMevAgentEntries(entries []MevAgentBinary, enumTable *txBinaryEnumTable) error {
|
func (enc *txBinaryEncoder) writeMevAgentEntries(entries []MevAgentBinary, enumTable *txBinaryEnumTable) error {
|
||||||
enc.writeUint32(uint32(len(entries)))
|
enc.writeUint32(uint32(len(entries)))
|
||||||
for i, entry := range entries {
|
for i, entry := range entries {
|
||||||
enumID, err := enumTable.mevAgents.id(entry.MevAgent)
|
enumID, err := enumTable.mevAgents.idOrFallback(entry.MevAgent, MevAgentUnknown)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("mev_agent[%d]: %w", i, err)
|
return fmt.Errorf("mev_agent[%d]: %w", i, err)
|
||||||
}
|
}
|
||||||
@@ -1474,7 +1490,7 @@ func (dec *txBinaryStreamDecoder) readTxsBinaryHeader() (*txsBinaryHeader, error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if schemaVersion != txBinarySchemaVersionCurrent {
|
if !txBinarySchemaVersionSupported(schemaVersion) {
|
||||||
return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion)
|
return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1531,7 +1547,7 @@ func (dec *txBinaryStreamDecoder) readTxsBinaryHeaderOrEOF() (*txsBinaryHeader,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if schemaVersion != txBinarySchemaVersionCurrent {
|
if !txBinarySchemaVersionSupported(schemaVersion) {
|
||||||
return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion)
|
return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1592,7 +1608,7 @@ func txBinaryReadPlatformEntries(dec txBinaryBodyReader, enumTable *txBinaryEnum
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
platform, err := enumTable.platforms.value(enumID)
|
platform, err := enumTable.platforms.valueOrFallback(enumID, PlatformNone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("platform[%d]: %w", i, err)
|
return nil, fmt.Errorf("platform[%d]: %w", i, err)
|
||||||
}
|
}
|
||||||
@@ -1619,7 +1635,7 @@ func txBinaryReadMevAgentEntries(dec txBinaryBodyReader, enumTable *txBinaryEnum
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
mevAgent, err := enumTable.mevAgents.value(enumID)
|
mevAgent, err := enumTable.mevAgents.valueOrFallback(enumID, MevAgentUnknown)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("mev_agent[%d]: %w", i, err)
|
return nil, fmt.Errorf("mev_agent[%d]: %w", i, err)
|
||||||
}
|
}
|
||||||
@@ -1799,6 +1815,13 @@ func txBinaryReadTxBody(dec txBinaryBodyReader, tx *TxBinary, enumTable *txBinar
|
|||||||
if tx.BlockIndex, err = dec.readUint64(); err != nil {
|
if tx.BlockIndex, err = dec.readUint64(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if tx.SchemaVersion >= txBinarySchemaVersionCurrent {
|
||||||
|
blockAt, err := dec.readUint64()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tx.BlockAt = int64(blockAt)
|
||||||
|
}
|
||||||
|
|
||||||
hasTxHash, err := dec.readBool()
|
hasTxHash, err := dec.readBool()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1854,7 +1877,9 @@ func txBinaryBuildMergePlan(sources []TxsBinaryReaderSource, opts TxsBinaryMerge
|
|||||||
builder := txBinaryAddressTableBuilder{
|
builder := txBinaryAddressTableBuilder{
|
||||||
index: make(map[solana.PublicKey]struct{}),
|
index: make(map[solana.PublicKey]struct{}),
|
||||||
}
|
}
|
||||||
plan := &txsBinaryMergePlan{}
|
plan := &txsBinaryMergePlan{
|
||||||
|
schemaVersion: txBinarySchemaVersionCurrent,
|
||||||
|
}
|
||||||
hasBatch := false
|
hasBatch := false
|
||||||
|
|
||||||
for sourceIndex, source := range sources {
|
for sourceIndex, source := range sources {
|
||||||
@@ -1896,15 +1921,10 @@ func txBinaryBuildMergePlan(sources []TxsBinaryReaderSource, opts TxsBinaryMerge
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !hasBatch {
|
if !hasBatch {
|
||||||
plan.schemaVersion = header.schemaVersion
|
|
||||||
plan.enumVersion = header.enumVersion
|
plan.enumVersion = header.enumVersion
|
||||||
plan.enumTable = header.enumTable
|
plan.enumTable = header.enumTable
|
||||||
hasBatch = true
|
hasBatch = true
|
||||||
} else {
|
} else {
|
||||||
if header.schemaVersion != plan.schemaVersion {
|
|
||||||
reader.Close()
|
|
||||||
return nil, fmt.Errorf("source[%d].batch[%d]: schema version mismatch: got %d want %d", sourceIndex, batchIndex, header.schemaVersion, plan.schemaVersion)
|
|
||||||
}
|
|
||||||
if header.enumVersion != plan.enumVersion {
|
if header.enumVersion != plan.enumVersion {
|
||||||
reader.Close()
|
reader.Close()
|
||||||
return nil, fmt.Errorf("source[%d].batch[%d]: enum version mismatch: got %d want %d", sourceIndex, batchIndex, header.enumVersion, plan.enumVersion)
|
return nil, fmt.Errorf("source[%d].batch[%d]: enum version mismatch: got %d want %d", sourceIndex, batchIndex, header.enumVersion, plan.enumVersion)
|
||||||
@@ -2143,6 +2163,8 @@ var txBinaryEnumTables = map[uint16]*txBinaryEnumTable{
|
|||||||
MevAgentSpeedlanding,
|
MevAgentSpeedlanding,
|
||||||
MevAgentAllenhark,
|
MevAgentAllenhark,
|
||||||
MevAgentRaiden,
|
MevAgentRaiden,
|
||||||
|
MevAgentZan,
|
||||||
|
MevAgentTunneling,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -2198,9 +2220,30 @@ func (set txBinaryEnumSet) id(value string) (uint16, error) {
|
|||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (set txBinaryEnumSet) idOrFallback(value string, fallback string) (uint16, error) {
|
||||||
|
if id, ok := set.ids[value]; ok {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
id, ok := set.ids[fallback]
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("unsupported %s fallback enum value %q for versioned tx binary", set.name, fallback)
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (set txBinaryEnumSet) value(id uint16) (string, error) {
|
func (set txBinaryEnumSet) value(id uint16) (string, error) {
|
||||||
if int(id) >= len(set.values) {
|
if int(id) >= len(set.values) {
|
||||||
return "", fmt.Errorf("unknown %s enum id %d", set.name, id)
|
return "", fmt.Errorf("unknown %s enum id %d", set.name, id)
|
||||||
}
|
}
|
||||||
return set.values[id], nil
|
return set.values[id], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (set txBinaryEnumSet) valueOrFallback(id uint16, fallback string) (string, error) {
|
||||||
|
if int(id) < len(set.values) {
|
||||||
|
return set.values[id], nil
|
||||||
|
}
|
||||||
|
if _, ok := set.ids[fallback]; !ok {
|
||||||
|
return "", fmt.Errorf("unsupported %s fallback enum value %q for versioned tx binary", set.name, fallback)
|
||||||
|
}
|
||||||
|
return fallback, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func TestTxBinaryRoundTrip(t *testing.T) {
|
|||||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
Block: 123456789,
|
Block: 123456789,
|
||||||
BlockIndex: 42,
|
BlockIndex: 42,
|
||||||
|
BlockAt: 1710000000,
|
||||||
TxHash: &txHash,
|
TxHash: &txHash,
|
||||||
CuFee: decimal.NewFromInt(5000),
|
CuFee: decimal.NewFromInt(5000),
|
||||||
CUPrice: decimal.RequireFromString("0.123456"),
|
CUPrice: decimal.RequireFromString("0.123456"),
|
||||||
@@ -41,6 +42,14 @@ func TestTxBinaryRoundTrip(t *testing.T) {
|
|||||||
MevAgent: MevAgentJito,
|
MevAgent: MevAgentJito,
|
||||||
MevAgentFee: decimal.RequireFromString("0.030000000"),
|
MevAgentFee: decimal.RequireFromString("0.030000000"),
|
||||||
},
|
},
|
||||||
|
MevAgentZan: {
|
||||||
|
MevAgent: MevAgentZan,
|
||||||
|
MevAgentFee: decimal.RequireFromString("0.040000000"),
|
||||||
|
},
|
||||||
|
MevAgentTunneling: {
|
||||||
|
MevAgent: MevAgentTunneling,
|
||||||
|
MevAgentFee: decimal.RequireFromString("0.050000000"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Swaps: []Swap{
|
Swaps: []Swap{
|
||||||
{
|
{
|
||||||
@@ -110,6 +119,9 @@ func TestTxBinaryRoundTrip(t *testing.T) {
|
|||||||
if decoded.BlockIndex != original.BlockIndex {
|
if decoded.BlockIndex != original.BlockIndex {
|
||||||
t.Fatalf("BlockIndex = %d, want %d", decoded.BlockIndex, original.BlockIndex)
|
t.Fatalf("BlockIndex = %d, want %d", decoded.BlockIndex, original.BlockIndex)
|
||||||
}
|
}
|
||||||
|
if decoded.BlockAt != original.BlockAt {
|
||||||
|
t.Fatalf("BlockAt = %d, want %d", decoded.BlockAt, original.BlockAt)
|
||||||
|
}
|
||||||
if decoded.TxHash == nil {
|
if decoded.TxHash == nil {
|
||||||
t.Fatal("TxHash = nil, want non-nil")
|
t.Fatal("TxHash = nil, want non-nil")
|
||||||
}
|
}
|
||||||
@@ -146,6 +158,12 @@ func TestTxBinaryRoundTrip(t *testing.T) {
|
|||||||
if !decoded.MevAgent[MevAgentJito].MevAgentFee.Equal(original.MevAgent[MevAgentJito].MevAgentFee) {
|
if !decoded.MevAgent[MevAgentJito].MevAgentFee.Equal(original.MevAgent[MevAgentJito].MevAgentFee) {
|
||||||
t.Fatalf("MevAgent fee mismatch")
|
t.Fatalf("MevAgent fee mismatch")
|
||||||
}
|
}
|
||||||
|
if !decoded.MevAgent[MevAgentZan].MevAgentFee.Equal(original.MevAgent[MevAgentZan].MevAgentFee) {
|
||||||
|
t.Fatalf("Zan MevAgent fee mismatch")
|
||||||
|
}
|
||||||
|
if !decoded.MevAgent[MevAgentTunneling].MevAgentFee.Equal(original.MevAgent[MevAgentTunneling].MevAgentFee) {
|
||||||
|
t.Fatalf("Tunneling MevAgent fee mismatch")
|
||||||
|
}
|
||||||
if len(decoded.Swaps) != 1 {
|
if len(decoded.Swaps) != 1 {
|
||||||
t.Fatalf("Swaps len = %d, want 1", len(decoded.Swaps))
|
t.Fatalf("Swaps len = %d, want 1", len(decoded.Swaps))
|
||||||
}
|
}
|
||||||
@@ -225,6 +243,87 @@ func TestTxBinaryRejectsUnknownProgramEnum(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTxBinaryLabelEnumsFallbackToUnknown(t *testing.T) {
|
||||||
|
original := &Tx{
|
||||||
|
Signer: solana.WrappedSol,
|
||||||
|
Platform: map[string]platformInfo{
|
||||||
|
"future-platform": {
|
||||||
|
Platform: "future-platform",
|
||||||
|
PlatformFee: decimal.RequireFromString("0.010000000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MevAgent: map[string]mevInfo{
|
||||||
|
"future-mev-agent": {
|
||||||
|
MevAgent: "future-mev-agent",
|
||||||
|
MevAgentFee: decimal.RequireFromString("0.020000000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := EncodeTxBinary(original)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
decoded, err := DecodeTxBinary(encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(decoded.Platform) != 1 {
|
||||||
|
t.Fatalf("Platform len = %d, want 1", len(decoded.Platform))
|
||||||
|
}
|
||||||
|
if _, exists := decoded.Platform["future-platform"]; exists {
|
||||||
|
t.Fatalf("future platform was preserved, want fallback")
|
||||||
|
}
|
||||||
|
if !decoded.Platform[PlatformNone].PlatformFee.Equal(original.Platform["future-platform"].PlatformFee) {
|
||||||
|
t.Fatalf("PlatformNone fee = %s, want %s", decoded.Platform[PlatformNone].PlatformFee, original.Platform["future-platform"].PlatformFee)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(decoded.MevAgent) != 1 {
|
||||||
|
t.Fatalf("MevAgent len = %d, want 1", len(decoded.MevAgent))
|
||||||
|
}
|
||||||
|
if _, exists := decoded.MevAgent["future-mev-agent"]; exists {
|
||||||
|
t.Fatalf("future mev agent was preserved, want fallback")
|
||||||
|
}
|
||||||
|
if !decoded.MevAgent[MevAgentUnknown].MevAgentFee.Equal(original.MevAgent["future-mev-agent"].MevAgentFee) {
|
||||||
|
t.Fatalf("MevAgentUnknown fee = %s, want %s", decoded.MevAgent[MevAgentUnknown].MevAgentFee, original.MevAgent["future-mev-agent"].MevAgentFee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTxBinaryReadLabelEnumUnknownIDsFallback(t *testing.T) {
|
||||||
|
enumTable := txBinaryEnumTables[txBinaryEnumVersionV1]
|
||||||
|
|
||||||
|
platformFee := uint64(123)
|
||||||
|
platformEnc := txBinaryEncoder{}
|
||||||
|
platformEnc.writeUint32(1)
|
||||||
|
platformEnc.writeUint16(uint16(len(enumTable.platforms.values) + 10))
|
||||||
|
platformEnc.writeUint64(platformFee)
|
||||||
|
|
||||||
|
platformDec := txBinaryDecoder{reader: bytes.NewReader(platformEnc.bytes())}
|
||||||
|
platforms, err := txBinaryReadPlatformEntries(&platformDec, enumTable)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("txBinaryReadPlatformEntries() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(platforms) != 1 || platforms[0].Platform != PlatformNone || platforms[0].PlatformFee != platformFee {
|
||||||
|
t.Fatalf("platform fallback = %+v, want %s/%d", platforms, PlatformNone, platformFee)
|
||||||
|
}
|
||||||
|
|
||||||
|
mevFee := uint64(456)
|
||||||
|
mevEnc := txBinaryEncoder{}
|
||||||
|
mevEnc.writeUint32(1)
|
||||||
|
mevEnc.writeUint16(uint16(len(enumTable.mevAgents.values) + 10))
|
||||||
|
mevEnc.writeUint64(mevFee)
|
||||||
|
|
||||||
|
mevDec := txBinaryDecoder{reader: bytes.NewReader(mevEnc.bytes())}
|
||||||
|
mevAgents, err := txBinaryReadMevAgentEntries(&mevDec, enumTable)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("txBinaryReadMevAgentEntries() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(mevAgents) != 1 || mevAgents[0].MevAgent != MevAgentUnknown || mevAgents[0].MevAgentFee != mevFee {
|
||||||
|
t.Fatalf("mev agent fallback = %+v, want %s/%d", mevAgents, MevAgentUnknown, mevFee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTxBinaryAcceptsKnownEventEnums(t *testing.T) {
|
func TestTxBinaryAcceptsKnownEventEnums(t *testing.T) {
|
||||||
events := []string{
|
events := []string{
|
||||||
TxEventAddLP,
|
TxEventAddLP,
|
||||||
@@ -415,6 +514,7 @@ func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
|
|||||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
Block: 1,
|
Block: 1,
|
||||||
BlockIndex: 1,
|
BlockIndex: 1,
|
||||||
|
BlockAt: 1710000001,
|
||||||
CuFee: decimal.NewFromInt(1000),
|
CuFee: decimal.NewFromInt(1000),
|
||||||
CUPrice: decimal.RequireFromString("0.123456"),
|
CUPrice: decimal.RequireFromString("0.123456"),
|
||||||
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
@@ -465,6 +565,7 @@ func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
|
|||||||
tx2 := tx1
|
tx2 := tx1
|
||||||
tx2.Block = 2
|
tx2.Block = 2
|
||||||
tx2.BlockIndex = 2
|
tx2.BlockIndex = 2
|
||||||
|
tx2.BlockAt = 1710000002
|
||||||
tx2.CuFee = decimal.NewFromInt(2000)
|
tx2.CuFee = decimal.NewFromInt(2000)
|
||||||
tx2.AfterSOLBalance = decimal.RequireFromString("0.700000000")
|
tx2.AfterSOLBalance = decimal.RequireFromString("0.700000000")
|
||||||
tx2.Swaps = []Swap{tx1.Swaps[0]}
|
tx2.Swaps = []Swap{tx1.Swaps[0]}
|
||||||
@@ -486,6 +587,9 @@ func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
|
|||||||
if decoded[0].Signer != tx1.Signer || decoded[1].Signer != tx2.Signer {
|
if decoded[0].Signer != tx1.Signer || decoded[1].Signer != tx2.Signer {
|
||||||
t.Fatalf("decoded signer mismatch")
|
t.Fatalf("decoded signer mismatch")
|
||||||
}
|
}
|
||||||
|
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt {
|
||||||
|
t.Fatalf("decoded block_at mismatch")
|
||||||
|
}
|
||||||
if decoded[0].Swaps[0].Pool != tx1.Swaps[0].Pool || decoded[1].Swaps[0].Pool != tx2.Swaps[0].Pool {
|
if decoded[0].Swaps[0].Pool != tx1.Swaps[0].Pool || decoded[1].Swaps[0].Pool != tx2.Swaps[0].Pool {
|
||||||
t.Fatalf("decoded shared address mismatch")
|
t.Fatalf("decoded shared address mismatch")
|
||||||
}
|
}
|
||||||
@@ -508,6 +612,7 @@ func TestDecodeTxsBinaryReader(t *testing.T) {
|
|||||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
Block: 100,
|
Block: 100,
|
||||||
BlockIndex: 7,
|
BlockIndex: 7,
|
||||||
|
BlockAt: 1710000100,
|
||||||
CuFee: decimal.NewFromInt(111),
|
CuFee: decimal.NewFromInt(111),
|
||||||
CUPrice: decimal.RequireFromString("0.123456"),
|
CUPrice: decimal.RequireFromString("0.123456"),
|
||||||
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
@@ -554,6 +659,7 @@ func TestDecodeTxsBinaryReader(t *testing.T) {
|
|||||||
tx2 := tx1
|
tx2 := tx1
|
||||||
tx2.Block = 101
|
tx2.Block = 101
|
||||||
tx2.BlockIndex = 8
|
tx2.BlockIndex = 8
|
||||||
|
tx2.BlockAt = 1710000101
|
||||||
tx2.CuFee = decimal.NewFromInt(222)
|
tx2.CuFee = decimal.NewFromInt(222)
|
||||||
tx2.AfterSOLBalance = decimal.RequireFromString("0.300000000")
|
tx2.AfterSOLBalance = decimal.RequireFromString("0.300000000")
|
||||||
tx2.Swaps = []Swap{tx1.Swaps[0]}
|
tx2.Swaps = []Swap{tx1.Swaps[0]}
|
||||||
@@ -582,6 +688,9 @@ func TestDecodeTxsBinaryReader(t *testing.T) {
|
|||||||
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
|
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
|
||||||
t.Fatalf("decoded block mismatch")
|
t.Fatalf("decoded block mismatch")
|
||||||
}
|
}
|
||||||
|
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt {
|
||||||
|
t.Fatalf("decoded block_at mismatch")
|
||||||
|
}
|
||||||
if decoded[0].Swaps[0].BaseAmount.Cmp(tx1.Swaps[0].BaseAmount) != 0 {
|
if decoded[0].Swaps[0].BaseAmount.Cmp(tx1.Swaps[0].BaseAmount) != 0 {
|
||||||
t.Fatalf("decoded tx1 swap base amount = %s, want %s", decoded[0].Swaps[0].BaseAmount, tx1.Swaps[0].BaseAmount)
|
t.Fatalf("decoded tx1 swap base amount = %s, want %s", decoded[0].Swaps[0].BaseAmount, tx1.Swaps[0].BaseAmount)
|
||||||
}
|
}
|
||||||
@@ -629,6 +738,7 @@ func TestMergeTxsBinaryBytes(t *testing.T) {
|
|||||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
Block: 11,
|
Block: 11,
|
||||||
BlockIndex: 1,
|
BlockIndex: 1,
|
||||||
|
BlockAt: 1710000011,
|
||||||
CuFee: decimal.NewFromInt(10),
|
CuFee: decimal.NewFromInt(10),
|
||||||
CUPrice: decimal.RequireFromString("0.000123"),
|
CUPrice: decimal.RequireFromString("0.000123"),
|
||||||
BeforeSolBalance: decimal.RequireFromString("1.100000000"),
|
BeforeSolBalance: decimal.RequireFromString("1.100000000"),
|
||||||
@@ -660,6 +770,7 @@ func TestMergeTxsBinaryBytes(t *testing.T) {
|
|||||||
Signer: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
Signer: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
Block: 12,
|
Block: 12,
|
||||||
BlockIndex: 2,
|
BlockIndex: 2,
|
||||||
|
BlockAt: 1710000012,
|
||||||
CuFee: decimal.NewFromInt(20),
|
CuFee: decimal.NewFromInt(20),
|
||||||
CUPrice: decimal.RequireFromString("0.000456"),
|
CUPrice: decimal.RequireFromString("0.000456"),
|
||||||
BeforeSolBalance: decimal.RequireFromString("2.200000000"),
|
BeforeSolBalance: decimal.RequireFromString("2.200000000"),
|
||||||
@@ -723,6 +834,9 @@ func TestMergeTxsBinaryBytes(t *testing.T) {
|
|||||||
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
|
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
|
||||||
t.Fatalf("decoded block mismatch")
|
t.Fatalf("decoded block mismatch")
|
||||||
}
|
}
|
||||||
|
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt {
|
||||||
|
t.Fatalf("decoded block_at mismatch")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
|
func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
|
||||||
@@ -730,6 +844,7 @@ func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
|
|||||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
Block: 21,
|
Block: 21,
|
||||||
BlockIndex: 1,
|
BlockIndex: 1,
|
||||||
|
BlockAt: 1710000021,
|
||||||
CuFee: decimal.NewFromInt(1),
|
CuFee: decimal.NewFromInt(1),
|
||||||
CUPrice: decimal.RequireFromString("0.000001"),
|
CUPrice: decimal.RequireFromString("0.000001"),
|
||||||
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
@@ -740,9 +855,11 @@ func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
|
|||||||
tx2 := tx1
|
tx2 := tx1
|
||||||
tx2.Block = 22
|
tx2.Block = 22
|
||||||
tx2.BlockIndex = 2
|
tx2.BlockIndex = 2
|
||||||
|
tx2.BlockAt = 1710000022
|
||||||
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
|
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
|
||||||
tx3 := tx1
|
tx3 := tx1
|
||||||
tx3.Block = 23
|
tx3.Block = 23
|
||||||
|
tx3.BlockAt = 1710000023
|
||||||
tx3.BlockIndex = 3
|
tx3.BlockIndex = 3
|
||||||
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
|
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
|
||||||
|
|
||||||
@@ -785,6 +902,9 @@ func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
|
|||||||
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block || decoded[2].Block != tx3.Block {
|
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block || decoded[2].Block != tx3.Block {
|
||||||
t.Fatalf("decoded block order mismatch")
|
t.Fatalf("decoded block order mismatch")
|
||||||
}
|
}
|
||||||
|
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt || decoded[2].BlockAt != tx3.BlockAt {
|
||||||
|
t.Fatalf("decoded block_at order mismatch")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) {
|
func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) {
|
||||||
@@ -792,6 +912,7 @@ func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) {
|
|||||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
Block: 31,
|
Block: 31,
|
||||||
BlockIndex: 1,
|
BlockIndex: 1,
|
||||||
|
BlockAt: 1710000031,
|
||||||
CuFee: decimal.NewFromInt(1),
|
CuFee: decimal.NewFromInt(1),
|
||||||
CUPrice: decimal.RequireFromString("0.000001"),
|
CUPrice: decimal.RequireFromString("0.000001"),
|
||||||
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
@@ -802,10 +923,12 @@ func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) {
|
|||||||
tx2 := tx1
|
tx2 := tx1
|
||||||
tx2.Block = 32
|
tx2.Block = 32
|
||||||
tx2.BlockIndex = 2
|
tx2.BlockIndex = 2
|
||||||
|
tx2.BlockAt = 1710000032
|
||||||
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
|
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
|
||||||
tx3 := tx1
|
tx3 := tx1
|
||||||
tx3.Block = 33
|
tx3.Block = 33
|
||||||
tx3.BlockIndex = 3
|
tx3.BlockIndex = 3
|
||||||
|
tx3.BlockAt = 1710000033
|
||||||
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
|
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
|
||||||
|
|
||||||
batch1, err := EncodeTxsBinary([]Tx{tx1})
|
batch1, err := EncodeTxsBinary([]Tx{tx1})
|
||||||
@@ -865,15 +988,127 @@ func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) {
|
|||||||
if decoded[0].Block != tx1.Block || decoded[1].Block != tx3.Block {
|
if decoded[0].Block != tx1.Block || decoded[1].Block != tx3.Block {
|
||||||
t.Fatalf("decoded block order mismatch after skip")
|
t.Fatalf("decoded block order mismatch after skip")
|
||||||
}
|
}
|
||||||
|
if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx3.BlockAt {
|
||||||
|
t.Fatalf("decoded block_at order mismatch after skip")
|
||||||
|
}
|
||||||
if source.opens != 2 {
|
if source.opens != 2 {
|
||||||
t.Fatalf("source.opens = %d, want 2", source.opens)
|
t.Fatalf("source.opens = %d, want 2", source.opens)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTxBinaryDecodeSchemaV3LeavesBlockAtZero(t *testing.T) {
|
||||||
|
original := &Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 41,
|
||||||
|
BlockIndex: 1,
|
||||||
|
BlockAt: 1710000041,
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded := mustEncodeTxBinaryV3(t, original)
|
||||||
|
decoded, err := DecodeTxBinary(encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxBinary(v3) error = %v", err)
|
||||||
|
}
|
||||||
|
if decoded.Block != original.Block || decoded.BlockIndex != original.BlockIndex {
|
||||||
|
t.Fatalf("decoded block mismatch: got (%d,%d), want (%d,%d)", decoded.Block, decoded.BlockIndex, original.Block, original.BlockIndex)
|
||||||
|
}
|
||||||
|
if decoded.BlockAt != 0 {
|
||||||
|
t.Fatalf("BlockAt = %d, want 0 for legacy v3", decoded.BlockAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeTxsBinaryBytesUpgradesSchemaV3AndPreservesV4BlockAt(t *testing.T) {
|
||||||
|
legacyTx := Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 51,
|
||||||
|
BlockIndex: 1,
|
||||||
|
BlockAt: 1710000051,
|
||||||
|
}
|
||||||
|
currentTx := Tx{
|
||||||
|
Signer: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
|
Block: 52,
|
||||||
|
BlockIndex: 2,
|
||||||
|
BlockAt: 1710000052,
|
||||||
|
}
|
||||||
|
|
||||||
|
merged, err := MergeTxsBinaryBytes([][]byte{
|
||||||
|
mustEncodeTxsBinaryV3(t, []Tx{legacyTx}),
|
||||||
|
mustEncodeTxsBinary(t, []Tx{currentTx}),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MergeTxsBinaryBytes(v3,v4) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mergedBinary TxsBinary
|
||||||
|
if err := mergedBinary.UnmarshalBinary(merged); err != nil {
|
||||||
|
t.Fatalf("UnmarshalBinary(merged) error = %v", err)
|
||||||
|
}
|
||||||
|
if mergedBinary.SchemaVersion != txBinarySchemaVersionCurrent {
|
||||||
|
t.Fatalf("merged schema version = %d, want %d", mergedBinary.SchemaVersion, txBinarySchemaVersionCurrent)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := DecodeTxsBinary(merged)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxsBinary(merged) error = %v", err)
|
||||||
|
}
|
||||||
|
if len(decoded) != 2 {
|
||||||
|
t.Fatalf("decoded len = %d, want 2", len(decoded))
|
||||||
|
}
|
||||||
|
if decoded[0].BlockAt != 0 {
|
||||||
|
t.Fatalf("legacy BlockAt = %d, want 0", decoded[0].BlockAt)
|
||||||
|
}
|
||||||
|
if decoded[1].BlockAt != currentTx.BlockAt {
|
||||||
|
t.Fatalf("current BlockAt = %d, want %d", decoded[1].BlockAt, currentTx.BlockAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mustPubKey(value string) solana.PublicKey {
|
func mustPubKey(value string) solana.PublicKey {
|
||||||
return solana.MustPublicKeyFromBase58(value)
|
return solana.MustPublicKeyFromBase58(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustEncodeTxBinaryV3(t *testing.T, tx *Tx) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
binaryTx, err := NewTxBinary(tx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
binaryTx.SchemaVersion = txBinarySchemaVersionV3
|
||||||
|
encoded, err := binaryTx.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MarshalBinary(v3) error = %v", err)
|
||||||
|
}
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustEncodeTxsBinary(t *testing.T, txs []Tx) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
encoded, err := EncodeTxsBinary(txs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustEncodeTxsBinaryV3(t *testing.T, txs []Tx) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
binaryTxs, err := NewTxsBinary(txs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewTxsBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
binaryTxs.SchemaVersion = txBinarySchemaVersionV3
|
||||||
|
for i := range binaryTxs.Txs {
|
||||||
|
binaryTxs.Txs[i].SchemaVersion = txBinarySchemaVersionV3
|
||||||
|
}
|
||||||
|
encoded, err := binaryTxs.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MarshalBinary(v3) error = %v", err)
|
||||||
|
}
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
func mustTxBinary(t *testing.T, data []byte) *TxsBinary {
|
func mustTxBinary(t *testing.T, data []byte) *TxsBinary {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user