Compare commits

...

11 Commits

Author SHA1 Message Date
thloyi
9f17ffce61 fix accounts len check 2026-05-28 10:23:56 +08:00
thloyi
e4eaddec4e reademe 2026-05-18 14:22:48 +08:00
thloyi
9454c3f6c7 ignore unknonw meta 2026-05-18 11:46:49 +08:00
thloyi
39bfeb085f fix pump wrapper buy and sell 2026-05-13 17:30:06 +08:00
thloyi
10885d5e08 fix pump wrapper buy and sell 2026-05-13 17:16:45 +08:00
thloyi
2406f6d087 fix pump wrapper buy and sell 2026-05-13 17:07:47 +08:00
thloyi
8b608889cb fix pump wrapper buy and sell 2026-05-13 16:54:23 +08:00
cachalots
8d4aad1932 tip 2026-05-13 16:52:55 +08:00
thloyi
5cd3a97d81 pump usdc support 2026-05-08 11:21:30 +08:00
thloyi
0a4aabc67f raw tx binary 2026-04-30 17:56:35 +08:00
thloyi
d46e8b651c raw tx binary 2026-04-30 17:02:04 +08:00
24 changed files with 2255 additions and 226 deletions

249
README.md Normal file
View File

@@ -0,0 +1,249 @@
# pump-parser
Solana transaction parser focused on swap, liquidity, migration, platform, MEV, compute budget, and compact binary persistence workflows.
The package works with a normalized `RawTx` representation, parses it into `Tx`, and emits one or more `Swap` records when a supported protocol action is found.
## Features
- Parse Solana RPC / Yellowstone transactions into local `RawTx`.
- Extract swap and liquidity events into `Tx.Swaps`.
- Preserve transaction metadata such as slot, block time, signer, fee, CU limit, CU consumed, token balances, platform fees, and MEV agent hints.
- Encode/decode parsed transactions with the `PTXB` / `PTXS` binary formats.
- Encode/decode raw transactions with the `PRTX` / `PRTS` / `PRBS` binary formats.
- Stream decode large `PTXS` / `PRTS` payloads without loading every transaction into memory.
- Merge `PTXS` batches while remapping address tables.
## Supported Parsers
Default parser initialization enables the hot-path Pump parsers only:
- Pump
- Pump AMM
`EnableAllParsers()` additionally enables:
- Meteora DLMM
- Meteora Pools
- Meteora DAMM v2
- Meteora Bonding Curve
- Orca Whirlpool
- Raydium AMM v4
- Raydium CLMM
- Raydium CPMM
- Raydium LaunchLab
Use `InitParser(WithMeteoraDlmm())` when only Meteora DLMM should be added to the default parser set.
## Installation
```bash
go get github.com/thloyi/pump-parser
```
This module currently declares `go 1.25.1` in `go.mod`.
## Basic Usage
```go
package main
import (
"fmt"
"log"
pump_parser "github.com/thloyi/pump-parser"
)
func main() {
// Enable all known parser programs. Omit this call to keep the default
// Pump + Pump AMM parser set.
pump_parser.EnableAllParsers()
var rawTx *pump_parser.RawTx
// Fill rawTx from RPC, Yellowstone, JSON, or RawTx binary decoding.
tx, err := pump_parser.ParseRawTx(rawTx)
if err != nil {
log.Fatal(err)
}
fmt.Println("tx:", tx.GetTxHash())
for _, swap := range tx.Swaps {
fmt.Printf("%s %s base=%s quote=%s pool=%s user=%s\n",
swap.Program,
swap.Event,
swap.BaseAmount,
swap.QuoteAmount,
swap.Pool,
swap.User,
)
}
}
```
## Swap Result
`ParseRawTx` returns a `Tx`. A transaction can contain zero, one, or multiple parsed swap records in `Tx.Swaps`.
Each `Swap` describes one protocol-level swap, liquidity, migration, or pool operation that the parser can normalize. The most important fields are:
| Field | Meaning |
| --- | --- |
| `Program` | Normalized protocol name, such as `Pump`, `PumpAMM`, `MeteoraDLMM`, `RaydiumV4`, or `OrcaWhirPool`. |
| `Event` | Normalized action, such as `buy`, `sell`, `add_liquidity`, `remove_liquidity`, `create`, `complete`, or `migrate`. |
| `TxIndex` | Approximate execution order across outer and inner instructions. |
| `InstrIdx` / `InnerIdx` | Outer instruction index and inner instruction index where the swap was found. |
| `Pool` | Pool, pair, bonding curve, or market account for the parsed action. |
| `BaseMint` / `QuoteMint` | Base and quote mint accounts after parser normalization. |
| `BaseTokenProgram` / `QuoteTokenProgram` | Token program for each side, useful when Token-2022 is involved. |
| `BaseMintDecimals` / `QuoteMintDecimals` | Decimals used to interpret raw token amounts. |
| `User` | User or effective owner account for the action. If the parsed user is not on-curve, the parser may fall back to the transaction signer. |
| `BaseAmount` / `QuoteAmount` | Actual parsed base-side and quote-side amounts, stored as `decimal.Decimal`. |
| `BaseReserve` / `QuoteReserve` | Pool reserves when the protocol event or accounts expose them. |
| `UserBaseBalance` / `UserQuoteBalance` | User token balances after the transaction when available from token balance metadata. |
| `AfterSOLBalance` | User or signer SOL balance after the transaction. |
| `EntryContract` | Known router / entry contract account when detected. |
| `Mayhem` / `Cashback` | Protocol or platform-specific labels detected from the transaction path. |
Swap amount and slippage fields normalize instruction intent:
| Field | Meaning |
| --- | --- |
| `SwapMode` | `exact_in`, `exact_out`, or empty when unknown. |
| `FixedAmount` / `FixedAmountSide` / `FixedMint` | The user-specified fixed side of the swap. For `exact_in`, this is the input amount. For `exact_out`, this is the target output amount. |
| `LimitAmountType` | `min_out` for `exact_in`, `max_in` for `exact_out`, or empty when unknown. |
| `LimitAmount` / `LimitAmountSide` / `LimitMint` | User-specified limit on the opposite side. |
| `ActualLimitAmount` / `ActualLimitAmountSide` | Actual executed amount on the limited side. |
| `SlippageBps` | Remaining headroom to the user's limit in basis points. See `SLIPPAGE_MAPPING.md` for protocol-specific derivation rules. |
Liquidity, migration, and DLMM-specific fields are populated only for protocols that expose them:
| Field | Meaning |
| --- | --- |
| `Creator` | Creator account when exposed by pool or launch events. |
| `MigrateToPool` / `MigrateTopProgram` | Destination pool and program for migration events. |
| `LpMint` | LP token mint for liquidity operations when available. |
| `ActiveBinId` / `StartBinId` / `EndBinId` | Meteora DLMM bin identifiers. |
| `RemoveBp` | DLMM remove-liquidity basis points. |
| `PositionAccount` | DLMM position account. |
| `FeeAmount` / `LpFeeAmount` | Parsed fee amounts when the protocol exposes fee breakdowns. |
| `FeeSide` / `FeeMint` / `FeeTokenProgram` / `FeeMintDecimals` | Fee side and mint metadata. |
| `ConsumeUnit` | Per-swap compute unit value when available. |
Amounts are normalized into decimals in parser output, but the exact scale depends on the source event and parser path. For persisted binary output, `tx_binary.go` defines the conversion rules and schema version.
## Convert Transactions
RPC transactions can be converted with `FromRpcTransactionWithMeta`:
```go
rawTx, err := pump_parser.FromRpcTransactionWithMeta(
txWithMeta,
blockTime,
slot,
indexWithinBlock,
)
```
Yellowstone transactions can be converted with `ConvertYellowstoneGrpcTransactionToSolanaTransaction`:
```go
rawTx, err := pump_parser.ConvertYellowstoneGrpcTransactionToSolanaTransaction(
update,
createdUnix,
)
```
Both conversion paths accept optional `RawTxConvertOptions`:
```go
rawTx, err := pump_parser.FromRpcTransactionWithMeta(
txWithMeta,
blockTime,
slot,
indexWithinBlock,
pump_parser.RawTxConvertOptions{
ParseLogEvents: true,
},
)
```
`ParseLogEvents` attaches decoded program log events to matching instructions. `IgnoreLogMessages` skips log-message retention.
## Binary Formats
Parsed transaction binary:
- `EncodeTxBinary` / `DecodeTxBinary` for one parsed `Tx`.
- `EncodeTxsBinary` / `DecodeTxsBinary` for a batch of parsed `Tx`.
- `DecodeTxsBinaryReader` for streaming `PTXS` reads.
- `MergeTxsBinaryBytes` and `MergeTxsBinarySourcesToWriter` for merging `PTXS` batches.
Raw transaction binary:
- `EncodeRawTxBinary` / `DecodeRawTxBinary` for one `RawTx`.
- `EncodeRawTxsBinary` / `DecodeRawTxsBinary` for a batch of `RawTx`.
- `EncodeRawTxBlocksBinary` / `DecodeRawTxBlocksBinary` for grouped block data.
- `DecodeRawTxsBinaryReader` for streaming `PRTS` reads.
Format magic values:
- `PTXB`: one parsed `Tx`.
- `PTXS`: parsed `Tx` batch.
- `PRTX`: one raw `RawTx`.
- `PRTS`: raw `RawTx` batch.
- `PRBS`: raw block-grouped `RawTx` batch.
When adding or renaming transaction-facing enum values, update `tx_binary.go` enum tables by appending new values only. Reordering existing values changes persisted numeric IDs.
## Commands
Parse a live transaction by signature:
```bash
TX_HASH=<signature> go run ./cmd/rpc_parse
```
Collect Yellowstone transactions into a `.prbs` raw-block binary file:
```bash
YELLOWSTONE_X_TOKEN=<token> go run ./cmd/collect_yellowstone_rawtx_binary \
-endpoint ams.rpc.orbitflare.com:10000 \
-duration 5m \
-output testdata/rawtx-binary/sample.prbs
```
Measure parsed `Tx` binary size from a `getBlock` JSON payload:
```bash
go run ./cmd/measure_tx_binary_block \
-file /path/to/block.json \
-slot 413539056 \
-swaps-only
```
Analyze raw transaction binary size distribution:
```bash
go run ./cmd/analyze_rawtx_binary_size \
-file testdata/rawtx-binary/sample.prbs
```
## Development
Run the full test suite:
```bash
go test ./...
```
Useful focused checks:
```bash
go test -run 'TestTxBinary|TestRawTxBinary' .
go test -run 'TestMetaoraPoolSwapEventFromLogsUsesMatchingInvocation|TestMetaoraPoolSwapInstructionOccurrenceIncludesInnerInstructions|TestAttachLogEventsToInstructions' .
TX_HASH=<signature> go run ./cmd/rpc_parse
```
For parser regressions, reproduce with the exact transaction signature first, then add a targeted unit test or fixture around the corrected parser path.

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,21 @@
# Codex Notes
## Tx Binary enum synchronization
When adding or renaming transaction-facing enum values, keep the binary format definitions in sync.
This includes, but is not limited to:
- tx events
- programs
- platforms
- MEV agents
- swap modes, amount sides, limit types, and fee sides
Checklist:
1. Add the public constant in the normal source location, such as `enum.go`.
2. Add any address mapping in `consts.go` when the enum is account-derived, such as platform or MEV agent detection.
3. Append the new value to the matching versioned enum list in `tx_binary.go` under `txBinaryEnumTables`.
4. Do not reorder or insert into existing `tx_binary.go` enum lists unless the binary version is intentionally changed; append to preserve existing numeric IDs.
5. Add or update tx-binary round-trip coverage so encoding and decoding the new enum value is exercised.

View File

@@ -186,6 +186,12 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas, solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas, solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasF3QPWPAKKmgA3GjfWax1kmTT1aoqSGxPzVLNUQ"): MevAgentSoyas, solana.MustPublicKeyFromBase58("soyasF3QPWPAKKmgA3GjfWax1kmTT1aoqSGxPzVLNUQ"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasi59njacMUPvo3TM5paHjeK8pYSdovXgFi32gRt"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasQYhJxv8uZgWDxhg72td6piAf7XTkoyWHtSATEz"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyastP66xyYC8XADXZjdMM5BAVGD2YRvz8dwtLsqb8"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasvdgUJWYcUCzDxpmjUnNjH7KamXLXTzLwFvdVPE"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasvxAunisNxaoRxkKGjNir7KmbwYnr37JmefkX9G"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyas5doVFUwH8s5zK8gEvCL5KR5ogDmf52LsrJEZ9h"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium, solana.MustPublicKeyFromBase58("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium,
solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium, solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium,
solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium, solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium,
@@ -200,6 +206,8 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane, solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"): MevAgentAstralane, solana.MustPublicKeyFromBase58("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane, solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane, solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane, solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("b1ooMDLjzz4QqecNsJ8bBXzJTzfAPDCP3CxijTS2K93"): MevAgentAstralane, solana.MustPublicKeyFromBase58("b1ooMDLjzz4QqecNsJ8bBXzJTzfAPDCP3CxijTS2K93"): MevAgentAstralane,
@@ -381,6 +389,16 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("t7qUQU35sLpPydh42BcPmtEfTWW8gBe4Ry3gjwVnokJ"): MevAgentRaiden, solana.MustPublicKeyFromBase58("t7qUQU35sLpPydh42BcPmtEfTWW8gBe4Ry3gjwVnokJ"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t8pPgarSK3TnuLbbHmoE1RCQdLxfxuPqNTyFjBKahok"): MevAgentRaiden, solana.MustPublicKeyFromBase58("t8pPgarSK3TnuLbbHmoE1RCQdLxfxuPqNTyFjBKahok"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t96GGdw3MiaGR993XN8PSsRpKGXx56t5Wf6zcF1hBpY"): MevAgentRaiden, solana.MustPublicKeyFromBase58("t96GGdw3MiaGR993XN8PSsRpKGXx56t5Wf6zcF1hBpY"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("7HkiWXe5deJvzn4D6kgMUFCADwX9Z4DMrdjNSSxN6bPp"): MevAgentZan,
solana.MustPublicKeyFromBase58("zanrUknLZXzT9JPj968A7RfgCjp77Lx1W1xKRAtfshb"): MevAgentZan,
solana.MustPublicKeyFromBase58("zanHbk2UsiT3jKsKjD7UuEqS5Vgpmcd4pG9HycAAV8g"): MevAgentZan,
solana.MustPublicKeyFromBase58("zanNazKCXNRoKnPS9BBbFTELTpNwUDJxeKEb1JtZJer"): MevAgentZan,
solana.MustPublicKeyFromBase58("zan3gbFhXCjGLHhRe2vaXRDta5fCrYiYr3Dq4RLvpfU"): MevAgentZan,
solana.MustPublicKeyFromBase58("zan6WoE3DX5aK7FMQT1vSGsGrgZG1ngns3oCsFMnBHU"): MevAgentZan,
solana.MustPublicKeyFromBase58("zan8Nb9fB4zMDsuTRP9R65QZbc9v2Cjn5a4Hjwnj8D3"): MevAgentZan,
solana.MustPublicKeyFromBase58("zanJgoR7ALJAJ6ohoKs6aS9T71D9ZkNN9gYM5xUsi3u"): MevAgentZan,
solana.MustPublicKeyFromBase58("zanAtYifQP7Bo6kStB97mJvzqSDW1toKNibWibwcKDd"): MevAgentZan,
solana.MustPublicKeyFromBase58("GWT5UjDheZzoqinLavJkYvSRH5sakW8vDRdAgrUS5ZcS"): MevAgentTunneling,
} }
var entryContractAddresses = map[solana.PublicKey]string{ var entryContractAddresses = map[solana.PublicKey]string{

View File

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

View File

@@ -25,7 +25,7 @@ func main() {
// laserstream-mainnet-slc.helius-rpc.com:80 // laserstream-mainnet-slc.helius-rpc.com:80
ch := make(chan example.SubscriptionMessage, 1) ch := make(chan example.SubscriptionMessage, 1)
go example.RunLoopWithReConnect(context.Background(), "", "", parser.SolProgramPump, ch) go example.RunLoopWithReConnect(context.Background(), "ams.rpc.orbitflare.com:10000", "ORBIT-EPUZGQ-177605-508881", parser.SolProgramPump, ch)
// var tokenTxs = make(map[string]*types.Tx) // var tokenTxs = make(map[string]*types.Tx)
// currentBlock := uint64(0) // currentBlock := uint64(0)
for msg := range ch { for msg := range ch {

View File

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

View File

@@ -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,6 +900,9 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
EntryContract: entryContract, EntryContract: entryContract,
}, },
} }
fixedSide := fixedSwapAmountSide(event, SwapModeExactIn)
limitSide := oppositeSwapAmountSide(fixedSide)
if fixedSide == SwapAmountSideUnknown || limitSide == SwapAmountSideUnknown {
swaps[0].SetSwapAmountInfo( swaps[0].SetSwapAmountInfo(
SwapModeExactIn, SwapModeExactIn,
decimal.NewFromUint64(args.InAmount), decimal.NewFromUint64(args.InAmount),
@@ -893,3 +910,185 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
) )
return swaps, offset, nil return swaps, offset, nil
} }
actualLimitAmount := swapAmountForSide(baseAmount, quoteAmount, limitSide)
if swapEvent, ok := metaoraPoolSwapEventFromInstruction(instruction); ok {
actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount)
} else if swapEvent, ok := metaoraPoolSwapEventForOffset(tx, swapOffset); ok {
actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount)
}
swaps[0].SetSwapAmountInfoDetailed(
SwapModeExactIn,
decimal.NewFromUint64(args.InAmount),
fixedSide,
swapMintForSide(baseMint, quoteMint, fixedSide),
SwapLimitTypeMinOut,
decimal.NewFromUint64(args.MinimumOutAmount),
limitSide,
swapMintForSide(baseMint, quoteMint, limitSide),
actualLimitAmount,
)
return swaps, offset, nil
}
func metaoraPoolSwapEventFromInstruction(instruction Instruction) (metaoraPoolSwapEvent, bool) {
for _, event := range instruction.LogEvents {
if swapEvent, ok := metaoraPoolDecodeSwapEventData(event); ok {
return swapEvent, true
}
}
return metaoraPoolSwapEvent{}, false
}
func metaoraPoolSwapEventForOffset(tx *Tx, offset [2]uint) (metaoraPoolSwapEvent, bool) {
if tx == nil || tx.rawTx == nil {
return metaoraPoolSwapEvent{}, false
}
occurrence := metaoraPoolSwapInstructionOccurrence(tx.rawTx, offset)
if occurrence == 0 {
return metaoraPoolSwapEvent{}, false
}
return metaoraPoolSwapEventFromLogs(tx.rawTx.Meta.LogMessages, occurrence)
}
func metaoraPoolSwapInstructionOccurrence(rawTx *RawTx, offset [2]uint) int {
if rawTx == nil {
return 0
}
accountList := rawTx.getAccountList()
innerByOuter := make(map[int]InnerInstructions, len(rawTx.Meta.InnerInstructions))
for _, inner := range rawTx.Meta.InnerInstructions {
innerByOuter[inner.Index] = inner
}
occurrence := 0
for i, instruction := range rawTx.Transaction.Message.Instructions {
if uint(i) == offset[0] && offset[1] == 0 {
if metaoraPoolIsSwapInstruction(accountList, instruction) {
return occurrence + 1
}
return 0
}
if metaoraPoolIsSwapInstruction(accountList, instruction) {
occurrence++
}
inner := innerByOuter[i]
for j, instruction := range inner.Instructions {
innerOffset := uint(j + 1)
if uint(i) == offset[0] && offset[1] == innerOffset {
if metaoraPoolIsSwapInstruction(accountList, instruction) {
return occurrence + 1
}
return 0
}
if metaoraPoolIsSwapInstruction(accountList, instruction) {
occurrence++
}
}
}
return 0
}
func metaoraPoolIsSwapInstruction(accountList []solana.PublicKey, instruction Instruction) bool {
if instruction.ProgramIDIndex < 0 || instruction.ProgramIDIndex >= len(accountList) {
return false
}
if !accountList[instruction.ProgramIDIndex].Equals(metaoraPoolProgramID) {
return false
}
return len(instruction.Data) >= 8 && bytes.Equal(instruction.Data[:8], metaoraPoolSwapDiscriminator[:])
}
func metaoraPoolSwapEventFromLogs(logMessages []string, occurrence int) (metaoraPoolSwapEvent, bool) {
if occurrence <= 0 {
return metaoraPoolSwapEvent{}, false
}
type frame struct {
program string
sawSwap bool
}
targetProgram := metaoraPoolProgramID.String()
var stack []frame
seen := 0
for _, logMessage := range logMessages {
if program, ok := metaoraPoolLogInvokeProgram(logMessage); ok {
stack = append(stack, frame{program: program})
continue
}
if len(stack) > 0 && stack[len(stack)-1].program == targetProgram {
if logMessage == "Program log: Instruction: Swap" {
stack[len(stack)-1].sawSwap = true
continue
}
if stack[len(stack)-1].sawSwap && strings.HasPrefix(logMessage, "Program data: ") {
event, ok := metaoraPoolDecodeSwapEventLog(strings.TrimSpace(strings.TrimPrefix(logMessage, "Program data: ")))
if ok {
seen++
if seen == occurrence {
return event, true
}
}
}
}
if program, ok := metaoraPoolLogFinishedProgram(logMessage); ok {
for i := len(stack) - 1; i >= 0; i-- {
if stack[i].program == program {
stack = stack[:i]
break
}
}
}
}
return metaoraPoolSwapEvent{}, false
}
func metaoraPoolLogInvokeProgram(logMessage string) (string, bool) {
if !strings.HasPrefix(logMessage, "Program ") || !strings.Contains(logMessage, " invoke [") {
return "", false
}
rest := strings.TrimPrefix(logMessage, "Program ")
program, _, ok := strings.Cut(rest, " ")
return program, ok
}
func metaoraPoolLogFinishedProgram(logMessage string) (string, bool) {
if !strings.HasPrefix(logMessage, "Program ") {
return "", false
}
if !strings.HasSuffix(logMessage, " success") && !strings.Contains(logMessage, " failed:") {
return "", false
}
rest := strings.TrimPrefix(logMessage, "Program ")
program, _, ok := strings.Cut(rest, " ")
return program, ok
}
func metaoraPoolDecodeSwapEventLog(encoded string) (metaoraPoolSwapEvent, bool) {
data, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return metaoraPoolSwapEvent{}, false
}
return metaoraPoolDecodeSwapEventData(data)
}
func metaoraPoolDecodeSwapEventData(data []byte) (metaoraPoolSwapEvent, bool) {
if len(data) < 48 {
return metaoraPoolSwapEvent{}, false
}
if !bytes.Equal(data[:8], metaoraPoolSwapEventDiscriminator[:]) {
return metaoraPoolSwapEvent{}, false
}
body := data[8:]
return metaoraPoolSwapEvent{
InAmount: binary.LittleEndian.Uint64(body[0:8]),
OutAmount: binary.LittleEndian.Uint64(body[8:16]),
TradeFee: binary.LittleEndian.Uint64(body[16:24]),
ProtocolFee: binary.LittleEndian.Uint64(body[24:32]),
HostFee: binary.LittleEndian.Uint64(body[32:40]),
}, true
}

152
metaorapool_test.go Normal file
View File

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

View File

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

659
pump.go
View File

@@ -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]
quoteReserves := decimal.Zero
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
quoteReserves = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuotePoolToken])
} else {
solReserves, _ := GetSolAfterTx(result, bcIdx) solReserves, _ := GetSolAfterTx(result, bcIdx)
quoteReserves = decimal.NewFromUint64(solReserves)
}
tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex) tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex)
swaps := []Swap{ swaps := []Swap{
{ {
Program: SolProgramPump, Program: SolProgramPump,
Event: event, Event: event,
Pool: result.accountList[instruction.Accounts[3]], Pool: pumpAccount(result, instruction, layout.Pool),
BaseMint: mint, BaseMint: mint,
QuoteMint: solana.PublicKey{}, QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram, BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: solana.PublicKey{}, QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: 6, BaseMintDecimals: 6,
QuoteMintDecimals: 9, QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
User: user, User: user,
BaseAmount: decimal.NewFromUint64(tokenAmount), BaseAmount: decimal.NewFromUint64(tokenAmount),
QuoteAmount: decimal.NewFromUint64(solAmount), QuoteAmount: decimal.NewFromUint64(quoteAmount),
BaseReserve: tokenReserves, BaseReserve: tokenReserves,
QuoteReserve: decimal.NewFromUint64(solReserves), QuoteReserve: quoteReserves,
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]), Mayhem: isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
UserBaseBalance: userBase, UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote), UserQuoteBalance: userQuote,
EntryContract: entryContract, EntryContract: entryContract,
}, },
} }
@@ -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,
}) })

View File

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

View File

@@ -182,6 +182,9 @@ func pumpAmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
} }
func ammCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { func ammCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 15 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
result := tx.rawTx result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error var err error
@@ -275,6 +278,9 @@ func pumpAmmSwapAmountInfoFromArgs(args PumpSwapArgs) (swapMode SwapMode, fixedA
} }
func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 13 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
if tx.Err == nil || tx.Err.UnKnown != "" { if tx.Err == nil || tx.Err.UnKnown != "" {
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1]) return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
} }
@@ -401,6 +407,9 @@ func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions Inn
} }
func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 13 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
if tx.Err == nil || tx.Err.UnKnown != "" { if tx.Err == nil || tx.Err.UnKnown != "" {
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1]) return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
} }
@@ -525,6 +534,9 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error var err error
var prefixLen = offset[1] var prefixLen = offset[1]
if len(instruction.Accounts) < 13 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
inners, err := getInnerInstructions(innerInstructions, prefixLen) inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen) return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
@@ -662,6 +674,9 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
result := tx.rawTx result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error var err error
if len(instruction.Accounts) < 13 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
var prefixLen = offset[1] var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen) inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil { if err != nil {
@@ -789,6 +804,9 @@ func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstru
result := tx.rawTx result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error var err error
if len(instruction.Accounts) < 11 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
var prefixLen = offset[1] var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen) inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil { if err != nil {
@@ -887,6 +905,9 @@ func withdrawParse(tx *Tx, instruction Instruction, innerInstructions InnerInstr
result := tx.rawTx result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error var err error
if len(instruction.Accounts) < 11 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
var prefixLen = offset[1] var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen) inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil { if err != nil {

185
rawtx.go
View File

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

View File

@@ -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)
} }
if !rawTxTokenBalanceBinarySameIdentity(out[existingIndex], encoded) { postSeenByAccountIndex[encoded.AccountIndex] = struct{}{}
return nil, fmt.Errorf("post[%d].account_index %d identity mismatch", i, encoded.AccountIndex) if existingIndex, exists := preByAccountIndex[encoded.AccountIndex]; exists && rawTxTokenBalanceBinarySameIdentity(out[existingIndex], encoded) {
}
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,18 +1574,62 @@ 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)
} }
} }
}
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 { for balanceIndex, balance := range tx.Meta.PreTokenBalances {
if err := rawTxBinaryAddTokenBalanceAddresses(&builder, balance); err != nil { if err := rawTxBinaryAppendTokenBalanceAccounts(&accountList, seen, balance); err != nil {
return nil, fmt.Errorf("tx[%d].pre_token_balances[%d]: %w", txIndex, balanceIndex, err) return nil, fmt.Errorf("pre_token_balances[%d]: %w", balanceIndex, err)
} }
} }
for balanceIndex, balance := range tx.Meta.PostTokenBalances { for balanceIndex, balance := range tx.Meta.PostTokenBalances {
if err := rawTxBinaryAddTokenBalanceAddresses(&builder, balance); err != nil { if err := rawTxBinaryAppendTokenBalanceAccounts(&accountList, seen, balance); err != nil {
return nil, fmt.Errorf("tx[%d].post_token_balances[%d]: %w", txIndex, balanceIndex, err) return nil, fmt.Errorf("post_token_balances[%d]: %w", balanceIndex, err)
} }
} }
return accountList, nil
} }
return builder.addresses, 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) {
@@ -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:

View File

@@ -108,38 +108,14 @@ func raydiumClmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
switch discriminator { switch discriminator {
case raydiumClmmIncreaseLiquidityDiscriminator: case raydiumClmmIncreaseLiquidityDiscriminator:
accountMin = 12 accountMin = 12
market = tx.rawTx.accountList[instruction.Accounts[2]]
vault0 = instruction.Accounts[9]
vault1 = instruction.Accounts[10]
case raydiumClmmIncreaseLiquidityV2Discriminator: case raydiumClmmIncreaseLiquidityV2Discriminator:
accountMin = 15 accountMin = 15
market = tx.rawTx.accountList[instruction.Accounts[2]]
vault0 = instruction.Accounts[9]
vault1 = instruction.Accounts[10]
//token0 = tx.rawTx.accountList[instruction.Accounts[13]]
//token1 = tx.rawTx.accountList[instruction.Accounts[14]]
case raydiumClmmOpenPositionDiscriminator: case raydiumClmmOpenPositionDiscriminator:
accountMin = 19 accountMin = 19
market = tx.rawTx.accountList[instruction.Accounts[5]]
vault0 = instruction.Accounts[12]
vault1 = instruction.Accounts[13]
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
case raydiumClmmOpenPositionV2Discriminator: case raydiumClmmOpenPositionV2Discriminator:
accountMin = 22 accountMin = 22
market = tx.rawTx.accountList[instruction.Accounts[5]]
vault0 = instruction.Accounts[12]
vault1 = instruction.Accounts[13]
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
//token0 = tx.rawTx.accountList[instruction.Accounts[20]]
//token1 = tx.rawTx.accountList[instruction.Accounts[21]]
case raydiumClmmOpenPositionWithToken22NftDiscriminator: case raydiumClmmOpenPositionWithToken22NftDiscriminator:
accountMin = 20 accountMin = 20
market = tx.rawTx.accountList[instruction.Accounts[4]]
vault0 = instruction.Accounts[11]
vault1 = instruction.Accounts[12]
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
//token0 = tx.rawTx.accountList[instruction.Accounts[18]]
//token1 = tx.rawTx.accountList[instruction.Accounts[19]]
default: default:
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator") return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
} }
@@ -148,6 +124,23 @@ func raydiumClmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for raydiumClmm add liquidity instruction, offset, %d, %d", offset[0], offset[1]) return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for raydiumClmm add liquidity instruction, offset, %d, %d", offset[0], offset[1])
} }
switch discriminator {
case raydiumClmmIncreaseLiquidityDiscriminator, raydiumClmmIncreaseLiquidityV2Discriminator:
market = tx.rawTx.accountList[instruction.Accounts[2]]
vault0 = instruction.Accounts[9]
vault1 = instruction.Accounts[10]
case raydiumClmmOpenPositionDiscriminator, raydiumClmmOpenPositionV2Discriminator:
market = tx.rawTx.accountList[instruction.Accounts[5]]
vault0 = instruction.Accounts[12]
vault1 = instruction.Accounts[13]
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
case raydiumClmmOpenPositionWithToken22NftDiscriminator:
market = tx.rawTx.accountList[instruction.Accounts[4]]
vault0 = instruction.Accounts[11]
vault1 = instruction.Accounts[12]
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
}
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0) baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
@@ -197,6 +190,8 @@ func raydiumClmmDecreaseLiquidityParser(tx *Tx, instruction Instruction, innerIn
accountMin = 14 accountMin = 14
} else if discriminator == raydiumClmmDecreaseLiquidityV2Discriminator { } else if discriminator == raydiumClmmDecreaseLiquidityV2Discriminator {
accountMin = 16 accountMin = 16
} else {
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
} }
if len(instruction.Accounts) < accountMin { if len(instruction.Accounts) < accountMin {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for decrease liquidity instruction") return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for decrease liquidity instruction")
@@ -299,23 +294,21 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
} }
if discriminator == raydiumClmmSwapDiscriminator { if discriminator == raydiumClmmSwapDiscriminator {
accountMin = 9 accountMin = 9
pool = tx.rawTx.accountList[instruction.Accounts[2]]
userTokenInAccount = instruction.Accounts[3]
userTokenOutAccount = instruction.Accounts[4]
tokenInVault = instruction.Accounts[5]
tokenOutVault = instruction.Accounts[6]
} else if discriminator == raydiumClmmSwapV2Discriminator { } else if discriminator == raydiumClmmSwapV2Discriminator {
accountMin = 13 accountMin = 13
pool = tx.rawTx.accountList[instruction.Accounts[2]] } else {
userTokenInAccount = instruction.Accounts[3] return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
userTokenOutAccount = instruction.Accounts[4]
tokenInVault = instruction.Accounts[5]
tokenOutVault = instruction.Accounts[6]
} }
if len(instruction.Accounts) < accountMin { if len(instruction.Accounts) < accountMin {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction") return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction")
} }
pool = tx.rawTx.accountList[instruction.Accounts[2]]
userTokenInAccount = instruction.Accounts[3]
userTokenOutAccount = instruction.Accounts[4]
tokenInVault = instruction.Accounts[5]
tokenOutVault = instruction.Accounts[6]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, tokenInVault) baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, tokenInVault)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get tokenIn vault balance after tx: %w", err) return nil, increaseOffset(offset), fmt.Errorf("failed to get tokenIn vault balance after tx: %w", err)

View File

@@ -373,6 +373,9 @@ type raydiumLaunchLabSwapArgs struct {
} }
func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 13 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]] platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
var programName string var programName string
if platformConfig.Equals(bonkPlatformConfig) { if platformConfig.Equals(bonkPlatformConfig) {

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1191,7 +1191,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 +1204,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)
} }
@@ -1592,7 +1592,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 +1619,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)
} }
@@ -2143,6 +2143,8 @@ var txBinaryEnumTables = map[uint16]*txBinaryEnumTable{
MevAgentSpeedlanding, MevAgentSpeedlanding,
MevAgentAllenhark, MevAgentAllenhark,
MevAgentRaiden, MevAgentRaiden,
MevAgentZan,
MevAgentTunneling,
}, },
), ),
} }
@@ -2198,9 +2200,30 @@ func (set txBinaryEnumSet) id(value string) (uint16, error) {
return id, nil return id, nil
} }
func (set txBinaryEnumSet) idOrFallback(value string, fallback string) (uint16, error) {
if id, ok := set.ids[value]; ok {
return id, nil
}
id, ok := set.ids[fallback]
if !ok {
return 0, fmt.Errorf("unsupported %s fallback enum value %q for versioned tx binary", set.name, fallback)
}
return id, nil
}
func (set txBinaryEnumSet) value(id uint16) (string, error) { func (set txBinaryEnumSet) value(id uint16) (string, error) {
if int(id) >= len(set.values) { if int(id) >= len(set.values) {
return "", fmt.Errorf("unknown %s enum id %d", set.name, id) return "", fmt.Errorf("unknown %s enum id %d", set.name, id)
} }
return set.values[id], nil return set.values[id], nil
} }
func (set txBinaryEnumSet) valueOrFallback(id uint16, fallback string) (string, error) {
if int(id) < len(set.values) {
return set.values[id], nil
}
if _, ok := set.ids[fallback]; !ok {
return "", fmt.Errorf("unsupported %s fallback enum value %q for versioned tx binary", set.name, fallback)
}
return fallback, nil
}

View File

@@ -41,6 +41,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{
{ {
@@ -146,6 +154,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 +239,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,