Compare commits

..

62 Commits

Author SHA1 Message Date
thloyi
43659ea4e4 fix pump amm quoteAmountIn 2026-04-27 14:36:03 +08:00
thloyi
6414e6a25f rawtx binary 2026-04-24 18:00:44 +08:00
thloyi
273e87b8ad fix ignore failed metaora swap 2026-04-22 11:16:26 +08:00
thloyi
bb858c643e fix orcawhirpool int64 overflow 2026-04-22 11:10:46 +08:00
thloyi
a620df5837 fix pump parser 2026-04-21 14:18:42 +08:00
thloyi
36da96eeaf no two hp swap slippage 2026-04-20 16:31:18 +08:00
thloyi
a765fafddd fix pump parser 2026-04-20 16:26:55 +08:00
thloyi
738e417167 fix EncodeTxBinary 2026-04-20 15:25:08 +08:00
thloyi
51f1511c8f fix EncodeTxBinary 2026-04-20 15:09:42 +08:00
thloyi
7dfe003e5b fix event enum 2026-04-20 14:16:20 +08:00
thloyi
fe94888b14 fix slippage 2026-04-20 12:31:30 +08:00
thloyi
1dd843c393 batch encode opts 2026-04-16 17:56:17 +08:00
thloyi
d2879efcc6 batch encode 2026-04-16 16:40:40 +08:00
thloyi
e761fd6f84 swap amount input 2026-04-16 14:24:14 +08:00
thloyi
ab0e87a48a fix raydium v4 swap v2 2026-04-16 11:39:15 +08:00
bijianing97
fb8d93f426 Update dlmm fee 2026-04-11 08:34:21 +08:00
bijianing97
0cc843b370 Update dlmm fee 2026-04-11 08:27:34 +08:00
bijianing97
d9a214b4b4 Add dlmm add liquidity one side function 2026-03-25 11:34:46 +08:00
thloyi
047b549d0f option ComputeUnitsConsumed 2026-03-23 20:20:21 +08:00
bijianing97
9327eab010 Fix dlmm parser 2026-03-23 15:30:43 +08:00
bijianing97
0ef57cf79a Add dlmm add_liquidity_by_weight 2026-03-20 17:06:37 +08:00
cachalots
03030d817d update 2026-03-20 11:41:44 +08:00
bijianing97
401dca225a Add dlmm open and close 2026-03-19 14:10:14 +08:00
bijianing97
db8c8727f4 Add dlmm start and end bin 2026-03-18 14:38:25 +08:00
bijianing97
09de6ba649 Add dlmm claim fee and update dlmm add and remove 2026-03-16 11:37:19 +08:00
bijianing97
7a82990770 Update meteoradlmm remove and add enum 2026-03-16 10:14:50 +08:00
bijianing97
e82bcb3c07 Merge remote-tracking branch 'origin' 2026-03-12 16:22:09 +08:00
bijianing97
a74f769064 Add raydiumv4 swapv2 2026-03-12 16:21:38 +08:00
thloyi
1e276e8bd2 [tx] swap inner idx 2026-03-12 13:49:34 +08:00
thloyi
eb2bde98ac update parsed tx: add swap at instr idx 2026-03-12 13:49:34 +08:00
66f0d247f5 chore: add astralane fee address 2026-03-12 12:08:06 +08:00
879b7fefad chore: add PlatformDexScreener 2026-03-12 11:52:58 +08:00
149dfae378 chore: add trojan fee address 2026-03-10 11:23:15 +08:00
8c4b43747c fix EntryContractAxiom 2026-03-02 20:04:38 +08:00
thloyi
e9ba16766f chain link parser 2026-03-02 18:01:32 +08:00
thloyi
cd1d681621 record ProgramFailedToComplete failed tx 2026-03-02 15:47:11 +08:00
thloyi
920c5ba25b fix errTx do not updte sol transfer 2026-02-27 17:41:58 +08:00
thloyi
3d447ef2e8 fix errTx mev and cu 2026-02-27 17:07:49 +08:00
thloyi
b0d4342fa2 pump and pump swap errTx parser 2026-02-27 16:37:15 +08:00
cachalots
972ddc7960 IsCashbackCoin 2026-02-27 02:10:12 +08:00
bcd442195c chore: support IsCashbackEnabled 2026-02-27 01:43:07 +08:00
0633707142 chore: add trojan fee addresses 2026-02-26 15:19:57 +08:00
8e49f01054 must to next inner at least 2026-02-21 09:29:28 +08:00
thloyi
62cc64a90a fix from rpc ComputeUnitsConsumed 2026-02-12 10:43:30 +08:00
thloyi
629ffe2ea7 fix pump complete parse error 2026-02-12 10:38:56 +08:00
cachalots
56dac04a2a fix culimit 2026-02-12 10:11:29 +08:00
cachalots
852ad4b382 cu limit 2026-02-11 17:49:43 +08:00
thloyi
3fdd4c4490 fix parse error 2026-02-11 14:58:12 +08:00
thloyi
40012b531c fix meteora pool entrycontract 2026-02-10 10:32:46 +08:00
bijianing97
0e30d6b35f Update address 2026-02-09 17:36:35 +08:00
thloyi
70d91fdd30 fix entryContract axiom 2026-02-09 16:09:31 +08:00
thloyi
9ece4aebb9 all parser 2026-02-09 14:46:19 +08:00
thloyi
5da088ce13 metaora dlmm init 2026-02-02 17:59:47 +08:00
thloyi
0eb1628119 is vote 2026-02-02 14:13:00 +08:00
cachalots
c25c856a47 axiom 2026-01-15 17:47:37 +08:00
cachalots
b4906a2c20 entry contract / tip agent 2026-01-15 17:47:37 +08:00
bijianing97
21692c2ecc Update 2026-01-15 17:45:17 +08:00
bijianing97
6b4cadb118 Update 2026-01-15 17:44:39 +08:00
bijianing97
b76d2efc88 Use dlmm as option 2026-01-08 12:00:59 +08:00
bijianing97
16b7461ac7 Add MetaOra DLMM StartBinId and EndBinId 2026-01-07 18:21:27 +08:00
bijianing97
6bc84ce126 Add MetaOra DLMM parser 2026-01-07 16:41:49 +08:00
thloyi
8128a325a9 add instr sol transfer and cufee 2026-01-05 11:55:44 +08:00
60 changed files with 19232 additions and 716 deletions

156
SLIPPAGE_MAPPING.md Normal file
View File

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

View File

@@ -27,10 +27,11 @@ func budgetParser(tx *Tx, instr Instruction, _ InnerInstructions, offset [2]uint
}
}
func computeUnitLimitParser(offset [2]uint, _ *Tx, decodedData []byte) ([2]uint, error) {
if len(decodedData) < 8 {
func computeUnitLimitParser(offset [2]uint, tx *Tx, decodedData []byte) ([2]uint, error) {
if len(decodedData) < 4 {
return increaseOffset(offset), nil
}
tx.CuLimit = binary.LittleEndian.Uint32(decodedData[:4])
return increaseOffset(offset), nil
}

120
chainlink.go Normal file
View File

@@ -0,0 +1,120 @@
package pump_parser
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"math/big"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
var (
chainlinkSOLUSDFeedAccount = solana.MustPublicKeyFromBase58("CH31Xns5z3M1cTAbKW34jcxPPciazARpijcHj9rxtemt")
chainlinkSubmitDiscriminator = calculateDiscriminator("global:submit")
)
func chainLinkParser(tx *Tx, instruction Instruction, inners InnerInstructions, offset [2]uint) ([2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(chainLinkProgram) {
return increaseOffset(offset), fmt.Errorf("system program instruction not found, block: %d, tx: %s, outerIndex: %d, innerIndex: %d", tx.rawTx.Slot, tx.rawTx.TxHash(), offset[0], offset[1])
}
if tx.rawTx.Meta.Err != nil {
return increaseOffset(offset), nil
}
decode := instruction.Data
discriminator := binary.LittleEndian.Uint32(decode[0:4])
switch discriminator {
case transferDiscriminator:
return chainLinkSubmitParser(instruction, inners, offset, tx, decode[4:])
default:
return increaseOffset(offset), nil
}
}
func chainLinkSubmitParser(instruction Instruction, inners InnerInstructions, offset [2]uint, tx *Tx, decodeData []byte) ([2]uint, error) {
if len(instruction.Accounts) < 6 {
return increaseOffset(offset), InstructionIgnoredError
}
inner, err := getInnerInstructions(inners, offset[1])
if err != nil {
return increaseOffset(offset), err
}
if len(inner) < 1 {
return increaseOffset(offset), InstructionIgnoredError
}
storeInstruction := inner[0]
if len(storeInstruction.Accounts) < 2 {
return increaseOffset(offset), InstructionIgnoredError
}
if storeInstruction.Accounts[0] >= len(tx.rawTx.accountList) || tx.rawTx.accountList[storeInstruction.Accounts[0]] != chainlinkSOLUSDFeedAccount {
return increaseOffset(offset), InstructionIgnoredError
}
if !bytes.Equal(storeInstruction.Data[0:8], chainlinkSubmitDiscriminator[:]) {
return increaseOffset(offset), InstructionIgnoredError
}
data, err := parseChainLinkSubmitData(storeInstruction.Data)
if err != nil {
return increaseOffset(offset), InstructionIgnoredError
}
tx.ChainLink.Timestamp = int64(data.Timestamp)
tx.ChainLink.Price = decimal.NewFromBigInt(data.Price(), -8)
return increaseOffset(offset), nil
}
type SubmitData struct {
Discriminator [8]byte
Timestamp uint64
Answer [16]byte
}
func parseChainLinkSubmitData(data []byte) (*SubmitData, error) {
if len(data) != 32 {
return nil, errors.New("invalid submit data length")
}
var submitData SubmitData
copy(submitData.Discriminator[:], data[:8])
submitData.Timestamp = binary.LittleEndian.Uint64(data[8:16])
copy(submitData.Answer[:], data[16:32])
return &submitData, nil
}
func (s *SubmitData) Price() *big.Int {
return int128LEBytesToBigInt(s.Answer)
}
func int128LEBytesToBigInt(bytes [16]byte) *big.Int {
// Create new big.Int
bigInt := new(big.Int)
// Reverse bytes for little-endian to big-endian conversion
reversed := make([]byte, 16)
for i := 0; i < 16; i++ {
reversed[15-i] = bytes[i]
}
// Check if negative (first byte in little-endian is highest byte)
isNegative := bytes[15]&0x80 != 0
if isNegative {
// If negative, flip all bits
for i := range reversed {
reversed[i] = ^reversed[i]
}
// Convert to big.Int
bigInt.SetBytes(reversed)
// Add 1 and negate
bigInt.Add(bigInt, big.NewInt(1))
bigInt.Neg(bigInt)
} else {
// If positive, convert directly
bigInt.SetBytes(reversed)
}
return bigInt
}

View File

@@ -1,368 +0,0 @@
package pump_parser
import (
"log"
"strings"
"github.com/gagliardetto/solana-go"
)
func checkBonkGmgnBuy(rawTx *RawTx) bool {
// 检查交易版本
var version, _ = rawTx.Version.(solana.MessageVersion)
if version != solana.MessageVersionLegacy {
return false
}
// 检查交易指令数量
if len(rawTx.Transaction.Message.Instructions) != 10 && len(rawTx.Transaction.Message.Instructions) != 9 {
return false
}
accountList := rawTx.getAccountList()
// 检查 cu limit
{
instruction := rawTx.Transaction.Message.Instructions[0]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.ComputeBudget {
return false
}
if len(instruction.Accounts) != 1 {
return false
}
accountId := accountList[instruction.Accounts[0]].String()
if !strings.HasPrefix(accountId, "jitodontfront1111111111151111111111111655") {
return false
}
}
// 检查 cu price
{
instruction := rawTx.Transaction.Message.Instructions[1]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.ComputeBudget {
return false
}
}
// 检查 ata.createIdempotent
{
instruction := rawTx.Transaction.Message.Instructions[2]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SPLAssociatedTokenAccountProgramID {
return false
}
if instruction.Data.String() != "2" {
return false
}
if len(instruction.Accounts) < 4 {
return false
}
// gmgn 会先创建 wsol 账户, 而不是 token 账户
accountId := accountList[instruction.Accounts[3]]
if accountId != solana.WrappedSol {
return false
}
}
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[3]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
return false
}
}
// 检查 token.syncNative
{
instruction := rawTx.Transaction.Message.Instructions[4]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.TokenProgramID {
return false
}
if instruction.Data.String() != "J" {
return false
}
}
offset := 5
if len(rawTx.Transaction.Message.Instructions) == 10 {
// 检查 ata.create
{
instruction := rawTx.Transaction.Message.Instructions[offset]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SPLAssociatedTokenAccountProgramID {
return false
}
if instruction.Data.String() != "1" {
return false
}
}
offset++
}
// 检查 bonk.buy
{
instruction := rawTx.Transaction.Message.Instructions[offset]
programId := accountList[instruction.ProgramIDIndex]
if programId != raydiumLaunchLabProgramID {
return false
}
}
offset++
// 检查 token.closeAccount
{
instruction := rawTx.Transaction.Message.Instructions[offset]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.TokenProgramID {
return false
}
if instruction.Data.String() != "A" {
return false
}
}
offset++
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[offset]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
return false
}
}
offset++
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[offset]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
return false
}
}
return true
}
var (
axiomTxLoopupTable = solana.MustPublicKeyFromBase58("7RKtfATWCe98ChuwecNq8XCzAzfoK3DtZTprFsPMGtio")
)
func checkBonkAxiomBuy(rawTx *RawTx) bool {
// 检查交易版本
var version, _ = rawTx.Version.(solana.MessageVersion)
if version == solana.MessageVersionLegacy || len(rawTx.Transaction.Message.AddressTableLookups) != 1 {
log.Printf("CheckBonkAxiomBuy: Invalid transaction version or address table lookups: %v %v %v", rawTx.Transaction.Signatures[0].String(), version, len(rawTx.Transaction.Message.AddressTableLookups))
return false
}
// 检查 addressLookupTable 是否是 Axiom 的
if rawTx.Transaction.Message.AddressTableLookups[0].AccountKey != axiomTxLoopupTable {
log.Printf("CheckBonkAxiomBuy: Invalid address lookup table: %v", rawTx.Transaction.Signatures[0].String())
return false
}
// 检查交易指令数量
if len(rawTx.Transaction.Message.Instructions) != 10 {
return false
}
accountList := rawTx.getAccountList()
// 检查 cu limit
{
instruction := rawTx.Transaction.Message.Instructions[0]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.ComputeBudget {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for ComputeBudget: %v", programId)
return false
}
if len(instruction.Accounts) != 1 {
log.Printf("CheckBonkAxiomBuy: Invalid number of accounts for ComputeBudget: %v", len(instruction.Accounts))
return false
}
accountId := accountList[instruction.Accounts[0]].String()
if !strings.HasPrefix(accountId, "jitodontfront") || !strings.HasSuffix(accountId, "TradeWithAxiomDotTrade") {
log.Printf("CheckBonkAxiomBuy: Invalid account ID for ComputeBudget: %v", accountId)
return false
}
}
// 检查 cu price
{
instruction := rawTx.Transaction.Message.Instructions[1]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.ComputeBudget {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for ComputeBudget: %v", programId)
return false
}
}
// 检查 ata.createIdempotent
{
instruction := rawTx.Transaction.Message.Instructions[2]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SPLAssociatedTokenAccountProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for SPLAssociatedTokenAccount: %v", programId)
return false
}
if instruction.Data.String() != "2" {
log.Printf("CheckBonkAxiomBuy: Invalid data for SPLAssociatedTokenAccount: %v", instruction.Data.String())
return false
}
if len(instruction.Accounts) < 4 {
log.Printf("CheckBonkAxiomBuy: Invalid number of accounts for SPLAssociatedTokenAccount: %v", len(instruction.Accounts))
return false
}
// axiom 会先创建 token 账户, 而不是 wsol 账户
accountId := accountList[instruction.Accounts[3]]
if accountId == solana.WrappedSol {
log.Printf("CheckBonkAxiomBuy: Invalid account ID for SPLAssociatedTokenAccount, expected token account but got wsol: %v", accountId)
return false
}
}
// 检查 ata.createIdempotent
{
instruction := rawTx.Transaction.Message.Instructions[3]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SPLAssociatedTokenAccountProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for SPLAssociatedTokenAccount2: %v", programId)
return false
}
if instruction.Data.String() != "2" {
log.Printf("CheckBonkAxiomBuy: Invalid data for SPLAssociatedTokenAccount2: %v", instruction.Data.String())
return false
}
if len(instruction.Accounts) < 4 {
log.Printf("CheckBonkAxiomBuy: Invalid number of accounts for SPLAssociatedTokenAccount2: %v", len(instruction.Accounts))
return false
}
// axiom 会先创建 token 账户, 而不是 wsol 账户
accountId := accountList[instruction.Accounts[3]]
if accountId != solana.WrappedSol {
log.Printf("CheckBonkAxiomBuy: Invalid account ID for SPLAssociatedTokenAccount2, expected wsol but got: %v", accountId)
return false
}
}
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[4]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for SystemProgram3: %v", programId)
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
log.Printf("CheckBonkAxiomBuy: Invalid data for SystemProgram transfer3: %v", instruction.Data)
return false
}
}
// 检查 token.syncNative
{
instruction := rawTx.Transaction.Message.Instructions[5]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.TokenProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for TokenProgram3: %v", programId)
return false
}
if instruction.Data.String() != "J" {
log.Printf("CheckBonkAxiomBuy: Invalid data for TokenProgram syncNative3: %v", instruction.Data.String())
return false
}
}
// 检查 bonk.buy
{
instruction := rawTx.Transaction.Message.Instructions[6]
programId := accountList[instruction.ProgramIDIndex]
if programId != raydiumLaunchLabProgramID {
return false
}
}
// 检查 token.closeAccount
{
instruction := rawTx.Transaction.Message.Instructions[7]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.TokenProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for TokenProgram closeAccount: %v", programId)
return false
}
if instruction.Data.String() != "A" {
log.Printf("CheckBonkAxiomBuy: Invalid data for TokenProgram closeAccount: %v", instruction.Data.String())
return false
}
}
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[8]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for SystemProgram transfer4: %v", programId)
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
log.Printf("CheckBonkAxiomBuy: Invalid data for SystemProgram transfer4: %v", instruction.Data)
return false
}
}
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[9]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for SystemProgram transfer5: %v", programId)
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
log.Printf("CheckBonkAxiomBuy: Invalid data for SystemProgram transfer5: %v", instruction.Data)
return false
}
}
return true
}

View File

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

View File

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

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

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

232
consts.go
View File

@@ -3,6 +3,7 @@ package pump_parser
import "github.com/gagliardetto/solana-go"
var platformFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("HeZVpHj9jLwTVtMMbzQRf6mLtFPkWNSg11o68qrbUBa3"): PlatformGMGN,
solana.MustPublicKeyFromBase58("BB5dnY55FXS1e1NXqZDwCzgdYJdMCj3B92PU6Q5Fb6DT"): PlatformGMGN,
solana.MustPublicKeyFromBase58("7sHXjs1j7sDJGVSMSPjD1b4v3FD6uRSvRWfhRdfv5BiA"): PlatformGMGN,
solana.MustPublicKeyFromBase58("ByRRgnZenY6W2sddo1VJzX9o4sMU4gPDUkcmgrpGBxRy"): PlatformGMGN,
@@ -12,6 +13,7 @@ var platformFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("dBhdrmwBkRa66XxBuAK4WZeZnsZ6bHeHCCLXa3a8bTJ"): PlatformGMGN,
solana.MustPublicKeyFromBase58("6TxjC5wJzuuZgTtnTMipwwULEbMPx5JPW3QwWkdTGnrn"): PlatformGMGN,
solana.MustPublicKeyFromBase58("AVUCZyuT35YSuj4RH7fwiyPu82Djn2Hfg7y2ND2XcnZH"): PlatformPhoton,
solana.MustPublicKeyFromBase58("9yj3zvLS3fDMqi1F8zhkaWfq8TZpZWHe6cz1Sgt7djXf"): PlatformPhoton,
solana.MustPublicKeyFromBase58("7LCZckF6XXGQ1hDY6HFXBKWAtiUgL9QY5vj1C4Bn1Qjj"): PlatformAxiom,
solana.MustPublicKeyFromBase58("4V65jvcDG9DSQioUVqVPiUcUY9v6sb6HKtMnsxSKEz5S"): PlatformAxiom,
solana.MustPublicKeyFromBase58("CeA3sPZfWWToFEBmw5n1Y93tnV66Vmp8LacLzsVprgxZ"): PlatformAxiom,
@@ -37,17 +39,35 @@ var platformFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("F4hJ3Ee3c5UuaorKAMfELBjYCjiiLH75haZTKqTywRP3"): PlatformBullX,
solana.MustPublicKeyFromBase58("47hEzz83VFR23rLTEeVm9A7eFzjJwjvdupPPmX3cePqF"): PlatformBanana,
solana.MustPublicKeyFromBase58("9yMwSPk9mrXSN7yDHUuZurAh1sjbJsfpUqjZ7SvVtdco"): PlatformTrojan,
solana.MustPublicKeyFromBase58("92Med3qeK7duC5iiYsHX38H2f2twJfRsSx93oNrza2VH"): PlatformTrojan,
solana.MustPublicKeyFromBase58("65gDv7pZQCZELsNpNYSFEBtNFpWZAbxmRFB6BGMqFkHH"): PlatformTrojan,
solana.MustPublicKeyFromBase58("8jgg7moFJkHyTtAv9M6RBSPMp2oXeXhuiUMKW8YbYCWn"): PlatformTrojan,
solana.MustPublicKeyFromBase58("BJgbYMZgqm79gNrmm31tV3L8GQorw91XFm4m7evyfPjr"): PlatformTrojan,
solana.MustPublicKeyFromBase58("BWgb8wR1FEGiu1jCDSKuHKf752W27b4iN6SvoNCiK4qp"): PlatformTrojan,
solana.MustPublicKeyFromBase58("GV4Bt6ehW5x5dqtaWAJBSnz8uum5Z2Rp9P2Tr5iVuQn5"): PlatformTrojan,
solana.MustPublicKeyFromBase58("2jwHNxavSoMZMEDbT1eV9PcPt5dDcayCqM6MkgaPpmWQ"): PlatformTrojan,
solana.MustPublicKeyFromBase58("66N1M2aaDSdJFZ1d7YoVN4EU45ju6XiscapLVHn5FLms"): PlatformTrojan,
solana.MustPublicKeyFromBase58("4mih95RmBqfHYvEfqq6uGGLp1Fr3gVS3VNSEa3JVRfQK"): PlatformRaybot,
solana.MustPublicKeyFromBase58("3udvfL24waJcLhskRAsStNMoNUvtyXdxrWQz4hgi953N"): PlatformMoonshot,
solana.MustPublicKeyFromBase58("3kxSQybWEeQZsMuNWMRJH4TxrhwoDwfv41TNMLRzFP5A"): PlatformMEVX,
solana.MustPublicKeyFromBase58("BS3CyJ9rRC4Tp8G7f86r6hGvuu3XdrVGNVpbNM9U5WRZ"): PlatformMEVX,
solana.MustPublicKeyFromBase58("97VmzkjX9w8gMFS2RnHTSjtMEDbifGXBq9pgosFdFnM"): PlatformTradeWiz,
solana.MustPublicKeyFromBase58("9rxM513XS4ruBbrGqCaRWuztmE34uxkFoMmp8SAAL7ar"): PlatformTradeWiz,
solana.MustPublicKeyFromBase58("F34kcgMgCF7mYWkwLN3WN7KrFprr2NbwxuLvXx4fbztj"): PlatformSolTradingBot,
solana.MustPublicKeyFromBase58("96aFQc9qyqpjMfqdUeurZVYRrrwPJG2uPV6pceu4B1yb"): PlatformSolTradingBot,
solana.MustPublicKeyFromBase58("5wkyL2FLEcyUUgc3UeGntHTAfWfzDrVuxMnaMm7792Gk"): PlatformMoonshotMoney,
solana.MustPublicKeyFromBase58("MaestroUL88UBnZr3wfoN7hqmNWFi3ZYCGqZoJJHE36"): PlatformMaestro,
solana.MustPublicKeyFromBase58("ZG98FUCjb8mJ824Gbs6RsgVmr1FhXb2oNiJHa2dwmPd"): PlatformBonkBot,
solana.MustPublicKeyFromBase58("J5XGHmzrRmnYWbmw45DbYkdZAU2bwERFZ11qCDXPvFB5"): PlatformPadre,
solana.MustPublicKeyFromBase58("5vPNE6VFyXmCmzmWotdxmRk57LEWiXxuAfZL3hKbi2LH"): PlatformAxiom,
solana.MustPublicKeyFromBase58("ECDrSz47nXihe5kyK4oWEePPsPi9qz6u5d6Fa2sDj3uM"): PlatformAxiom,
solana.MustPublicKeyFromBase58("EqGzowSp6cKAsMSRyyrFTaBxnZEVeNY81LC18YFy8Cx9"): PlatformAxiom,
solana.MustPublicKeyFromBase58("3Tu1Y9aNveLFN4WTAwnAwXL6tbUp5MMe3RxyybG4jTAS"): PlatformAxiom,
solana.MustPublicKeyFromBase58("3PvqoztjnRxaAiFmLuEfqZkU4GSbjUareks8S2xCZaTa"): PlatformAxiom,
solana.MustPublicKeyFromBase58("HkJYryz2BNeMQfuuSWDYktWt5fZLV26eK6nqu7EJycoG"): PlatformAxiom,
solana.MustPublicKeyFromBase58("BfFX9rUm8qTZiZjmeq9BktWVTNuG3YWMc5AvkrCKJike"): PlatformAxiom,
solana.MustPublicKeyFromBase58("2ApLdwLrGayEmxgpLX9BTR47Q2QprfMg5SpjrLeaK8s7"): PlatformAxiom,
solana.MustPublicKeyFromBase58("AVahywMVNRYzdgWrufSWrtdGXAeNEvfpJFxhVFK516mT"): PlatformDexScreener,
}
var mevAgentFeeAddresses = map[solana.PublicKey]string{
@@ -161,6 +181,206 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("BnGKHAC386n4Qmv9xtpBVbRaUTKixjBe3oagkPFKtoy6"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("Dd7K2Fp7AtoN8xCghKDRmyqr5U169t48Tw5fEd3wT9mq"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("AP6qExwrbRgBAVaehg4b5xHENX815sMabtBzUzVB4v8S"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("soyas4s6L8KWZ8rsSk1mF3d1mQScoTGGAgjk98bF8nP"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyascXFW5wEEYiwfEmHy2pNwomqzvggJosGVD6TJdY"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasF3QPWPAKKmgA3GjfWax1kmTT1aoqSGxPzVLNUQ"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium,
solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium,
solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium,
solana.MustPublicKeyFromBase58("ste11p7e2KLYou5bwtt35H7BM6uMdo4pvioGjJXKFcN"): MevAgentStellium,
solana.MustPublicKeyFromBase58("ste11TMV68LMi1BguM4RQujtbNCZvf1sjsASpqgAvSX"): MevAgentStellium,
solana.MustPublicKeyFromBase58("astra4uejePWneqNaJKuFFA8oonqCE1sqF6b45kDMZm"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astra9xWY93QyfG6yM8zwsKsRodscjQ2uU2HKNL5prk"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraRVUuTHjpwEVvNBeQEgwYx9w9CFyfxjYoobCZhL"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astrazznxsGUhWShqgNtAdfrzP2G83DzcWVJDxwV9bF"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("ASde6y8pBCU1aityWHRpqT7pEAcEonjCgFUMeh5egRes"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("ASUv6G8Cj6zt71UAqD1aVtDC3CRn6FFddqF17ZiegrES"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("b1ooMDLjzz4QqecNsJ8bBXzJTzfAPDCP3CxijTS2K93"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("b1oomst2baE3FqxFPHaA9JwhXgFG9HdTLmbNKDen1kK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("b1ooMngj7WbNPMZpWpnYRjxQ96RcDZ9ZFpRfjw1g7tg"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1oomgV9SAeiUc7GMEg9WhqkZJGccJuHAnh15DbezcN"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("b1oom3jaRNoyJzvSdSVbvSbth5uB4rRYtbjHXT5c1eW"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMauwuJPhHsXqt3uj7B92CAFG8kaD1Q2iGEmGYnx"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMdjcY7zemxDWiH8jVZPxEMdHnE5AraWPHdHQoPj"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMKzu6siJzQutP6a6oLiY3fpzgQnBZsAjxuAm9qo"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AstrA1ejL4UeXC2SBP4cpeEmtcFPZVLxx3XGKXyCW6to"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AsTra79FET4aCKWspPqeSFvjJNyp96SvAnrmyAxqg5b7"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AsTRADtvb6tTmrsqULQ9Wji9PigDMjhfEMza6zkynEvV"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AsTRAEoyMofR3vUPpf9k68Gsfb6ymTZttEtsAbv8Bk4d"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AStrAJv2RN2hKCHxwUMtqmSxgdcNZbihCwc1mCSnG83W"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("Astran35aiQUF57XZsmkWMtNCtXGLzs8upfiqXxth2bz"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AStRAnpi6kFrKypragExgeRoJ1QnKH7pbSjLAKQVWUum"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("ASTRaoF93eYt73TYvwtsv6fMWHWbGmMUZfVZPo3CRU9C"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("Gu2UGEfze3Gg5cHuEC4jGbyCufgpev75RkVvBdKKtf12"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("E8wD3SMD1trozPrvSN9F6SyuUXD7rrFDuR3WexGziKG5"): MevAgent0slot,
solana.MustPublicKeyFromBase58("18hCV7f9CPmZRAH3QCNZaGHhHeNSfisQKeKuFkQsPLY"): MevAgent0slot,
solana.MustPublicKeyFromBase58("2sYKRWBNVY6UomMBi4juoMrrL98bqizDMn98cJ3cBmye"): MevAgent0slot,
solana.MustPublicKeyFromBase58("CZubxabMM7CPFSDAfMUhxNuvXRDLjDf6yVVq1RoJ66rk"): MevAgent0slot,
solana.MustPublicKeyFromBase58("Dz8rMcdokTLfbnNz2ZdYocZixgaA1TMqbA31xtwPgcxb"): MevAgent0slot,
solana.MustPublicKeyFromBase58("ForLDu55GfA2U1aTUaitmjzjs92vvVn1MSqzY3D9HtAK"): MevAgent0slot,
solana.MustPublicKeyFromBase58("6MgjyQU7G988jgL6EGAgfHYoeesCnwYMyPeh1fpJ71FP"): MevAgent0slot,
solana.MustPublicKeyFromBase58("12pHu2j2DDShyCVFU7vtSLXga74et9y83VD38mw6XYhB"): MevAgent0slot,
solana.MustPublicKeyFromBase58("5QuV4TS5TJFWPu7Yd56VaPvf4nKUicPvTfC3mwnb7dNW"): MevAgent0slot,
solana.MustPublicKeyFromBase58("4gh9m7RV7G4WwRftA6qV7RhDfytdepb3XbxFRfTtneYJ"): MevAgent0slot,
solana.MustPublicKeyFromBase58("AumQWSLrWwDXRq1yDEYPiw8vT5NUBYzrbdWCprJ4ZUa8"): MevAgent0slot,
solana.MustPublicKeyFromBase58("3vGEsQA5jzvN8TBgytuYEdZxW6P2pK1c6pq56JiFuygS"): MevAgent0slot,
solana.MustPublicKeyFromBase58("AsEF2SWSEZ1xpGZ5fdzDKaoka1XEtFSjGo39YUXkpvAh"): MevAgent0slot,
solana.MustPublicKeyFromBase58("2WoQNgmc4SEXrR3rKQypmeWmsxGqHHE6rApnVrP6Pt77"): MevAgent0slot,
solana.MustPublicKeyFromBase58("9vTpfGYN2jtjZgXQ7gihyHmN3FseLP7uW1CWMdsgcny"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("CyL8mfycXYbWHVoTTsfvnAfF2MvfcqeQAmmsqNQLxF7g"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("Eg85QSYLwtZfBBPF4CsNmijJDXUAeCMjoh36L1cwboqg"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("9gBzvLKedrs9HxaLPhBdkPaeFTxEDNDGfqJmqvHjfiZp"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("7BxoFqM3swL46Lt9EWzL9z2LeXYfmJL7MVzpFrDpLPei"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("91Ht2gq1CMPcLySuq8NjHaA1rXysm8zzoiiyfT4uSE7u"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("2zCYpNSWcHX9AzFndF1mcT1bMkG1EXMzzjFcBjSnJq9f"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("4Kfqkx3c8TxLX74J1nzfzfHCGdoDCuZ8k84sGpnVh1a4"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("GeiVfSfUBVxjJA6F2SNSASoK8JaSCiSmsC2hBrPLfpiv"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("DggsS83MWeUHZdrV2jyMUh8GDfLrU5P9Es36h7Uf3wRp"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("2d5viHZBHKt5DgEpMckXEfndR1CoZ1tHvcbL9fU4xqT7"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("73VnqgMJq29j4HMzF6GRdBeVpZgz7ibouyKQvyAKbVZy"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("HvgA9hTyrTQCU5869fhZ7My9WkkHK2yBo4Wu6ojHmMio"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("4xJEQnuMpoUNxhNew4AechRBo1DnpVfLyUe68BXTTF73"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("7ZKL8BAPfKKa6FNmds48QKFnckrcj4mkppRnsBAR2xVH"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("BYpBPSRkVSvutxHngtxnqeoTBrENZ8iM56Ywnsmy829w"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("4LEkLhb2u5qCUXS1Hc3eL2zTxk2kjSzQeFK4ZgWsV3EM"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("96Zc2GT7ZmMvF7rXgcwHAyJ7KmK8RaS4Z3VZw2b7GjJx"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("9Kaz3Q9KJ3x8SXvui37FK5m1AwcwqkYLvS9Xg1Why9Q1"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("muV321VhQ4XgJkVtsZP13zbCqg9HokT222bWS3DBxp3"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("Hth7qf5dv683k3ZJffjJvJ8gSU21dfPWy3mBEyRRhCiN"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("pD7KfmGkxHqQFNLqYv3zshSzkGaAB99vjNDKz6e7nGC"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("AQChXZ1ZWvPH8EjdPxXXsC8VqCaBmPVruJbswhE3xNZ8"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("2L2DgQ5ZXRYnv8K97NFDJvsNrA1MsrCGr3CvokPtDy8D"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("4BjQeBGZmGNWeHfQC4scHK5d4RtDr79h1hZNPcrLDS8C"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("E99TTcqBPAY1F4ZppMRkDX3pTqaSnRC24tUErfd2opNL"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("8zi6AG7oSKoswSEMaxNmXrwBYmDwuQ4GLiY4Q1j9Rayu"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("8GgU7tKJSA97G97kD9AbxYgsC9Hcjfg7RpAofWuA6oHt"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("JDxQoXGFRwEojWzkirDNeHz88SDEPzdDakjsobJ4YHrj"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("GQgHdPuDNcss3BoKrMfS6bgGekjitmKQRJxnuhUBu921"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("8FoxPbnucCZ3wuzhMofKE5VdYKcHfWmYNrnC2whVBAhS"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("3L9UZWLAprLtB2xddEHsCmgXbPc2PidgSjtHGZd2MzB3"): MevAgentBlockRazor,
solana.MustPublicKeyFromBase58("FAST3dMFZvESiEipBvLSiXq3QCV51o3xuoHScqRU6cB6"): MevAgentFast,
solana.MustPublicKeyFromBase58("FASTCKnwwY6iL3CknRgg3Zqir7jeagDDhxSnBQQy5a1C"): MevAgentFast,
solana.MustPublicKeyFromBase58("FASTHPW6akdGh9PFSdhMTbCuGkCSX7LsUjjnaB2RTQ4v"): MevAgentFast,
solana.MustPublicKeyFromBase58("FASTKL1AamNKrwnvbKwo4PU8434BBdqVrTtugM6oDU71"): MevAgentFast,
solana.MustPublicKeyFromBase58("FASTPB76TxKPMZ7Q29m8v4zJn8gUjbWyvTEQaaxhwN7M"): MevAgentFast,
solana.MustPublicKeyFromBase58("FASTYKWXRfAoty7SQCM1mGVrmPUyyNcF4tc3DUkLDAu9"): MevAgentFast,
solana.MustPublicKeyFromBase58("FASTYmSidNfLwdwiQEhCTtzghxEtaipeNSDSwh9xDPs3"): MevAgentFast,
solana.MustPublicKeyFromBase58("FASTs6ctgbsuZegMzUs4DPUYhRSZUPCjgCVnttHbpQAp"): MevAgentFast,
solana.MustPublicKeyFromBase58("node1FdMPnJBN7QTuhzNw3VS823nxFuDTizrrbcEqzp"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1zrVjcY2XB3Au8qYj5MxjbNfGu3baHaqZMkPM7Z"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1E3hguapYA18HCpEEkRHQmLNiyv9pdfE9s2zo5X"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1CVxtFas2Pw5Vcf86Pq89Hqx4jveo1ntY7ARFMK"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1EoLojAvoUmyDytcvgdXs6GPtY3zpQXPCRVncEA"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1VwH169UqyJHr5MYCH3EBuwrdvn5KHXAkhEEfav"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1JkDqyiEg7CDNj3ATPiRmWaAG2gnrAEiMJ4Rzcc"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1GS8pZnP6MzGSXwhA2MXH6EBfCpFaAE64G2ubpB"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1AVfbcSi98LAgGyAHUGS4eYkYTbS5vUPZYQnViF"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1L7Xat2tSkRNNi6TSuUScMYfj64ovhr2aceJm9g"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1kMY97W3LPXaKKV43yRa2Q3BLg4WZiT27VifUDc"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1Zi3r7hmGYwF9cJAkfCHh9EKWbkSrYdvcvLukF4"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1G3fmoCuEJzcPNF4hLbSZ2ypcUuh9CB3k9E7Q8k"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node18nQgpjoKe1fM72GiV6tHXg5dMKbVPFGwRBD9MU"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1spgxXR8HCbm4LyZNoisFLmBXxy2qnZrv63WxMp"): MevAgentNode1,
solana.MustPublicKeyFromBase58("node1rmmFXeLh94mBGtDHbSwCrBJqDnc16xrURHRYD9"): MevAgentNode1,
solana.MustPublicKeyFromBase58("EnchantKMZ93cDKwsnyvnD5WCpZLFTLVRWozFjAUzTko"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("CJwbwPfVFZDPGKJKCtLkzDJPFrGyyroEPFjXigmJB6mr"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("3Thhhj3omvVFfbhEHdFe8djwDZT5oS6BQ4k5KrZkYt1r"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("6CQzBpGJn6XYcCkm77xNd944MpbjLHLsP6sCEWSZVUHS"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("DeMZbwKtu9kteFdxL1yh6aTWqDwYfH79DKzYrgfTwAc2"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("3Pn9ZFCsNTf9MvWbpemQccWuyHNMbBjxg1eW53ikHcpH"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("DjfRXWegRn9bWnBvZFxAnpu1jNikcoy8iiu6ZX9AxAd5"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("8mbjzuz8ka3zVGnry6xMEwm96tzk4yKnWgvwAT1LwEGx"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("5KpS5Q3nUtp1cUynUxzH2bA93SWzmx2y3GwU45AeEEP5"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgK8f5H4ocVdNrkUrspUFmAaEosGQtbc1JCMqLwvvRe"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bg7BgfutLpjFdxDNcbwQFGFkLGQT9Kww9wv6EWUHQr3"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bg9zUQnVkYLgAWJvL9MjP4tFDecCxbvmQRqrAuZpQUA"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgu2xgHEJocs4tggHDEwNnmgduftnXfJuWoLiUYfiLW"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgd4MvpBH3LaVz6sHvqFphoUex4taUe2E5mKuk4sVXn"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgSfpx2Pr3bHYev6ikwTqdBo2aaPGgjEseAWhjxp6F5"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgfaB4sngcm7cARjjiEvKfWE87owf2HuDfYDy8EyP45"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgzmGhu9qcyLW6qR1HKQuLTY6PWktNSAuzLNmo7aiQY"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgMTi1qFtbiFiHsURKW4Bfg4wjXtT8iJL7HC1z3gXsm"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgDRbhSLK62ApA2PbZs1W7SecodGhTFf6udU3MWDadu"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bg67LJN9Ngvfq4hJbSmm7tZ2wqmn2f1pxXbXW5QfxRz"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgmnrKWgN5jE8pF3PbFxRWYaho1bjCtmcTZ9VfRbhxf"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgTZqxCX4ej98P1UyYJjgGmGDmst7nteSyUWDwzMxNj"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgauKkwFcT8w7SHau9NufDfvmq1cy79X52bRbL6yzEB"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bg8WP8cEtWhdCjDd7rrwzsnz7K9f3oiEm2Qqu7TYmDn"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("DzrVK357ynzkPtdC7jzUbXgsUY8ULUeR2ihoPcX1JB3n"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfn2d1g6xkwhykkyjtoccFbC7r19ADf5dGB2YnT1Hgw"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfnXi2FdpFUUn6VyoxUohNyWk2Nup3ruguTgK8jaZaF"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bg1rCzhyASbzib75ohpRfNY3mGJaX1k6v56WCrUkh3a"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgsouue9XeHUzNwwuAKqBj1Fk1RbJkcBjvs4zkmUhLc"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("mwGELGMgGGrNL1UibNCQeJHDE7qdPptWRYB6noUHmTj"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfn9b35be4L7xh7G8P2jUzWsJAigrKDSoBeRMiyg75p"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfnSbG36fCGpT8WsB1NEbQ2BH11iog6qjFqMEVCZZgV"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfnaxrmMoemfvbhXek6offTNXas11GtepGQMN9UF3gk"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfnKWwarhjuKKg3WV3nw3wAE8zuymigT3vuJHwZeL4s"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfnaZXdkjJq26auzzFKeQm7YKphuNCdDGcJVqqb6awr"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfnP85qobXv2wETniKjXBhxKvgivpfT8EGAcS8sb3bq"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfnSAoQWtJCDKnjmR8oduqbZYXr69Q4cFQ6VhgFkvgT"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfnSbziLrSSVNqPBD9tpx3Ud4VtbxwsXjdfYv9SmBDx"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("Vn7tfMvrvrymGYMnxhj1DV16Sz2R9YXmaXF3hiSAHuC"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfnTcJ1i4mRYzbqGduF71RsooUCFkPSpk8UE7drCkjh"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfnUxCuZcfP6yidkG3EsqyR5DTbyie3R74fGoA5oB3J"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfnEJvqLGddJxQTA9DcYLTbVwiFdT3KmLXo6UcnmcgC"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfn6dyanKiTTinHs887D7qe2S4727wzK7xi7ERGaizC"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgDETv6tnt9mwYqAKebLXY5B5o6akiKJmAdU7Gd9G7H"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgH7YhymSykyvMa3nAZpzvrn73owJHU5iB75S1aiLT9"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfngGVVQLiVRFbLWw3Ektiv17ef9NiRZbcgdAhh4ZEW"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("nEFs3jph8HJt7honu3k7XtGUufMnwAvSXmXcKSPxryP"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("Fa1con11xLjPddfzRwRUB16sbFZggp2JeJkCeWREyR8X"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con11TM1RuAQzbQzYjTy4Ekfap9Lnc9fnEbQYEd6Q"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con113Bvi76nS5AzUiRDC2fqjfzkNMUNRLgQybMYt"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con1QGHJK232s8yZpzZZwqPexnAKcoyKj626LNsMv"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con1zUzb6qJVFz5tNkPq1Ahm8H1qKW7Q48252QbkQ"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con16d3MSwd3SAiwvr2LwgkpE7ot8zntbpuec8HAx"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con1i7mpa7Qc6epYJ6r4P9AbU77DFFz173r59Df1x"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con18nWn8TdAGL7JX8PertfMUGVSc899NawokJ4Bq"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con1GKusK2EqsfzrDzGPaYZSxQtFGzJiRMMU9Zm2g"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con1RDwVwM9VrJ53CwVefD3VU9c58EMpDawV7fLMi"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Sp1x2AqpQckPLaWnWCJUNg8k6qQexfaEWcSRKf5JcDV"): MevagentBlocksprint,
solana.MustPublicKeyFromBase58("Sp4JHSh9cksfzXbgK7Pq2ovtn8LirLQydaJKTsiNT77"): MevagentBlocksprint,
solana.MustPublicKeyFromBase58("Sp1xMS2cbw83SZDNr4AGqkBYYLjb3LvVnmDSrTMaHkr"): MevagentBlocksprint,
solana.MustPublicKeyFromBase58("SpagSJmnh8E9cGT5Y431xPPaS2c1xLREGGCWN9yDeUf"): MevagentBlocksprint,
solana.MustPublicKeyFromBase58("SpWrza9E63MQuHeGnnfzmtLVCs3pBdjyKPXUABPo9nq"): MevagentBlocksprint,
solana.MustPublicKeyFromBase58("moon17L6BgxXRX5uHKudAmqVF96xia9h8ygcmG2sL3F"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moon26Sek222Md7ZydcAGxoKG832DK36CkLrS3PQY4c"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moon7fwyajcVstMoBnVy7UBcTx87SBtNoGGAaH2Cb8V"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonBtH9HvLHjLqi9ivyrMVKgFUsSfrz9BwQ9khhn1u"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonCJg8476LNFLptX1qrK8PdRsA1HD1R6XWyu9MB93"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonF2sz7qwAtdETnrgxNbjonnhGGjd6r4W4UC9284s"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonKfftMiGSak3cezvhEqvkPSzwrmQxQHXuspC96yj"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonQBUKBpkifLcTd78bfxxt4PYLwmJ5admLW6cBBs8"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonXwpKwoVkMegt5Bc776cSW793X1irL5hHV1vJ3JA"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonZ6u9E2fgk6eWd82621eLPHt9zuJuYECXAYjMY1C"): MevAgentMoon,
solana.MustPublicKeyFromBase58("SpEEdz8S1KorkMZqjMUxfxrmWwofmp6ReNP2Nx6CUmq"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("SpeeDy3GJM4wcrQmk1itRFWgidvxX4rwjTLMv78wwjE"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("SPeEdva37vW8vRtqgYjprQs1g3965icfVN5Rt7SMAyh"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("speEdrSEpox5GUfHWcBc7tQjRuSfUin2yvB7qoYvvJh"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("SPeEDmkHkN3A2roSZf6aZyEMsmrGqTHKqwP51y2Y4rV"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("SpeedLdTJXh2RKpXEaP8JCxkWoUVXhtdPQ1EnxBJMxc"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("SpEediGKLbbXndSYTzwmz6Z3NDgHQLDcTDEvGFkSMH9"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("harkEpXoJv5qVzHaN7HSuUAd6PHjyMcFMcDYBMDJCEQ"): MevAgentAllenhark,
solana.MustPublicKeyFromBase58("harkm2BTWxZuszoNpZnfe84jRbQTg6KGHaQBmWzDGQQ"): MevAgentAllenhark,
solana.MustPublicKeyFromBase58("harkR2YJ4Dpt4UDJTcBirjnSPBhNpQFcoFkNpCkVqNk"): MevAgentAllenhark,
solana.MustPublicKeyFromBase58("t3QLYyXH4vZYbEifLqjD581t5dPVhq9LABxWceySzL2"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t46SqGmwStEffUMp1fr2xmv5uyR85TB9annJuLKLf83"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t1TcSg9biJsz4NjKjhopK8QZzPS4KzBgFSszu5QTGgF"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t2XFAFBaUkCzxJwEbLWFX9PKFjfBCp2tSyFtx5z4RZM"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t55hdzzftxWkYy3J8t32C9RRcZDuMZ4LDuBmbTzJFkU"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t6UtTQLUGHJJrzxAb8PBBZdZKra8SWUqvTv9zPnxKNz"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t7qUQU35sLpPydh42BcPmtEfTWW8gBe4Ry3gjwVnokJ"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t8pPgarSK3TnuLbbHmoE1RCQdLxfxuPqNTyFjBKahok"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t96GGdw3MiaGR993XN8PSsRpKGXx56t5Wf6zcF1hBpY"): MevAgentRaiden,
}
var entryContractAddresses = map[solana.PublicKey]string{
@@ -196,7 +416,19 @@ var entryContractAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e"): EntryContractMayhem,
solana.MustPublicKeyFromBase58("proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u"): EntryContractOKXDexRouterV2,
solana.MustPublicKeyFromBase58("term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3"): EntryContractTerm,
solana.MustPublicKeyFromBase58("DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH"): EntryContractDFlow,
solana.MustPublicKeyFromBase58("MaestroAAe9ge5HTc64VbBQZ6fP77pwvrhM8i1XWSAx"): EntryContractMaestroBot,
solana.MustPublicKeyFromBase58("BBRouter1cVunVXvkcqeKkZQcBK7ruan37PPm3xzWaXD"): EntryContractBonkBot,
solana.MustPublicKeyFromBase58("B3111yJCeHBcA1bizdJjUFPALfhAfSRnAbJzGUtnt56A"): EntryContractBinanceWallet,
solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9"): EntryContractAxiom,
solana.MustPublicKeyFromBase58("F5tfvbLog9VdGUPqBDTT8rgXvTTcq7e5UiGnupL1zvBq"): EntryContractAxiom,
solana.MustPublicKeyFromBase58("Gz9VPiSLQYbvKyb3jZPjNfyA6n4T4qVFUuAukgL964nL"): EntryContractAxiom,
solana.MustPublicKeyFromBase58("B3jytJa6Tzpn4Ly7GNnDm3dMGqUin5aMRm5aPsJGU5G7"): EntryContractTradewiz,
solana.MustPublicKeyFromBase58("DBotWvSso9oD1ZB3aHx2LiD2ZoFpF8PbKjaT4uHKLLVs"): EntryContractDbot,
solana.MustPublicKeyFromBase58("term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3"): EntryContractPadre,
}
var okxDexRoutersV2 = solana.MustPublicKeyFromBase58("proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u")
var okxAggregatorV2 = solana.MustPublicKeyFromBase58("6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma")
var axiomOuterContract = solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9")

33
enum.go
View File

@@ -11,6 +11,16 @@ const (
MevAgentFlashBlock = "flashBlock"
MevAgentUnknown = "unknown"
MevAgentBlockRazor = "blockrazor"
MevAgentFast = "fast"
MevAgentSoyas = "soyas"
MevAgentStellium = "stellium"
MevAgentAstralane = "astralane"
MevagentFa1con = "fa1con"
MevagentBlocksprint = "blocksprint"
MevAgentMoon = "moon"
MevAgentSpeedlanding = "speedlanding"
MevAgentAllenhark = "allenhark"
MevAgentRaiden = "raiden"
)
const (
@@ -42,8 +52,15 @@ const (
EntryContractFluxbeamDEX = "fluxbeamDEX"
EntryContractNovaBotsProgram = "novaBotsProgram"
EntryContractTaggedSearcher = "taggedSearcher"
EntryContractPadre = "padre"
EntryContractDFlow = "dflow"
EntryContractMaestroBot = "maestroBot"
EntryContractBonkBot = "bonkBot"
EntryContractBinanceWallet = "binanceWallet"
EntryContractMayhem = "pumpMayhem"
EntryContractTerm = "term"
EntryContractTradewiz = "tradewiz"
EntryContractDbot = "dbot"
EntryContractUnknown = "unknown"
)
@@ -64,6 +81,7 @@ const (
PlatformMaestro = "maestro"
PlatformBonkBot = "bonkbot"
PlatformPadre = "padre"
PlatformDexScreener = "dexscreener"
// used to flag transactions impersonating platform users
PlatformFake = "fake"
@@ -105,5 +123,20 @@ const (
TxEventRemoveLP = "remove"
TxEventBuy = "buy"
TxEventSell = "sell"
TxEventBuyFailed = "buy_failed"
TxEventSellFailed = "sell_failed"
TxEventBurn = "burn"
TxEventCreate = "create"
TxEventComplete = "complete"
TxEventMigrate = "migrate"
TxEventDeposit = "deposit"
TxEventWithdraw = "withdraw"
TxEventOpen = "open"
TxEventClose = "close"
TxEventClaimFee = "claim_fee"
TxEventAddLiquidity = "add_liquidity"
TxEventAddLiquidityOneSide = "add_liquidity_one_side"
TxEventRemoveLiquidity = "remove_liquidity"
TxEventRemoveLiquidityOneSide = "remove_liquidity_one_side"
)

154
error.go
View File

@@ -79,54 +79,54 @@ const (
const (
GenericError InstructionErrorVariant = iota
/// The arguments provided to a program were invalid
// InvalidArgument / The arguments provided to a program were invalid
InvalidArgument
/// An instruction's data contents were invalid
// InvalidInstructionData / An instruction's data contents were invalid
InvalidInstructionData
/// An account's data contents was invalid
// InvalidAccountData / An account's data contents was invalid
InvalidAccountData
/// An account's data was too small
// AccountDataTooSmall / An account's data was too small
AccountDataTooSmall
/// An account's balance was too small to complete the instruction
// InsufficientFunds / An account's balance was too small to complete the instruction
InsufficientFunds
/// The account did not have the expected program id
// IncorrectProgramId / The account did not have the expected program id
IncorrectProgramId
/// A signature was required but not found
// MissingRequiredSignature / A signature was required but not found
MissingRequiredSignature
/// An initialize instruction was sent to an account that has already been initialized.
// AccountAlreadyInitialized / An initialize instruction was sent to an account that has already been initialized.
AccountAlreadyInitialized
/// An attempt to operate on an account that hasn't been initialized.
// UninitializedAccount / An attempt to operate on an account that hasn't been initialized.
UninitializedAccount
/// Program's instruction lamport balance does not equal the balance after the instruction
// UnbalancedInstruction / Program's instruction lamport balance does not equal the balance after the instruction
UnbalancedInstruction
/// Program illegally modified an account's program id
// ModifiedProgramId / Program illegally modified an account's program id
ModifiedProgramId
/// Program spent the lamports of an account that doesn't belong to it
// ExternalAccountLamportSpend / Program spent the lamports of an account that doesn't belong to it
ExternalAccountLamportSpend
/// Program modified the data of an account that doesn't belong to it
// ExternalAccountDataModified / Program modified the data of an account that doesn't belong to it
ExternalAccountDataModified
/// Read-only account's lamports modified
// ReadonlyLamportChange / Read-only account's lamports modified
ReadonlyLamportChange
/// Read-only account's data was modified
// ReadonlyDataModified / Read-only account's data was modified
ReadonlyDataModified
/// An account was referenced more than once in a single instruction
// DuplicateAccountIndex / An account was referenced more than once in a single instruction
// Deprecated, instructions can now contain duplicate accounts
DuplicateAccountIndex
/// Executable bit on account changed, but shouldn't have
// ExecutableModified / Executable bit on account changed, but shouldn't have
ExecutableModified
/// Rent_epoch account changed, but shouldn't have
// RentEpochModified / Rent_epoch account changed, but shouldn't have
RentEpochModified
/// The instruction expected additional account keys
// NotEnoughAccountKeys / The instruction expected additional account keys
NotEnoughAccountKeys
/// Program other than the account's owner changed the size of the account data
// AccountDataSizeChanged / Program other than the account's owner changed the size of the account data
AccountDataSizeChanged
/// The instruction expected an executable account
// AccountNotExecutable / The instruction expected an executable account
AccountNotExecutable
/// Failed to borrow a reference to account data, already borrowed
// AccountBorrowFailed / Failed to borrow a reference to account data, already borrowed
AccountBorrowFailed
/// Account data has an outstanding reference after a program's execution
// InstructionAccountBorrowOutstanding / Account data has an outstanding reference after a program's execution
InstructionAccountBorrowOutstanding
/// The same account was multiply passed to an on-chain program's entrypoint, but the program
// DuplicateAccountOutOfSync / The same account was multiply passed to an on-chain program's entrypoint, but the program
/// modified them differently. A program can only modify one instance of the account because
/// the runtime cannot determine which changes to pick or how to merge them if both are modified
DuplicateAccountOutOfSync
@@ -136,42 +136,42 @@ const (
Custom // Custom(u32),
/// The return value from the program was invalid. Valid errors are either a defined builtin
// InvalidError / The return value from the program was invalid. Valid errors are either a defined builtin
/// error value or a user-defined error in the lower 32 bits.
InvalidError
/// Executable account's data was modified
// ExecutableDataModified / Executable account's data was modified
ExecutableDataModified
/// Executable account's lamports modified
// ExecutableLamportChange / Executable account's lamports modified
ExecutableLamportChange
/// Executable accounts must be rent exempt
// ExecutableAccountNotRentExempt / Executable accounts must be rent exempt
ExecutableAccountNotRentExempt
/// Unsupported program id
// UnsupportedProgramId / Unsupported program id
UnsupportedProgramId
/// Cross-program invocation call depth too deep
// CallDepth / Cross-program invocation call depth too deep
CallDepth
/// An account required by the instruction is missing
// MissingAccount / An account required by the instruction is missing
MissingAccount
/// Cross-program invocation reentrancy not allowed for this instruction
// ReentrancyNotAllowed / Cross-program invocation reentrancy not allowed for this instruction
ReentrancyNotAllowed
/// Length of the seed is too long for address generation
// MaxSeedLengthExceeded / Length of the seed is too long for address generation
MaxSeedLengthExceeded
/// Provided seeds do not result in a valid address
// InvalidSeeds / Provided seeds do not result in a valid address
InvalidSeeds
/// Failed to reallocate account data of this length
// InvalidRealloc / Failed to reallocate account data of this length
InvalidRealloc
/// Computational budget exceeded
// ComputationalBudgetExceeded / Computational budget exceeded
ComputationalBudgetExceeded
/// Cross-program invocation with unauthorized signer or writable account
// PrivilegeEscalation / Cross-program invocation with unauthorized signer or writable account
PrivilegeEscalation
/// Failed to create program execution environment
// ProgramEnvironmentSetupFailure / Failed to create program execution environment
ProgramEnvironmentSetupFailure
/// Program failed to complete
// ProgramFailedToComplete / Program failed to complete
ProgramFailedToComplete
/// Program failed to compile
// ProgramFailedToCompile / Program failed to compile
ProgramFailedToCompile
/// Account is immutable
// Immutable / Account is immutable
Immutable
/// Incorrect authority provided
// IncorrectAuthority / Incorrect authority provided
IncorrectAuthority
/// Failed to serialize or deserialize account data
///
@@ -185,23 +185,23 @@ const (
BorshIoError // BorshIoError(String)
// An account does not have enough lamports to be rent-exempt
// AccountNotRentExempt An account does not have enough lamports to be rent-exempt
AccountNotRentExempt
/// Invalid account owner
// InvalidAccountOwner Invalid account owner
InvalidAccountOwner
/// Program arithmetic overflowed
// ArithmeticOverflow Program arithmetic overflowed
ArithmeticOverflow
/// Unsupported sysvar
// UnsupportedSysvar Unsupported sysvar
UnsupportedSysvar
/// Illegal account owner
// IllegalOwner Illegal account owner
IllegalOwner
/// Accounts data allocations exceeded the maximum allowed per transaction
// MaxAccountsDataAllocationsExceeded / Accounts data allocations exceeded the maximum allowed per transaction
MaxAccountsDataAllocationsExceeded
/// Max accounts exceeded
// MaxAccountsExceeded Max accounts exceeded
MaxAccountsExceeded
/// Max instruction trace length exceeded
// MaxInstructionTraceLengthExceeded Max instruction trace length exceeded
MaxInstructionTraceLengthExceeded
/// Builtin programs must consume compute units
// BuiltinProgramsMustConsumeComputeUnits Builtin programs must consume compute units
BuiltinProgramsMustConsumeComputeUnits
)
@@ -210,6 +210,15 @@ type TransactionError struct {
rest []byte
}
type TransactionParsedError struct {
Index uint8
Variant TransactionErrorVariant
Enum InstructionErrorVariant
CustomCode uint32
UnKnown string
}
var (
ErrInvalidTransactionError = errors.New("invalid transaction error")
NotAnInstructionError = errors.New("not an instruction error")
@@ -233,6 +242,49 @@ func DecodeTransactionError(data []byte) (*TransactionError, error) {
return &err, nil
}
func ParseTransactionErrorFromGeyser(data []byte) *TransactionParsedError {
if len(data) == 0 {
return nil
}
transactionError, err := DecodeTransactionError(data)
if err != nil {
return &TransactionParsedError{
UnKnown: string(data),
}
}
enumErr, err := transactionError.GetInstructionError()
if err != nil {
return &TransactionParsedError{
Variant: transactionError.Variant,
UnKnown: string(data),
}
}
if enumErr.Variant != Custom {
return &TransactionParsedError{
Index: enumErr.Index,
Variant: transactionError.Variant,
Enum: enumErr.Variant,
}
}
customCode, err := enumErr.Custom()
if err != nil {
return &TransactionParsedError{
Index: enumErr.Index,
Variant: transactionError.Variant,
Enum: enumErr.Variant,
UnKnown: string(data),
}
}
return &TransactionParsedError{
Index: enumErr.Index,
Variant: transactionError.Variant,
Enum: enumErr.Variant,
CustomCode: customCode.Code,
}
}
func (e *TransactionError) GetCustomErrorCode() (uint8, uint32, error) {
instr, err := e.GetInstructionError()
if err != nil {

47
error_test.go Normal file
View File

@@ -0,0 +1,47 @@
package pump_parser
import (
"encoding/base64"
"testing"
)
func TestDecodeTransactionError(t *testing.T) {
var testCases = []struct {
name string
data string
}{
{
name: "ComputationalBudgetExceeded",
data: "CAAAAAMlAAAA",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
bytesData, err := base64.StdEncoding.DecodeString(tc.data)
if err != nil {
t.Fatalf("Failed to decode base64 data for %s: %v", tc.name, err)
return
}
te, err := DecodeTransactionError(bytesData)
if err != nil {
t.Errorf("Decoded error for %s: %v", tc.name, err)
return
}
if te.Variant != InstructionError {
t.Errorf("Expected Variant to be InstructionError for %s, got %d", tc.name, te.Variant)
return
}
errName, err := te.GetInstructionError()
if err != nil {
t.Errorf("Failed to get instruction error for %s: %v", tc.name, err)
return
}
if errName.Variant != ComputationalBudgetExceeded {
t.Errorf("Expected instruction error variant to be Custom for %s, got %d", tc.name, errName.Variant)
return
}
})
}
}

View File

@@ -1,6 +1,7 @@
package pump_parser
import (
"encoding/binary"
"errors"
"fmt"
@@ -38,6 +39,36 @@ func getInnerInstructions(innerInstructions InnerInstructions, offset uint) ([]I
return inners, nil
}
func parseTokenTransfer(tx *RawTx, instr Instruction) (from solana.PublicKey, to solana.PublicKey, amount uint64, err error) {
if len(instr.Accounts) < 3 {
return solana.PublicKey{}, solana.PublicKey{}, 0, fmt.Errorf("not enough accounts for token transfer instruction")
}
programAccount := tx.accountList[instr.ProgramIDIndex]
if !programAccount.Equals(solana.TokenProgramID) && !programAccount.Equals(solana.Token2022ProgramID) {
return solana.PublicKey{}, solana.PublicKey{}, 0, fmt.Errorf("not a token program instruction")
}
if len(instr.Data) < 9 {
return solana.PublicKey{}, solana.PublicKey{}, 0, fmt.Errorf("invalid data length for token transfer instruction")
}
method := instr.Data[0]
if method != 3 && method != 12 { // Transfer instruction
return solana.PublicKey{}, solana.PublicKey{}, 0, fmt.Errorf("not a token transfer instruction")
}
if method == 3 {
// Transfer
amount = binary.LittleEndian.Uint64(instr.Data[1:9])
from = tx.accountList[instr.Accounts[0]]
to = tx.accountList[instr.Accounts[1]]
} else {
// TransferChecked
amount = binary.LittleEndian.Uint64(instr.Data[1:9])
from = tx.accountList[instr.Accounts[0]]
to = tx.accountList[instr.Accounts[2]]
}
return from, to, amount, nil
}
func isMayhemPump(feeAccount solana.PublicKey) bool {
for _, mayhemFeeAccount := range mayhemFeeAccounts {
if feeAccount.Equals(mayhemFeeAccount) {

10
go.mod
View File

@@ -10,11 +10,12 @@ require (
github.com/shopspring/decimal v1.4.0
go.onsig.ai/onsig/yellowstone-proto v1.0.0
google.golang.org/grpc v1.78.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/blendle/zapdriver v1.3.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -22,6 +23,12 @@ require (
github.com/gagliardetto/treeout v0.1.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
@@ -39,6 +46,7 @@ require (
go.uber.org/zap v1.27.1 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect

71
go.sum
View File

@@ -1,14 +1,9 @@
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI=
github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI=
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -21,8 +16,6 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg=
@@ -41,8 +34,6 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -80,8 +71,9 @@ github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwX
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
@@ -95,16 +87,22 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU=
github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -122,14 +120,11 @@ github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -140,7 +135,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk=
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
@@ -148,8 +142,6 @@ github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjW
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -165,8 +157,6 @@ github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+D
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU=
github.com/streamingfast/logging v0.0.0-20250918142248-ac5a1e292845 h1:VMA0pZ3MI8BErRA3kh8dKJThP5d0Xh5vZVk5yFIgH/A=
github.com/streamingfast/logging v0.0.0-20250918142248-ac5a1e292845/go.mod h1:BtDq81Tyc7H8up5aXNi/I95nPmG3C0PLEqGWY/iWQ2E=
github.com/streamingfast/logging v0.0.0-20251216203033-fdad0a00f1ca h1:D9r6WXATiqumhUTqSysurIi3N50z4orVBW+TEMp50Q4=
github.com/streamingfast/logging v0.0.0-20251216203033-fdad0a00f1ca/go.mod h1:fJ5nP7ZSMB4MQQ6RM7cF+LiSQ43b5cVletcSUNL8z2M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -178,7 +168,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -186,15 +175,9 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws=
go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.onsig.ai/onsig/yellowstone-proto v1.0.0 h1:+XBNIoyl3HoQGBhgWCf8Ma3zNoUHKorFV8tR+HnE4Lw=
@@ -215,29 +198,24 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA=
go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
@@ -252,11 +230,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -276,14 +251,14 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 h1:6/3JGEh1C88g7m+qzzTbl3A0FtsLguXieqofVLU/JAo=
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -305,8 +280,6 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -315,8 +288,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -325,16 +296,11 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -353,21 +319,14 @@ golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -382,4 +341,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=

View File

@@ -5,10 +5,17 @@ import (
"fmt"
"time"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
parser "github.com/thloyi/pump-parser"
example "github.com/thloyi/pump-parser/internal/example"
)
var (
pumpProgram = solana.MustPublicKeyFromBase58("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
pumpAmmProgram = solana.MustPublicKeyFromBase58("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
)
func main() {
//pool, err := ants.NewPool(100, ants.WithPreAlloc(true), ants.WithNonblocking(true))
//if err != nil {
@@ -18,7 +25,7 @@ func main() {
// laserstream-mainnet-slc.helius-rpc.com:80
ch := make(chan example.SubscriptionMessage, 1)
go example.RunLoopWithReConnect(context.Background(), "127.0.0.1:10001", parser.SolProgramPump, ch)
go example.RunLoopWithReConnect(context.Background(), "", "", parser.SolProgramPump, ch)
// var tokenTxs = make(map[string]*types.Tx)
// currentBlock := uint64(0)
for msg := range ch {
@@ -31,6 +38,7 @@ func main() {
continue
}
ptx := msg.Tx
// fmt.Println("consume", ptx.ComputeUnitsConsumed, "limit", ptx.CuLimit, "hash", ptx.GetTxHash())
//data, _ := json.Marshal(tx)
//fmt.Println(string(data))
//continue
@@ -43,42 +51,23 @@ func main() {
//}
// 处理交易
txErr, ok := ptx.Err.(*parser.TransactionError)
var customerErrCode uint32
var instructorErrIndex uint8
if ok {
instructorErrIndex, customerErrCode, _ = txErr.GetCustomErrorCode()
fmt.Printf("now: %s, block: %d, tx: %s, errInstr Code: %d, errInstrIndex: %d, err: %v\n", time.Now().Format("2006-01-02 15:04:05"), ptx.Block, ptx.GetTxHash(), customerErrCode, instructorErrIndex, ptx.Err)
} else {
txs := example.FromTx(ptx)
if len(txs) == 0 {
fmt.Printf("tx is empty, block: %d, tx %s \n", ptx.Block, ptx.GetTxHash())
continue
if len(ptx.Swaps) > 0 {
for _, swap := range ptx.Swaps {
if swap.SlippageBps.LessThan(decimal.Zero) || swap.SlippageBps.GreaterThan(decimal.NewFromInt(10000)) {
fmt.Printf("success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s \n", time.Now().Format("2006-01-02 15:04:05"), swap.Program, swap.Event, ptx.Block, ptx.GetTxHash(),
swap.BaseAmount.Div(decimal.NewFromInt(1e6)), swap.QuoteAmount.Div(decimal.NewFromInt(1e9)))
}
// printed := false
for _, tx := range txs {
if tx.Program != parser.SolProgramPumpAMM {
continue
if swap.SlippageBps.Equal(decimal.Zero) && (swap.Event == "buy" || swap.Event == "sell") {
fmt.Printf("zero success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s, fix: %s, limit: %s, \n", time.Now().Format("2006-01-02 15:04:05"), swap.Program, swap.Event, ptx.Block, ptx.GetTxHash(),
swap.BaseAmount.Div(decimal.NewFromInt(1e6)), swap.QuoteAmount.Div(decimal.NewFromInt(1e9)), swap.FixedAmount.String(), swap.LimitAmount.String())
}
if tx.EntryContract == "" || tx.EntryContract == parser.SolProgramPumpAMM || tx.EntryContract == parser.EntryContractOKXDexRouterV2 || tx.EntryContract == "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" {
continue
}
//if tx.Token1Amount.GreaterThanOrEqual(decimal.NewFromFloat(0.1)) || tx.Event != "buy" {
// continue
//}
// printed = true
fmt.Printf("t: %s, block: %d, hash: %s, maker: %s, program: %s, event: %s, token0: %s, entryContract: %s, token balance: %s, EntryContract: %s\n",
time.Now().Format(time.RFC3339Nano),
tx.Block, tx.GetTxHash(), tx.Maker, tx.Program, tx.Event, tx.Token0Amount, tx.EntryContract, tx.AfterSignerToken0Balance, tx.EntryContract)
//break
if len(ptx.Swaps) > 0 {
_, err := parser.EncodeTxBinary(ptx)
if err != nil {
fmt.Printf("success tx : %s, , block: %d, tx: %s, err: %s \n", time.Now().Format("2006-01-02 15:04:05"), ptx.Block, ptx.GetTxHash(), err.Error())
}
}
//if !printed {
// continue
//}
//fmt.Printf("t: %s, block: %d, hash: %s, signer: %s, program: %s, event: %s, token0: %s, token1: %s, signer before sol :%s, after sol: %s, after token: %s, tokencreator: %s, tokenprogram: %s, mayhem: %t\n",
// time.Now().Format(time.RFC3339Nano),
// tx.Block, tx.GetTxHash(), tx.Maker, tx.Program, tx.Event, tx.Token0Amount.String(), tx.Token1Amount.String(),
// tx.BeforeSolBalance, tx.AfterSOLBalance, tx.AfterSignerToken0Balance, tx.TokenCreator, tx.Token0Program, tx.Mayhem)
}
// currentBlock = ptx.Block

View File

@@ -3,7 +3,6 @@ package parser
import (
"fmt"
"github.com/shopspring/decimal"
types "github.com/thloyi/pump-parser"
)
@@ -23,24 +22,14 @@ func NewPumpHandler(cb func(*types.Tx)) *PumpHandler {
func (h *PumpHandler) HandleMessage(rawTx *types.RawTx) {
if rawTx.Meta.Err != nil {
// Notify the channel about the failed transaction
beforeSolBalance := decimal.Zero
afterSolBalance := decimal.Zero
if rawTx.Meta.PreBalances != nil && len(rawTx.Meta.PreBalances) > 0 {
beforeSolBalance = decimal.NewFromUint64(rawTx.Meta.PreBalances[0]).Div(decimal.NewFromInt(1e9))
var parsedTx = &types.Tx{}
parsedTx.SetRawTx(rawTx)
err := parsedTx.Parser()
if err != nil {
fmt.Printf("parser failed tx error: %s, block: %d tx: %s\n", err, rawTx.Slot, rawTx.TxHash())
return
}
if rawTx.Meta.PostBalances != nil && len(rawTx.Meta.PostBalances) > 0 {
afterSolBalance = decimal.NewFromUint64(rawTx.Meta.PostBalances[0]).Div(decimal.NewFromInt(1e9))
}
h.callback(&types.Tx{
TxHash: (*[64]byte)((rawTx.Transaction.Signatures[0][:])),
Err: rawTx.Meta.Err,
Signer: rawTx.GetSigner(),
Block: rawTx.Slot,
BlockIndex: uint64(rawTx.IndexWithinBlock),
BeforeSolBalance: beforeSolBalance,
AfterSOLBalance: afterSolBalance,
})
h.callback(parsedTx)
return
}

View File

@@ -51,6 +51,7 @@ type Tx struct {
EntryContract string `gorm:"column:tx_entry_contract;type:entry_contract;default:'none'" json:"tx_entry_contract"`
Mayhem bool
Cashback bool `json:"is_cashback_coin"`
}
func (tx *Tx) GetTxHash() string {
@@ -121,6 +122,7 @@ func FromTx(tx *parser.Tx) []*Tx {
EntryContract: s.CheckEntryContract(),
Mayhem: s.Mayhem,
Cashback: s.Cashback,
}
} else if s.Program == "PumpAMM" {
if s.BaseMint.Equals(solana.WrappedSol) {
@@ -175,6 +177,7 @@ func FromTx(tx *parser.Tx) []*Tx {
EntryContract: s.CheckEntryContract(),
Mayhem: s.Mayhem,
Cashback: s.Cashback,
}
} else {
newTx = &Tx{
@@ -219,6 +222,7 @@ func FromTx(tx *parser.Tx) []*Tx {
EntryContract: s.CheckEntryContract(),
Mayhem: s.Mayhem,
Cashback: s.Cashback,
}
}
}

View File

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

866
internal/test/test.go Normal file
View File

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

60
internal/test2/test.go Normal file
View File

@@ -0,0 +1,60 @@
package main
import (
"context"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
solana_parser "github.com/thloyi/pump-parser"
)
var ()
func main() {
var slot uint64 = 414696178
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
var rewards = false
var version uint64 = 0
blocks, err := client.GetBlockWithOpts(context.Background(), slot, &rpc.GetBlockOpts{
TransactionDetails: rpc.TransactionDetailsFull,
Rewards: &rewards,
Commitment: rpc.CommitmentFinalized,
Encoding: solana.EncodingBase64,
MaxSupportedTransactionVersion: &version,
})
if err != nil {
slot++
fmt.Println("get block error:", err)
return
}
solana_parser.EnableAllParsers()
var txs []solana_parser.Tx
for i, tx := range blocks.Transactions {
var blockTime uint64
if blocks.BlockTime != nil {
blockTime = uint64(*blocks.BlockTime)
}
rawTx, err := solana_parser.FromRpcTransactionWithMeta(tx, &blockTime, slot, int64(i))
if err != nil {
fmt.Println("from rpc tx error:", i, err)
break
}
//if rawTx.Meta.Err != nil {
// continue
//}
parsedTx, err := solana_parser.ParseRawTx(rawTx)
if err != nil {
fmt.Println("parse tx error:", i, rawTx.TxHash(), err)
break
}
txs = append(txs, *parsedTx)
}
_, err = solana_parser.EncodeTxsBinary(txs)
if err != nil {
fmt.Println("EncodeTxsBinary err", err)
}
}

821
internal/test3/test.go Normal file
View File

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

193
meta.go
View File

@@ -37,6 +37,10 @@ var pumpBuyEventDiscriminator = [8]byte{189, 219, 127, 211, 78, 230, 97, 238}
var (
pumpAmmProgram = solana.MustPublicKeyFromBase58("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
wSolMint = solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112")
usdcMint = solana.MustPublicKeyFromBase58("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
usd1Mint = solana.MustPublicKeyFromBase58("USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB")
meteoraDlmmProgram = solana.MustPublicKeyFromBase58("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo")
meteoraDammV2Program = solana.MustPublicKeyFromBase58("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG")
)
var (
@@ -63,6 +67,190 @@ var (
pumpAmmDepositEventDiscriminator = calculateDiscriminator("event:DepositEvent")
)
var (
meteoraInitializeCustomizablePermissionlessLbPairDiscriminator = calculateDiscriminator("global:initialize_customizable_permissionless_lb_pair")
meteoraInitializeCustomizablePermissionlessLbPair2Discriminator = calculateDiscriminator("global:initialize_customizable_permissionless_lb_pair2")
meteoraInitializeLbPairDiscriminator = calculateDiscriminator("global:initialize_lb_pair")
meteoraInitializeLbPair2Discriminator = calculateDiscriminator("global:initialize_lb_pair2")
meteoraInitializePermissionLbPairDiscriminator = calculateDiscriminator("global:initialize_permission_lb_pair")
meteoraInitializeLbPairEventDiscriminator = calculateDiscriminator("event:LbPairCreate")
meteoraDlmmSwapDiscriminator = calculateDiscriminator("global:swap")
meteoraDlmmSwap2Discriminator = calculateDiscriminator("global:swap2")
meteoraDlmmSwapExactOutDiscriminator = calculateDiscriminator("global:swap_exact_out")
meteoraDlmmSwapExactOut2Discriminator = calculateDiscriminator("global:swap_exact_out2")
meteoraDlmmSwapWithPriceImpactDiscriminator = calculateDiscriminator("global:swap_with_price_impact")
meteoraDlmmSwapWithPriceImpact2Discriminator = calculateDiscriminator("global:swap_with_price_impact2")
meteoraDlmmInitializePositionDiscriminator = calculateDiscriminator("global:initialize_position")
meteoraDlmmInitializePosition2Discriminator = calculateDiscriminator("global:initialize_position2")
meteoraDlmmInitializePositionByOperatorDiscriminator = calculateDiscriminator("global:initialize_position_by_operator")
meteoraDlmmInitializePositionPdaDiscriminator = calculateDiscriminator("global:initialize_position_pda")
meteoraDlmmClosePositionDiscriminator = calculateDiscriminator("global:close_position")
meteoraDlmmClosePosition2Discriminator = calculateDiscriminator("global:close_position2")
meteoraDlmmClosePositionIfEmptyDiscriminator = calculateDiscriminator("global:close_position_if_empty")
meteoraDlmmSwapEventDiscriminator = calculateDiscriminator("event:Swap")
meteoraDlmmAddLiquidityDiscriminator = calculateDiscriminator("global:add_liquidity")
meteoraDlmmAddLiquidity2Discriminator = calculateDiscriminator("global:add_liquidity2")
meteoraDlmmAddLiquidityByStrategyDiscriminator = calculateDiscriminator("global:add_liquidity_by_strategy")
meteoraDlmmAddLiquidityByStrategy2Discriminator = calculateDiscriminator("global:add_liquidity_by_strategy2")
meteoraDlmmAddLiquidityByWeightDiscriminator = calculateDiscriminator("global:add_liquidity_by_weight")
meteoraDlmmAddLiquidityOneSideDiscriminator = calculateDiscriminator("global:add_liquidity_one_side")
meteoraDlmmAddLiquidityOneSidePreciseDiscriminator = calculateDiscriminator("global:add_liquidity_one_side_precise")
meteoraDlmmAddLiquidityOneSidePrecise2Discriminator = calculateDiscriminator("global:add_liquidity_one_side_precise2")
meteoraDlmmAddLiquidityByStrategyOneSideDiscriminator = calculateDiscriminator("global:add_liquidity_by_strategy_one_side")
meteoraDlmmClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee")
meteoraDlmmClaimFee2Discriminator = calculateDiscriminator("global:claim_fee2")
meteoraDlmmRebalanceLiquidityDiscriminator = calculateDiscriminator("global:rebalance_liquidity")
meteoraDlmmRemoveAllLiquidityDiscriminator = calculateDiscriminator("global:remove_all_liquidity")
meteoraDlmmRemoveLiquidityDiscriminator = calculateDiscriminator("global:remove_liquidity")
meteoraDlmmRemoveLiquidity2Discriminator = calculateDiscriminator("global:remove_liquidity2")
meteoraDlmmRemoveLiquidityByRangeDiscriminator = calculateDiscriminator("global:remove_liquidity_by_range")
meteoraDlmmRemoveLiquidityByRange2Discriminator = calculateDiscriminator("global:remove_liquidity_by_range2")
meteoraDlmmAddLiquidityEventDiscriminator = calculateDiscriminator("event:AddLiquidity")
meteoraDlmmClaimFeeEventDiscriminator = calculateDiscriminator("event:ClaimFee")
meteoraDlmmClaimFee2EventDiscriminator = calculateDiscriminator("event:ClaimFee2")
meteoraDlmmPositionCloseEventDiscriminator = calculateDiscriminator("event:PositionClose")
meteoraDlmmPositionCreateEventDiscriminator = calculateDiscriminator("event:PositionCreate")
meteoraDlmmRebalancingEventDiscriminator = calculateDiscriminator("event:Rebalancing")
meteoraDlmmRemoveLiquidityEventDiscriminator = calculateDiscriminator("event:RemoveLiquidity")
)
var (
// metaora pool
metaoraPoolProgramID = solana.MustPublicKeyFromBase58("Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB")
metaoraPoolInitializePermissionedPoolDiscriminator = calculateDiscriminator("global:initialize_permissioned_pool")
metaoraPoolInitializePermissionlessPoolDiscriminator = calculateDiscriminator("global:initialize_permissionless_pool")
metaoraPoolInitializePermissionlessPoolWithFeeTierDiscriminator = calculateDiscriminator("global:initialize_permissionless_pool_with_fee_tier")
metaoraPoolInitializePermissionlessConstantProductPoolWithConfigDiscriminator = calculateDiscriminator("global:initialize_permissionless_constant_product_pool_with_config")
metaoraPoolInitializePermissionlessConstantProductPoolWithConfig2Discriminator = calculateDiscriminator("global:initialize_permissionless_constant_product_pool_with_config2")
metaoraPoolInitializeCustomizablePermissionlessConstantProductPoolDiscriminator = calculateDiscriminator("global:initialize_customizable_permissionless_constant_product_pool")
metaoraPoolSwapDiscriminator = calculateDiscriminator("global:swap")
metaoraPoolAddImbalanceLiquidityDiscriminator = calculateDiscriminator("global:add_imbalance_liquidity")
metaoraPoolAddBalanceLiquidityDiscriminator = calculateDiscriminator("global:add_balance_liquidity")
metaoraPoolRemoveLiquiditySingleSideDiscriminator = calculateDiscriminator("global:remove_liquidity_single_side")
metaoraPoolRemoveBalanceLiquidityDiscriminator = calculateDiscriminator("global:remove_balance_liquidity")
metaoraPoolClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee")
metaoraPoolBootstrapLiquidityDiscriminator = calculateDiscriminator("global:bootstrap_liquidity")
)
var (
metaoraBcProgramID = solana.MustPublicKeyFromBase58("dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN")
metaoraBcInitializedPoolDiscriminator = calculateDiscriminator("global:initialize_virtual_pool_with_spl_token")
metaoraBcInitialize2022PoolDiscriminator = calculateDiscriminator("global:initialize_virtual_pool_with_token2022")
metaoraBcMigrateMeteoraDammDiscriminator = calculateDiscriminator("global:migrate_meteora_damm")
metaoraBcMigrateMeteoraDammV2Discriminator = calculateDiscriminator("global:migration_damm_v2")
metaoraBcSwapDiscriminator = calculateDiscriminator("global:swap")
metaoraBcSwapV2Discriminator = calculateDiscriminator("global:swap2")
metaoraBcEventInitializePoolDiscriminator = [8]byte{228, 50, 246, 85, 203, 66, 134, 37}
metaoraBcEventSwapDiscriminator = [8]byte{27, 60, 21, 213, 138, 170, 187, 147}
metaoraBcEventSwap2Discriminator = [8]byte{189, 66, 51, 168, 38, 80, 117, 153}
metaoraBcEventCompleteDiscriminator = [8]byte{229, 231, 86, 84, 156, 134, 75, 24}
)
var (
meteoraDammV2AddLiquidityDiscriminator = calculateDiscriminator("global:add_liquidity")
meteoraDammV2RemoveLiquidityDiscriminator = calculateDiscriminator("global:remove_liquidity")
meteoraDammV2RemoveAllLiquidityDiscriminator = calculateDiscriminator("global:remove_all_liquidity")
meteoraDammV2SwapDiscriminator = calculateDiscriminator("global:swap")
meteoraDammV2SwapV2Discriminator = calculateDiscriminator("global:swap2")
meteoraDammV2InitializeCustomizablePoolDiscriminator = calculateDiscriminator("global:initialize_customizable_pool")
meteoraDammV2InitializePoolWithDynamicConfig = calculateDiscriminator("global:initialize_pool_with_dynamic_config")
meteoraDammV2InitializePoolDiscriminator = calculateDiscriminator("global:initialize_pool")
)
var (
orcaProgramID = solana.MustPublicKeyFromBase58("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc")
orcaInitializePoolDiscriminator = calculateDiscriminator("global:initialize_pool")
orcaInitializePoolV2Discriminator = calculateDiscriminator("global:initialize_pool_v2")
orcaInitializePoolWithAdaptiveFeeDiscriminator = calculateDiscriminator("global:initialize_pool_with_adaptive_fee")
orcaIncreaseLiquidityDiscriminator = calculateDiscriminator("global:increase_liquidity")
orcaDecreaseLiquidityDiscriminator = calculateDiscriminator("global:decrease_liquidity")
orcaDecreaseLiquidityV2Discriminator = calculateDiscriminator("global:decrease_liquidity_v2")
orcaIncreaseLiquidityV2Discriminator = calculateDiscriminator("global:increase_liquidity_v2")
orcaCollectFeesDiscriminator = calculateDiscriminator("global:collect_fees")
orcaCollectProtocolFeesDiscriminator = calculateDiscriminator("global:collect_protocol_fees")
orcaCollectFeesV2Discriminator = calculateDiscriminator("global:collect_fees_v2")
orcaCollectProtocolFeesV2Discriminator = calculateDiscriminator("global:collect_protocol_fees_v2")
orcaSwapDiscriminator = calculateDiscriminator("global:swap")
orcaTwoHopSwapDiscriminator = calculateDiscriminator("global:two_hop_swap")
orcaSwapV2Discriminator = calculateDiscriminator("global:swap_v2")
orcaTwoHopSwapV2Discriminator = calculateDiscriminator("global:two_hop_swap_v2")
)
var (
raydiumClmmProgramID solana.PublicKey = solana.MustPublicKeyFromBase58("CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK")
raydiumClmmCreatePoolDiscriminator = calculateDiscriminator("global:create_pool")
raydiumClmmCollectProtocolFeeDiscriminator = calculateDiscriminator("global:collect_protocol_fee")
raydiumClmmCollectFundFeeDiscriminator = calculateDiscriminator("global:collect_fund_fee")
raydiumClmmOpenPositionDiscriminator = calculateDiscriminator("global:open_position")
raydiumClmmOpenPositionV2Discriminator = calculateDiscriminator("global:open_position_v2")
raydiumClmmOpenPositionWithToken22NftDiscriminator = calculateDiscriminator("global:open_position_with_token22_nft")
raydiumClmmIncreaseLiquidityDiscriminator = calculateDiscriminator("global:increase_liquidity")
raydiumClmmDecreaseLiquidityDiscriminator = calculateDiscriminator("global:decrease_liquidity")
raydiumClmmIncreaseLiquidityV2Discriminator = calculateDiscriminator("global:increase_liquidity_v2")
raydiumClmmDecreaseLiquidityV2Discriminator = calculateDiscriminator("global:decrease_liquidity_v2")
raydiumClmmSwapDiscriminator = calculateDiscriminator("global:swap")
raydiumClmmSwapV2Discriminator = calculateDiscriminator("global:swap_v2")
)
var (
raydiumCPmmProgramID = solana.MustPublicKeyFromBase58("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C")
raydiumCPmmSwapBaseInputDiscriminator = [8]byte{143, 190, 90, 218, 196, 30, 51, 222}
raydiumCPmmSwapBaseOutputDiscriminator = [8]byte{55, 217, 98, 86, 163, 74, 180, 173}
raydiumCPmmWithdrawDiscriminator = [8]byte{183, 18, 70, 156, 148, 109, 161, 34}
raydiumCPmmDepositDiscriminator = [8]byte{242, 35, 198, 137, 82, 225, 242, 182}
raydiumCPmmCollectProtocolFeeDiscriminator = [8]byte{136, 136, 252, 221, 194, 66, 126, 89}
raydiumCPmmCollectFundFeeDiscriminator = [8]byte{167, 138, 78, 149, 223, 194, 6, 126}
raydiumCPmmInitializeDiscriminator = [8]byte{175, 175, 109, 31, 13, 152, 155, 237}
raydiumCPmmInitializeWithPermissionDiscriminator = calculateDiscriminator("global:initialize_with_permission")
)
var (
raydiumV4Program = solana.MustPublicKeyFromBase58("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8")
)
const (
raydiumV4InitializePoolDiscriminator = uint8(1)
raydiumV4SwapBaseInDiscriminator = uint8(9)
raydiumV4SwapBaseOutDiscriminator = uint8(11)
raydiumV4SwapBaseInV2Discriminator = uint8(16)
raydiumV4SwapBaseOutV2Discriminator = uint8(17)
raydiumV4AddLiquidityDiscriminator = uint8(3)
raydiumV4RemoveLiquidityDiscriminator = uint8(4)
raydiumV4WithdrawPNLDiscriminator = uint8(7)
)
var (
raydiumLaunchLabProgramID = solana.MustPublicKeyFromBase58("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj")
bonkPlatformConfig = solana.MustPublicKeyFromBase58("FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1")
raydiumLaunchLabCreatePoolEvnet = [8]byte{151, 215, 226, 9, 118, 161, 115, 174}
raydiumLaunchLabTradeEvnet = [8]byte{189, 219, 127, 211, 78, 230, 97, 238}
raydiumLaunchLabInitializeV2PoolDiscriminator = [8]byte{67, 153, 175, 39, 218, 16, 38, 32}
raydiumLaunchLabInitializeWithToken2022PoolDiscriminator = [8]byte{37, 190, 126, 222, 44, 154, 171, 17}
raydiumLaunchLabSellExactInDiscriminator = [8]byte{0x95, 0x27, 0xde, 0x9b, 0xd3, 0x7c, 0x98, 0x1a}
raydiumLaunchLabSellExactOutDiscriminator = [8]byte{0x5f, 0xc8, 0x47, 0x22, 0x08, 0x09, 0x0b, 0xa6}
raydiumLaunchLabBuyExactInDiscriminator = [8]byte{0xfa, 0xea, 0x0d, 0x7b, 0xd5, 0x9c, 0x13, 0xec}
raydiumLaunchLabBuyExactOutDiscriminator = [8]byte{0x18, 0xd3, 0x74, 0x28, 0x69, 0x03, 0x99, 0x38}
raydiumLaunchLabMigrateToAmmDiscriminator = [8]byte{0xcf, 0x52, 0xc0, 0x91, 0xfe, 0xcf, 0x91, 0xdf}
raydiumLaunchLabMigrateToCpmmDiscriminator = [8]byte{0x88, 0x5c, 0xc8, 0x67, 0x1c, 0xda, 0x90, 0x8c}
)
// Program PumpAmm program ID
var budgGetProgram = solana.MustPublicKeyFromBase58("ComputeBudget111111111111111111111111111111")
@@ -73,5 +261,8 @@ var transferDiscriminator = uint32(2)
var createAccountWithSeedDiscriminator = uint32(3)
var systemProgram = solana.MustPublicKeyFromBase58("11111111111111111111111111111111")
var momoProgram = solana.MustPublicKeyFromBase58("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")
var raydiumLaunchLabProgramID = solana.MustPublicKeyFromBase58("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj")
var chainLinkProgram = solana.MustPublicKeyFromBase58("cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ")
var eventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29}

2543
metaoradlmm.go Normal file

File diff suppressed because it is too large Load Diff

468
metaoradlmm_test.go Normal file
View File

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

895
metaorapool.go Normal file
View File

@@ -0,0 +1,895 @@
package pump_parser
import (
"bytes"
"encoding/binary"
"fmt"
agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
type metaoraPoolInitializePoolData struct {
TokenAAmount uint64 `json:"tokenAAmount"`
TokenBAmount uint64 `json:"tokenBAmount"`
}
type metaoraPoolSwapArgs struct {
InAmount uint64
MinimumOutAmount uint64
}
var (
meteoraVaultProgram = solana.MustPublicKeyFromBase58("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi")
meteoraVaultDepositDiscriminator = []byte{0xf2, 0x23, 0xc6, 0x89, 0x52, 0xe1, 0xf2, 0xb6}
meteoraVaultWithdrawDiscriminator = []byte{0xb7, 0x12, 0x46, 0x9c, 0x94, 0x6d, 0xa1, 0x22}
tokenProgramMintToDiscriminator = []byte{0x07}
tokenProgramTransferDiscriminator = []byte{0x03}
tokenProgramBurnDiscriminator = []byte{0x08}
)
func metaoraPoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(metaoraPoolProgramID) {
return nil, increaseOffset(offset), fmt.Errorf("metaoraPool program instruction not found, offset, %d, %d", offset[0], offset[1])
}
decode := instruction.Data
if len(decode) < 8 {
return nil, increaseOffset(offset), fmt.Errorf("metaoraPool program instruction data too short, offset, %d, %d", offset[0], offset[1])
}
discriminator := *(*[8]byte)(decode[:8])
switch discriminator {
case metaoraPoolInitializePermissionlessConstantProductPoolWithConfigDiscriminator,
metaoraPoolInitializePermissionlessConstantProductPoolWithConfig2Discriminator:
return metaoraPoolInitializePermissionlessConstantProductPoolWithConfig(tx, instruction, innerInstructions, offset)
case metaoraPoolInitializePermissionlessPoolDiscriminator,
metaoraPoolInitializePermissionlessPoolWithFeeTierDiscriminator,
metaoraPoolInitializeCustomizablePermissionlessConstantProductPoolDiscriminator:
return metaoraPoolInitializePermissionlessPool(tx, instruction, innerInstructions, offset)
case metaoraPoolInitializePermissionedPoolDiscriminator:
return metaoraPoolInitializePermissionedPool(tx, instruction, innerInstructions, offset)
case metaoraPoolSwapDiscriminator:
return metaoraPoolSwap(tx, instruction, innerInstructions, offset)
case metaoraPoolAddImbalanceLiquidityDiscriminator,
metaoraPoolAddBalanceLiquidityDiscriminator,
metaoraPoolBootstrapLiquidityDiscriminator:
return metaoraPoolAddLiquidity(tx, instruction, innerInstructions, offset)
case metaoraPoolRemoveLiquiditySingleSideDiscriminator,
metaoraPoolRemoveBalanceLiquidityDiscriminator,
metaoraPoolClaimFeeDiscriminator:
return metaoraPoolRemoveLiquidity(tx, instruction, innerInstructions, offset)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
}
// InitializePermissionlessConstantProductPoolWithConfig
// InitializePermissionlessConstantProductPoolWithConfig2
func metaoraPoolInitializePermissionlessConstantProductPoolWithConfig(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
var data metaoraPoolInitializePoolData
err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&data)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize initialize pool data: %w", err)
}
if len(instruction.Accounts) < 20 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction")
}
tokenAMint := tx.rawTx.accountList[instruction.Accounts[3]]
tokenBMint := tx.rawTx.accountList[instruction.Accounts[4]]
baseVaultAccountIndex := instruction.Accounts[7]
quoteVaultAccountIndex := instruction.Accounts[8]
baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex)
if baseVaultTokenBalance == nil || quoteVaultTokenBalance == nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token balances")
}
baseReserve, err := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err)
}
quoteReserve, err := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err)
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var meteoraVaultProgramId int
for i, acc := range tx.rawTx.accountList {
if acc.Equals(meteoraVaultProgram) {
meteoraVaultProgramId = i
break
}
}
var baseFound, quoteFound bool
if meteoraVaultProgramId > 0 {
for innerIndex, innerInstr := range inners {
if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraVaultDepositDiscriminator[:]) {
if len(innerInstr.Accounts) < 2 {
continue
}
if !baseFound && innerInstr.Accounts[1] == baseVaultAccountIndex {
baseFound = true
}
if !quoteFound && innerInstr.Accounts[1] == quoteVaultAccountIndex {
quoteFound = true
}
if baseFound && quoteFound {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
}
break
}
}
}
}
return []Swap{
{
Program: SolProgramMeteoraPools,
Event: "create",
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
BaseMint: tokenAMint,
QuoteMint: tokenBMint,
BaseTokenProgram: baseVaultTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteVaultTokenBalance.ProgramIDAccount,
Creator: tx.rawTx.accountList[0],
BaseMintDecimals: uint8(baseVaultTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteVaultTokenBalance.UITokenAmount.Decimals),
User: tx.rawTx.accountList[instruction.Accounts[18]],
BaseAmount: decimal.NewFromUint64(data.TokenAAmount),
QuoteAmount: decimal.NewFromUint64(data.TokenBAmount),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
EntryContract: entryContract,
},
}, offset, nil
}
// InitializePermissionlessPool
// InitializePermissionlessPoolWithFeeTier
func metaoraPoolInitializePermissionlessPool(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
// discriminator + tokenA amount + tokenB amount
if len(instruction.Data) < 24 {
return nil, increaseOffset(offset), fmt.Errorf("not enough data for initialize instruction")
}
var data metaoraPoolInitializePoolData
err := agbinary.NewBorshDecoder(instruction.Data[len(instruction.Data)-16:]).Decode(&data)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize initialize pool data: %w", err)
}
if len(instruction.Accounts) < 20 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction")
}
tokenAMint := tx.rawTx.accountList[instruction.Accounts[2]]
tokenBMint := tx.rawTx.accountList[instruction.Accounts[3]]
baseVaultAccountIndex := instruction.Accounts[6]
quoteVaultAccountIndex := instruction.Accounts[7]
baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex)
if baseVaultTokenBalance == nil || quoteVaultTokenBalance == nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token balances")
}
baseReserve, err := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err)
}
quoteReserve, err := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err)
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var meteoraVaultProgramId int
for i, acc := range tx.rawTx.accountList {
if acc.Equals(meteoraVaultProgram) {
meteoraVaultProgramId = i
break
}
}
var baseFound, quoteFound bool
if meteoraVaultProgramId > 0 {
for innerIndex, innerInstr := range inners {
if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraVaultDepositDiscriminator[:]) {
if len(innerInstr.Accounts) < 2 {
continue
}
if !baseFound && innerInstr.Accounts[1] == baseVaultAccountIndex {
baseFound = true
}
if !quoteFound && innerInstr.Accounts[1] == quoteVaultAccountIndex {
quoteFound = true
}
if baseFound && quoteFound {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
}
break
}
}
}
}
return []Swap{
{
Program: SolProgramMeteoraPools,
Event: "create",
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
BaseMint: tokenAMint,
QuoteMint: tokenBMint,
BaseTokenProgram: baseVaultTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteVaultTokenBalance.ProgramIDAccount,
Creator: tx.rawTx.accountList[0],
BaseMintDecimals: uint8(baseVaultTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteVaultTokenBalance.UITokenAmount.Decimals),
User: tx.rawTx.accountList[instruction.Accounts[18]],
BaseAmount: decimal.NewFromUint64(data.TokenAAmount),
QuoteAmount: decimal.NewFromUint64(data.TokenBAmount),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
EntryContract: entryContract,
},
}, offset, nil
}
func metaoraPoolInitializePermissionedPool(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
// discriminator + tokenA amount + tokenB amount
if len(instruction.Data) < 24 {
return nil, increaseOffset(offset), fmt.Errorf("not enough data for initialize instruction")
}
var data metaoraPoolInitializePoolData
err := agbinary.NewBorshDecoder(instruction.Data[len(instruction.Data)-16:]).Decode(&data)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize initialize pool data: %w", err)
}
if len(instruction.Accounts) < 20 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction")
}
tokenAMint := tx.rawTx.accountList[instruction.Accounts[2]]
tokenBMint := tx.rawTx.accountList[instruction.Accounts[3]]
baseVaultAccountIndex := instruction.Accounts[10]
quoteVaultAccountIndex := instruction.Accounts[11]
baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex)
if baseVaultTokenBalance == nil || quoteVaultTokenBalance == nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token balances")
}
baseReserve, err := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err)
}
quoteReserve, err := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err)
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var meteoraVaultProgramId int
for i, acc := range tx.rawTx.accountList {
if acc.Equals(meteoraVaultProgram) {
meteoraVaultProgramId = i
break
}
}
var baseFound, quoteFound bool
if meteoraVaultProgramId > 0 {
for innerIndex, innerInstr := range inners {
if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraVaultDepositDiscriminator[:]) {
if len(innerInstr.Accounts) < 2 {
continue
}
if !baseFound && innerInstr.Accounts[1] == baseVaultAccountIndex {
baseFound = true
}
if !quoteFound && innerInstr.Accounts[1] == quoteVaultAccountIndex {
quoteFound = true
}
if baseFound && quoteFound {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
}
break
}
}
}
}
return []Swap{
{
Program: SolProgramMeteoraPools,
Event: "create",
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
BaseMint: tokenAMint,
QuoteMint: tokenBMint,
BaseTokenProgram: baseVaultTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteVaultTokenBalance.ProgramIDAccount,
Creator: tx.rawTx.accountList[0],
BaseMintDecimals: uint8(baseVaultTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteVaultTokenBalance.UITokenAmount.Decimals),
User: tx.rawTx.accountList[instruction.Accounts[18]],
BaseAmount: decimal.NewFromUint64(data.TokenAAmount),
QuoteAmount: decimal.NewFromUint64(data.TokenBAmount),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
EntryContract: entryContract,
},
}, offset, nil
}
// BootstrapLiquidity
// AddImbalanceLiquidity
// AddBalanceLiquidity
func metaoraPoolAddLiquidity(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 14 {
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
pool := tx.rawTx.accountList[instruction.Accounts[0]]
lpMint := tx.rawTx.accountList[instruction.Accounts[1]]
payer := tx.rawTx.accountList[instruction.Accounts[13]]
userPoolLp := tx.rawTx.accountList[instruction.Accounts[2]]
// vault for storing real tokens
// NOTE: because meteora pools will put assets of different pairs together,
// we cannot directly use the vault balance to calculate liquidity
var meteoraVaultProgramId int
for i, acc := range tx.rawTx.accountList {
if acc.Equals(meteoraVaultProgram) {
meteoraVaultProgramId = i
break
}
}
if meteoraVaultProgramId == 0 {
return nil, increaseOffset(offset), fmt.Errorf("meteora vault program not found")
}
// 7, 8
baseVaultLpAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[7])
quoteVaultLpAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[8])
if baseVaultLpAccountBalance == nil || quoteVaultLpAccountBalance == nil {
return nil, increaseOffset(offset), InstructionIgnoredError //fmt.Errorf("failed to get vault lp account balances")
}
// 9,10
baseVaultAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[9])
quoteVaultAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[10])
if baseVaultAccountBalance == nil || quoteVaultAccountBalance == nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get vault lp account balances")
}
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var baseFound, quoteFound bool
var baseAmount, quoteAmount decimal.Decimal
var (
baseMint = solana.PublicKey{}
quoteMint = solana.PublicKey{}
baseTokenProgram = solana.PublicKey{}
quoteTokenProgram = solana.PublicKey{}
baseDecimals uint8
quoteDecimals uint8
baseReserve decimal.Decimal
quoteReserve decimal.Decimal
)
baseMint = baseVaultAccountBalance.MintAccount
quoteMint = quoteVaultAccountBalance.MintAccount
quoteTokenProgram = quoteVaultAccountBalance.ProgramIDAccount
baseTokenProgram = baseVaultAccountBalance.ProgramIDAccount
baseDecimals = uint8(baseVaultAccountBalance.UITokenAmount.Decimals)
baseReserve, err = decimal.NewFromString(baseVaultLpAccountBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err)
}
if baseDecimals != uint8(baseVaultLpAccountBalance.UITokenAmount.Decimals) {
decimalDiff := int(baseDecimals) - int(baseVaultLpAccountBalance.UITokenAmount.Decimals)
baseReserve = baseReserve.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(decimalDiff))))
}
quoteDecimals = uint8(quoteVaultAccountBalance.UITokenAmount.Decimals)
quoteReserve, err = decimal.NewFromString(quoteVaultLpAccountBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err)
}
if quoteDecimals != uint8(quoteVaultLpAccountBalance.UITokenAmount.Decimals) {
decimalDiff := int(quoteDecimals) - int(quoteVaultLpAccountBalance.UITokenAmount.Decimals)
quoteReserve = quoteReserve.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(decimalDiff))))
}
for innerIndex := 0; innerIndex < len(inners); innerIndex++ {
innerInstr := inners[innerIndex]
if innerInstr.ProgramIDIndex == meteoraVaultProgramId &&
len(innerInstr.Data) >= 16 &&
bytes.Equal(innerInstr.Data[:8], eventDiscriminator[:]) &&
bytes.Equal(innerInstr.Data[8:16], meteoraVaultDepositDiscriminator[:]) {
if len(innerInstr.Accounts) < 6 {
continue
}
if innerIndex+1 >= len(inners) {
continue
}
transferInstr := inners[innerIndex+1]
_, to, amount, err := parseTokenTransfer(tx.rawTx, transferInstr)
if err != nil {
continue
}
innerIndex++ // skip transfer instruction
if !baseFound && to.Equals(tx.rawTx.accountList[baseVaultAccountBalance.AccountIndex]) {
baseFound = true
baseAmount = decimal.NewFromUint64(amount)
} else if !quoteFound && to.Equals(tx.rawTx.accountList[quoteVaultAccountBalance.AccountIndex]) {
quoteFound = true
quoteAmount = decimal.NewFromUint64(amount)
}
}
if (baseFound || quoteFound) && (tx.rawTx.accountList[innerInstr.ProgramIDIndex] == solana.TokenProgramID ||
tx.rawTx.accountList[innerInstr.ProgramIDIndex] == solana.Token2022ProgramID) && innerInstr.Data[0] == 7 {
if len(innerInstr.Accounts) < 3 {
continue
}
// mint lp token
if tx.rawTx.accountList[innerInstr.Accounts[0]] == lpMint && tx.rawTx.accountList[innerInstr.Accounts[1]] == userPoolLp {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
}
break
}
}
}
if !baseFound && !quoteFound {
return nil, increaseOffset(offset), fmt.Errorf("failed to find deposit instructions")
}
var event = "add_liquidity_one_side"
if baseFound && quoteFound {
// both sides
event = "add_liquidity"
}
swap := Swap{
Program: SolProgramMeteoraPools,
Event: event,
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: baseDecimals,
QuoteMintDecimals: quoteDecimals,
User: payer,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
EntryContract: entryContract,
}
return []Swap{swap}, offset, nil
}
// RemoveLiquiditySingleSide
// ClaimFee
// RemoveBalanceLiquidity
func metaoraPoolRemoveLiquidity(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 14 {
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
pool := tx.rawTx.accountList[instruction.Accounts[0]]
lpMint := tx.rawTx.accountList[instruction.Accounts[1]]
var (
userPoolLp solana.PublicKey
baseVaultIdx int
quoteVaultIdx int
baseLpVaultIdx int
quoteLpVaultIdx int
userIdx int
)
if bytes.Equal(instruction.Data[:8], metaoraPoolRemoveLiquiditySingleSideDiscriminator[:]) {
userPoolLp = tx.rawTx.accountList[instruction.Accounts[2]]
//userBaseAccountIdx = 11
//userQuoteAccountIdx = 12
baseVaultIdx = 9
quoteVaultIdx = 10
baseLpVaultIdx = 3
quoteLpVaultIdx = 4
userIdx = 12
} else if bytes.Equal(instruction.Data[:8], metaoraPoolClaimFeeDiscriminator[:]) {
if len(instruction.Accounts) < 16 {
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
}
userPoolLp = tx.rawTx.accountList[instruction.Accounts[5]]
//userBaseAccountIdx = 15
//userQuoteAccountIdx = 16
baseVaultIdx = 7
quoteVaultIdx = 8
baseLpVaultIdx = 11
quoteLpVaultIdx = 12
userIdx = 3
} else if bytes.Equal(instruction.Data[:8], metaoraPoolRemoveBalanceLiquidityDiscriminator[:]) {
userPoolLp = tx.rawTx.accountList[instruction.Accounts[2]]
//userBaseAccountIdx = 11
//userQuoteAccountIdx = 12
baseVaultIdx = 9
quoteVaultIdx = 10
baseLpVaultIdx = 3
quoteLpVaultIdx = 4
userIdx = 12
} else {
return nil, increaseOffset(offset), fmt.Errorf("invalid remove liquidity instruction discriminator")
}
// vault for storing real tokens
// NOTE: because meteora pools will put assets of different pairs together,
// we cannot directly use the vault balance to calculate liquidity
var meteoraVaultProgramId int
for i, acc := range tx.rawTx.accountList {
if acc.Equals(meteoraVaultProgram) {
meteoraVaultProgramId = i
break
}
}
if meteoraVaultProgramId == 0 {
return nil, increaseOffset(offset), fmt.Errorf("meteora vault program not found")
}
// 7, 8
baseVaultLpAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[baseLpVaultIdx])
quoteVaultLpAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[quoteLpVaultIdx])
if baseVaultLpAccountBalance == nil || quoteVaultLpAccountBalance == nil {
return nil, increaseOffset(offset), InstructionIgnoredError // fmt.Errorf("failed to get vault lp account balances")
}
// 9,10
baseVaultAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[baseVaultIdx])
quoteVaultAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[quoteVaultIdx])
if baseVaultAccountBalance == nil || quoteVaultAccountBalance == nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get vault lp account balances")
}
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var baseFound, quoteFound bool
var baseAmount, quoteAmount decimal.Decimal
var (
baseMint = solana.PublicKey{}
quoteMint = solana.PublicKey{}
baseTokenProgram = solana.PublicKey{}
quoteTokenProgram = solana.PublicKey{}
baseDecimals uint8
quoteDecimals uint8
baseReserve decimal.Decimal
quoteReserve decimal.Decimal
)
baseMint = baseVaultAccountBalance.MintAccount
quoteMint = quoteVaultAccountBalance.MintAccount
baseTokenProgram = baseVaultAccountBalance.ProgramIDAccount
quoteTokenProgram = quoteVaultAccountBalance.ProgramIDAccount
baseDecimals = uint8(baseVaultAccountBalance.UITokenAmount.Decimals)
baseReserve, err = decimal.NewFromString(baseVaultLpAccountBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err)
}
if baseDecimals != uint8(baseVaultLpAccountBalance.UITokenAmount.Decimals) {
decimalDiff := int(baseDecimals) - int(baseVaultLpAccountBalance.UITokenAmount.Decimals)
baseReserve = baseReserve.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(decimalDiff))))
}
quoteDecimals = uint8(quoteVaultAccountBalance.UITokenAmount.Decimals)
quoteReserve, err = decimal.NewFromString(quoteVaultLpAccountBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err)
}
if quoteDecimals != uint8(quoteVaultLpAccountBalance.UITokenAmount.Decimals) {
decimalDiff := int(quoteDecimals) - int(quoteVaultLpAccountBalance.UITokenAmount.Decimals)
quoteReserve = quoteReserve.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(decimalDiff))))
}
for innerIndex := 0; innerIndex < len(inners); innerIndex++ {
innerInstr := inners[innerIndex]
if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 8 &&
bytes.Equal(innerInstr.Data[:8], meteoraVaultWithdrawDiscriminator[:]) {
if len(innerInstr.Accounts) < 6 {
continue
}
if innerIndex+1 >= len(inners) {
continue
}
transferInstr := inners[innerIndex+1]
from, _, amount, err := parseTokenTransfer(tx.rawTx, transferInstr)
if err != nil {
fmt.Println("parse tx error:", err, tx.GetTxHash(), transferInstr)
continue
}
innerIndex++ // skip transfer instruction
if !baseFound && from.Equals(tx.rawTx.accountList[instruction.Accounts[baseVaultIdx]]) {
//base
baseFound = true
baseAmount = decimal.NewFromUint64(amount)
} else if !quoteFound && from.Equals(tx.rawTx.accountList[instruction.Accounts[quoteVaultIdx]]) {
// quote
quoteFound = true
quoteAmount = decimal.NewFromUint64(amount)
}
}
if (baseFound || quoteFound) && (tx.rawTx.accountList[innerInstr.ProgramIDIndex] == solana.TokenProgramID ||
tx.rawTx.accountList[innerInstr.ProgramIDIndex] == solana.Token2022ProgramID) && innerInstr.Data[0] == 8 {
if len(innerInstr.Accounts) < 3 {
continue
}
// mint lp token
if tx.rawTx.accountList[innerInstr.Accounts[1]] == lpMint && tx.rawTx.accountList[innerInstr.Accounts[0]] == userPoolLp {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
}
break
}
}
}
if !baseFound && !quoteFound {
return nil, increaseOffset(offset), fmt.Errorf("failed to find withdraw instructions, baseFound: %v, quoteFound: %v", baseFound, quoteFound)
}
var event = "remove_liquidity_one_side"
if baseFound && quoteFound {
event = "remove_liquidity"
}
swap := Swap{
Program: SolProgramMeteoraPools,
Event: event,
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: baseDecimals,
QuoteMintDecimals: quoteDecimals,
User: tx.rawTx.accountList[userIdx],
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
EntryContract: entryContract,
}
return []Swap{swap}, offset, nil
}
func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
var args metaoraPoolSwapArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode meteora pools swap args: %w", err)
}
pool := tx.rawTx.accountList[instruction.Accounts[0]]
payer := tx.rawTx.accountList[instruction.Accounts[12]]
sourceAccountIndex := instruction.Accounts[1]
destinationAccountIndex := instruction.Accounts[2]
// vault for storing real tokens
// NOTE: because meteora pools will put assets of different pairs together,
// we cannot directly use the vault balance to calculate liquidity
//parse reserves from vault accounts
baseVaultIdx := instruction.Accounts[6]
quoteVaultIdx := instruction.Accounts[5]
baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
if baseVaultTokenBalance == nil || quoteVaultTokenBalance == nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get vault token balances")
}
baseVaultLpBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[10])
quoteVaultLpBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[9])
if baseVaultLpBalance == nil || quoteVaultLpBalance == nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get vault lp balances")
}
baseTokenProgram := baseVaultTokenBalance.ProgramIDAccount
quoteTokenProgram := quoteVaultTokenBalance.ProgramIDAccount
baseMint := baseVaultTokenBalance.MintAccount
quoteMint := quoteVaultTokenBalance.MintAccount
baseDecimals := uint8(baseVaultTokenBalance.UITokenAmount.Decimals)
quoteDecimals := uint8(quoteVaultTokenBalance.UITokenAmount.Decimals)
baseReserve := decimal.Zero
quoteReserve := decimal.Zero
if baseVaultLpBalance.UITokenAmount.Decimals == baseVaultTokenBalance.UITokenAmount.Decimals {
baseReserve, _ = decimal.NewFromString(baseVaultLpBalance.UITokenAmount.Amount)
} else {
decimalsDiff := int32(baseVaultTokenBalance.UITokenAmount.Decimals) - int32(baseVaultLpBalance.UITokenAmount.Decimals)
multiplier := decimal.NewFromInt(10).Pow(decimal.NewFromInt32(decimalsDiff))
baseLpAmount, _ := decimal.NewFromString(baseVaultLpBalance.UITokenAmount.Amount)
baseReserve = baseLpAmount.Mul(multiplier)
}
if quoteVaultLpBalance.UITokenAmount.Decimals == quoteVaultTokenBalance.UITokenAmount.Decimals {
quoteReserve, _ = decimal.NewFromString(quoteVaultLpBalance.UITokenAmount.Amount)
} else {
decimalsDiff := int32(quoteVaultTokenBalance.UITokenAmount.Decimals) - int32(quoteVaultLpBalance.UITokenAmount.Decimals)
multiplier := decimal.NewFromInt(10).Pow(decimal.NewFromInt32(decimalsDiff))
quoteLpAmount, _ := decimal.NewFromString(quoteVaultLpBalance.UITokenAmount.Amount)
quoteReserve = quoteLpAmount.Mul(multiplier)
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var meteoraVaultProgramId int
for i, acc := range tx.rawTx.accountList {
if acc.Equals(meteoraVaultProgram) {
meteoraVaultProgramId = i
break
}
}
var baseFound, quoteFound bool
var (
baseAmount decimal.Decimal
quoteAmount decimal.Decimal
event string
)
for innerIndex := 0; innerIndex < len(inners); innerIndex++ {
innerInstr := inners[innerIndex]
//
if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 8 &&
(bytes.Equal(innerInstr.Data[:8], meteoraVaultWithdrawDiscriminator[:]) ||
bytes.Equal(innerInstr.Data[:8], meteoraVaultDepositDiscriminator[:])) {
if len(innerInstr.Accounts) < 6 {
continue
}
if innerIndex+1 >= len(inners) {
continue
}
transferInstr := inners[innerIndex+1]
if (tx.rawTx.accountList[transferInstr.ProgramIDIndex] != solana.TokenProgramID &&
tx.rawTx.accountList[transferInstr.ProgramIDIndex] != solana.Token2022ProgramID) || transferInstr.Data[0] != 3 {
continue
}
innerIndex++ // skip transfer instruction
if len(innerInstr.Accounts) == 7 &&
(bytes.Equal(innerInstr.Data[:8], meteoraVaultWithdrawDiscriminator[:]) || bytes.Equal(innerInstr.Data[:8], meteoraVaultDepositDiscriminator[:])) {
if innerInstr.Accounts[1] == baseVaultIdx {
//base
baseFound = true
baseAmount = decimal.NewFromUint64(binary.LittleEndian.Uint64(transferInstr.Data[1:9]))
if bytes.Equal(innerInstr.Data[:8], meteoraVaultWithdrawDiscriminator[:]) {
event = "buy"
} else {
event = "sell"
}
} else if innerInstr.Accounts[1] == quoteVaultIdx {
// quote
quoteFound = true
quoteAmount = decimal.NewFromUint64(binary.LittleEndian.Uint64(transferInstr.Data[1:9]))
}
}
}
if baseFound && quoteFound {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen + 1 // +1 for mint or withdraw instruction,
}
break
}
}
if !baseFound || !quoteFound {
if args.InAmount == 0 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
return nil, increaseOffset(offset), fmt.Errorf("failed to find meteora pool event in inner instructions")
}
userBase := getAccountBalanceAfterTx(tx.rawTx, sourceAccountIndex)
userQuote := getAccountBalanceAfterTx(tx.rawTx, destinationAccountIndex)
swaps := []Swap{
{
Program: SolProgramMeteoraPools,
Event: event,
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
Creator: solana.PublicKey{},
BaseMintDecimals: baseDecimals,
QuoteMintDecimals: quoteDecimals,
User: payer,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}
swaps[0].SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(args.InAmount),
decimal.NewFromUint64(args.MinimumOutAmount),
)
return swaps, offset, nil
}

396
meteora_bonding_curve.go Normal file
View File

@@ -0,0 +1,396 @@
package pump_parser
import (
"bytes"
"encoding/binary"
"fmt"
agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
type MetaoraBcEvtInitializePool struct {
Pool solana.PublicKey
Config solana.PublicKey
Creator solana.PublicKey
BaseMint solana.PublicKey
//PoolType uint8
//ActivationPoint uint64
}
type MetaoraBcSwapEvent struct {
Pool solana.PublicKey `json:"pool"`
Config solana.PublicKey `json:"config"`
TradeDirection uint8 `json:"tradeDirection"`
HasReferral bool `json:"hasReferral"`
Params *struct {
AmountIn uint64 `json:"amountIn"`
MinimumAmountOut uint64 `json:"minimumAmountOut"`
} `json:"params"`
SwapResult *struct {
ActualInputAmount uint64 `json:"actualInputAmount"`
OutputAmount uint64 `json:"outputAmount"`
NextSqrtPrice [16]byte `json:"nextSqrtPrice"`
TradingFee uint64 `json:"tradingFee"`
ProtocolFee uint64 `json:"protocolFee"`
ReferralFee uint64 `json:"referralFee"`
} `json:"swapResult"`
AmountIn uint64 `json:"amountIn"`
CurrentTimestamp uint64 `json:"currentTimestamp"`
}
func metaoraBcParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(metaoraBcProgramID) {
return nil, increaseOffset(offset), fmt.Errorf("metaora Bonding Curve program instruction not found, offset, %d, %d", offset[0], offset[1])
}
decode := instruction.Data
if len(decode) < 8 {
return nil, increaseOffset(offset), fmt.Errorf("metaora Bonding Curve program instruction data too short, offset, %d, %d", offset[0], offset[1])
}
discriminator := *(*[8]byte)(decode[:8])
switch discriminator {
case metaoraBcInitialize2022PoolDiscriminator,
metaoraBcInitializedPoolDiscriminator:
return metaBcInitializePoolParser(tx, instruction, innerInstructions, offset)
case metaoraBcMigrateMeteoraDammDiscriminator:
return metaBcMigrateParser(tx, instruction, innerInstructions, offset)
case metaoraBcMigrateMeteoraDammV2Discriminator:
return metaBcMigrateV2Parser(tx, instruction, innerInstructions, offset)
case metaoraBcSwapDiscriminator:
return metaBcSwapParser(tx, instruction, innerInstructions, offset)
case metaoraBcSwapV2Discriminator:
return metaBcSwapParser(tx, instruction, innerInstructions, offset)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
}
type MetaoraCreateData struct {
Name string
Symbol string
Uri string
}
func metaBcInitializePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 14 {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool not enough accounts, offset, %d, %d", offset[0], offset[1])
}
var createData MetaoraCreateData
err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&createData)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool parse create data error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
}
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[6])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool get base token balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[7])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool get quote token balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
baseReserve, err := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool parse base reserve error: %v, offset, %d, %d", err, offset[0], offset[1])
}
quoteReserve, err := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool parse quote reserve error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var user solana.PublicKey
if bytes.Equal(instruction.Data[:8], metaoraBcInitialize2022PoolDiscriminator[:]) {
user = tx.rawTx.accountList[instruction.Accounts[8]]
} else if bytes.Equal(instruction.Data[:8], metaoraBcInitializedPoolDiscriminator[:]) {
user = tx.rawTx.accountList[instruction.Accounts[10]]
} else {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool unknown discriminator, offset, %d, %d", offset[0], offset[1])
}
baseTokenProgram := baseTokenBalance.ProgramIDAccount
quoteTokenProgram := quoteTokenBalance.ProgramIDAccount
baseMintDecimals := uint8(baseTokenBalance.UITokenAmount.Decimals)
quoteMintDecimals := uint8(quoteTokenBalance.UITokenAmount.Decimals)
var (
pool solana.PublicKey
baseMint solana.PublicKey
creator solana.PublicKey
totalSupply *decimal.Decimal
)
for innerIndex, innerInstr := range inners {
if tx.rawTx.accountList[innerInstr.ProgramIDIndex].Equals(baseMint) &&
len(innerInstr.Data) >= 9 && innerInstr.Data[0] == 7 &&
len(innerInstr.Accounts) == 3 && tx.rawTx.accountList[innerInstr.Accounts[0]].Equals(baseMint) &&
innerInstr.Accounts[1] == instruction.Accounts[6] {
supply := decimal.NewFromUint64(binary.LittleEndian.Uint64(innerInstr.Data[1:9]))
totalSupply = &supply
}
if innerInstr.ProgramIDIndex == instruction.ProgramIDIndex &&
len(innerInstr.Data) >= 16 &&
bytes.Equal(innerInstr.Data[0:8], pumpEventDiscriminator[:]) &&
bytes.Equal(innerInstr.Data[8:16], metaoraBcEventInitializePoolDiscriminator[:]) {
var event MetaoraBcEvtInitializePool
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
}
err := agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&event)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool deserialize event error: %v, offset, %d, %d", err, offset[0], offset[1])
}
pool = event.Pool
baseMint = event.BaseMint
creator = event.Creator
break
}
}
if pool.IsZero() {
return nil, offset, fmt.Errorf("meta Bonding Curve initialize pool event not found, offset, %d, %d", offset[0], offset[1])
}
quoteMint := tx.rawTx.accountList[instruction.Accounts[4]]
tx.Token[baseMint] = TokenMeta{
Mint: baseMint,
TokenProgram: baseTokenProgram,
Decimals: baseMintDecimals,
Name: createData.Name,
Symbol: createData.Symbol,
Url: createData.Uri,
TotalSupply: totalSupply,
}
return []Swap{
{
Program: SolProgramMeteoraBondingCurve,
Event: "create",
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
Creator: creator,
BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals,
User: user,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
EntryContract: entryContract,
},
}, offset, nil
}
func metaBcMigrateV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 25 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for migrate instruction")
}
baseVaultBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[17])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve migrate get base vault balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
quoteVaultBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[18])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve migrate get quote vault balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
swaps := []Swap{
{
Program: SolProgramMeteoraBondingCurve,
Event: "migrate",
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
BaseMint: tx.rawTx.accountList[instruction.Accounts[13]],
QuoteMint: tx.rawTx.accountList[instruction.Accounts[14]],
BaseTokenProgram: tx.rawTx.accountList[instruction.Accounts[20]],
QuoteTokenProgram: tx.rawTx.accountList[instruction.Accounts[21]],
BaseMintDecimals: uint8(baseVaultBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteVaultBalance.UITokenAmount.Decimals),
User: tx.rawTx.accountList[instruction.Accounts[19]],
//BaseAmount: decimal.Decimal{},
//QuoteAmount: decimal.Decimal{},
//BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount),
//QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount),
MigrateTopProgram: meteoraDammV2Program,
MigrateToPool: tx.rawTx.accountList[instruction.Accounts[4]],
EntryContract: entryContract,
},
}
return swaps, offset, nil
}
func metaBcMigrateParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 23 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for migrate instruction")
}
baseVaultBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[17])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve migrate get base vault balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
quoteVaultBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[18])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve migrate get quote vault balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
swaps := []Swap{
{
Program: SolProgramMeteoraBondingCurve,
Event: "migrate",
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
BaseMint: tx.rawTx.accountList[instruction.Accounts[7]],
QuoteMint: tx.rawTx.accountList[instruction.Accounts[8]],
BaseTokenProgram: baseVaultBalance.ProgramIDAccount,
QuoteTokenProgram: baseVaultBalance.ProgramIDAccount,
BaseMintDecimals: uint8(baseVaultBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteVaultBalance.UITokenAmount.Decimals),
User: tx.rawTx.accountList[instruction.Accounts[22]],
//BaseAmount: decimal.Decimal{},
//QuoteAmount: decimal.Decimal{},
//BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount),
//QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount),
MigrateTopProgram: metaoraPoolProgramID,
MigrateToPool: tx.rawTx.accountList[instruction.Accounts[4]],
EntryContract: entryContract,
},
}
return swaps, offset, nil
}
func metaBcSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 15 {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve swap not enough accounts, offset, %d, %d", offset[0], offset[1])
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
userBase := getAccountBalanceAfterTx(tx.rawTx, instruction.Accounts[3])
userQuote := getAccountBalanceAfterTx(tx.rawTx, instruction.Accounts[4])
inputToken := tx.rawTx.accountList[instruction.Accounts[3]]
outputToken := tx.rawTx.accountList[instruction.Accounts[4]]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[5])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve swap get base token balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[6])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve swap get quote token balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
baseReserve, err := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve swap parse base reserve error: %v, offset, %d, %d", err, offset[0], offset[1])
}
quoteReserve, err := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve swap parse quote reserve error: %v, offset, %d, %d", err, offset[0], offset[1])
}
baseTokenProgram := baseTokenBalance.ProgramIDAccount
quoteTokenProgram := quoteTokenBalance.ProgramIDAccount
baseMintDecimals := uint8(baseTokenBalance.UITokenAmount.Decimals)
quoteMintDecimals := uint8(quoteTokenBalance.UITokenAmount.Decimals)
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var (
swapEvent MetaoraBcSwapEvent
eventLoaded bool
event string
)
for innerIndex, innerInstr := range inners {
from, to, _, err := parseTokenTransfer(tx.rawTx, innerInstr)
if err == nil {
if from.Equals(inputToken) && to.Equals(tx.rawTx.accountList[quoteTokenBalance.AccountIndex]) {
event = "buy"
} else if from.Equals(tx.rawTx.accountList[quoteTokenBalance.AccountIndex]) && to.Equals(outputToken) {
event = "sell"
}
}
if innerInstr.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstr.Data) >= 16 &&
bytes.Equal(innerInstr.Data[0:8], pumpEventDiscriminator[:]) &&
bytes.Equal(innerInstr.Data[8:16], metaoraBcEventSwapDiscriminator[:]) {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
}
err := agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&swapEvent)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve pool swap event deserialize event error: %v, offset, %d, %d", err, offset[0], offset[1])
}
eventLoaded = true
break
}
}
if !eventLoaded {
return nil, offset, fmt.Errorf("meta Bonding Curve swap event not found, offset, %d, %d", offset[0], offset[1])
}
baseMint := tx.rawTx.accountList[instruction.Accounts[7]]
quoteMint := tx.rawTx.accountList[instruction.Accounts[8]]
user := tx.rawTx.accountList[instruction.Accounts[9]]
pool := tx.rawTx.accountList[instruction.Accounts[2]]
var (
baseMintAmount decimal.Decimal
quoteMintAmount decimal.Decimal
)
if swapEvent.TradeDirection == 0 {
// A -> B
if event == "sell" {
baseMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.ActualInputAmount)
quoteMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.OutputAmount)
} else {
baseMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.OutputAmount)
quoteMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.ActualInputAmount)
}
} else {
// B -> A
if event == "buy" {
baseMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.OutputAmount)
quoteMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.ActualInputAmount)
} else {
baseMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.ActualInputAmount)
quoteMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.OutputAmount)
}
}
swaps := []Swap{
{
Program: SolProgramMeteoraBondingCurve,
Event: event,
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
Creator: solana.PublicKey{},
BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals,
User: user,
BaseAmount: baseMintAmount,
QuoteAmount: quoteMintAmount,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}
if swapEvent.Params != nil {
swaps[0].SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(swapEvent.Params.AmountIn),
decimal.NewFromUint64(swapEvent.Params.MinimumAmountOut),
)
}
return swaps, offset, nil
}

518
meteoradamm.go Normal file
View File

@@ -0,0 +1,518 @@
package pump_parser
import (
"bytes"
"fmt"
agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func metaoraDammParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(meteoraDammV2Program) {
return nil, increaseOffset(offset), fmt.Errorf("metaora damm program instruction not found, offset, %d, %d", offset[0], offset[1])
}
decode := instruction.Data
if len(decode) < 8 {
return nil, increaseOffset(offset), fmt.Errorf("metaora damm program instruction data too short, offset, %d, %d", offset[0], offset[1])
}
discriminator := *(*[8]byte)(decode[:8])
switch discriminator {
case meteoraDammV2InitializeCustomizablePoolDiscriminator,
meteoraDammV2InitializePoolWithDynamicConfig,
meteoraDammV2InitializePoolDiscriminator:
return meteoraDammV2InitializePoolParser(tx, instruction, innerInstructions, offset)
case meteoraDammV2SwapDiscriminator, meteoraDammV2SwapV2Discriminator:
return meteoraDammV2Swap(tx, instruction, innerInstructions, offset)
case meteoraDammV2AddLiquidityDiscriminator:
return meteoraDammV2AddLiquidityParser(tx, instruction, innerInstructions, offset)
case meteoraDammV2RemoveLiquidityDiscriminator, meteoraDammV2RemoveAllLiquidityDiscriminator:
return meteoraDammV2RemoveLiquidityParser(tx, instruction, innerInstructions, offset)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
}
var (
metaoraDammInitializePoolDiscriminator = []byte{228, 69, 165, 46, 81, 203, 154, 29, 228, 50, 246, 85, 203, 66, 134, 37}
meteoraDammSwapDiscriminator = []byte{228, 69, 165, 46, 81, 203, 154, 29, 189, 66, 51, 168, 38, 80, 117, 153}
// EvtLiquidityChange
meteoraDammAddLiquidityDiscriminator = []byte{228, 69, 165, 46, 81, 203, 154, 29, 197, 171, 78, 127, 224, 211, 87, 13}
meteoraDammRemoveLiquidityDiscriminator = []byte{228, 69, 165, 46, 81, 203, 154, 29, 197, 171, 78, 127, 224, 211, 87, 13}
)
type MetaoraDammDynamicFeeParameters struct {
BinStep uint16
BinStepU128 [16]byte
FilterPeriod uint16
DecayPeriod uint16
ReductionFactor uint16
MaxVolatilityAccumulator uint32
VariableFeeControl uint32
}
type MetaoraDammInitializePoolEvent struct {
Pool solana.PublicKey `json:"pool"`
TokenAMint solana.PublicKey `json:"tokenAMint"`
TokenBMint solana.PublicKey `json:"tokenBMint"`
Creator solana.PublicKey `json:"creator"`
Payer solana.PublicKey `json:"payer"`
AlphaVault solana.PublicKey `json:"alphaVault"`
//PoolFees *struct {
// BaseFee [30]byte
// DynamicFee *MetaoraDammDynamicFeeParameters `json:"dynamicFee"`
//} `json:"poolFees"`
//SqrtMinPrice [16]byte `json:"sqrtMinPrice"`
//SqrtMaxPrice [16]byte `json:"sqrtMaxPrice"`
//ActivationType uint8 `json:"activationType"`
//CollectFeeMode uint8 `json:"collectFeeMode"`
//Liquidity [16]byte `json:"liquidity"`
//SqrtPrice [16]byte `json:"sqrtPrice"`
//ActivationPoint uint64 `json:"activationPoint"`
//TokenAFlag uint8 `json:"tokenAFlag"`
//TokenBFlag uint8 `json:"tokenBFlag"`
//TokenAAmount uint64 `json:"tokenAAmount"`
//TokenBAmount uint64 `json:"tokenBAmount"`
//TotalAmountA uint64 `json:"totalAmountA"`
//TotalAmountB uint64 `json:"totalAmountB"`
//PoolType uint8 `json:"poolType"`
}
func meteoraDammV2InitializePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 12 {
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta damm initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var loadedEvent bool
var initializePoolEvent MetaoraDammInitializePoolEvent
for i, innerInstruction := range inners {
if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && bytes.Equal(innerInstruction.Data[:16], metaoraDammInitializePoolDiscriminator) {
err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&initializePoolEvent)
if err != nil {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(i) + 1 + prefixLen
}
return nil, offset, fmt.Errorf("failed to deserialize initialize pool event: %w", err)
}
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(i) + 1 + prefixLen
}
loadedEvent = true
break
}
}
if !loadedEvent {
return nil, increaseOffset(offset), fmt.Errorf("failed to get initialize pool event")
}
baseVaultAccountIndex := instruction.Accounts[10]
quoteVaultAccountIndex := instruction.Accounts[11]
if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializePoolWithDynamicConfig[:]) {
baseVaultAccountIndex = instruction.Accounts[11]
quoteVaultAccountIndex = instruction.Accounts[12]
} else if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializeCustomizablePoolDiscriminator[:]) {
baseVaultAccountIndex = instruction.Accounts[9]
quoteVaultAccountIndex = instruction.Accounts[10]
}
baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("metaora damm get base vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("metaora damm get quote vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
baseReserve, _ := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount)
swap := Swap{
Program: SolProgramMeteoraAmmV2,
Event: "create",
Pool: initializePoolEvent.Pool,
BaseMint: initializePoolEvent.TokenAMint,
QuoteMint: initializePoolEvent.TokenBMint,
BaseTokenProgram: baseVaultTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteVaultTokenBalance.ProgramIDAccount,
Creator: initializePoolEvent.Creator,
BaseMintDecimals: uint8(baseVaultTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteVaultTokenBalance.UITokenAmount.Decimals),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
User: tx.rawTx.accountList[instruction.Accounts[0]],
LpMint: tx.rawTx.accountList[instruction.Accounts[1]],
EntryContract: entryContract,
}
return []Swap{swap}, offset, nil
}
type meteoraDammSwapEvent struct {
Pool solana.PublicKey
TradeDirection uint8
CollectFeeMode uint8
HasReferral bool
Params *struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}
SwapResult *struct {
IncludedFeeInputAmount uint64
ExcludedFeeInputAmount uint64
AmountLeft uint64
OutputAmount uint64
NextSqrtPrice [16]byte
TradingFee uint64
ProtocolFee uint64
PartnerFee uint64
ReferralFee uint64
}
IncludedTransferFeeAmountIn uint64
IncludedTransferFeeAmountOut uint64
ExcludedTransferFeeAmountOut uint64
CurrentTimestamp uint64
ReserveAAmount uint64
ReserveBAmount uint64
}
func meteoraDammSwapAmountInfo(event string, params *struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
_ = event
if params == nil {
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
}
// Meteora DAMM v2 IDL defines:
// - swap: SwapParameters{ amountIn, minimumAmountOut }
// - swap2: SwapParameters2{ amount0, amount1, swapMode }
// - ExactIn / PartialFill: amount0=amount_in, amount1=minimum_amount_out
// - ExactOut: amount0=amount_out, amount1=maximum_amount_in
//
// `SetSwapAmountInfo` derives sides from the normalized buy/sell event, so
// the instruction parameters should stay in raw IDL order here.
switch params.SwapMode {
case 0, 1: // ExactIn / PartialFill
swapMode = SwapModeExactIn
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
case 2: // ExactOut
swapMode = SwapModeExactOut
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
default:
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
}
}
func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 9 {
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
}
sourceAccountIndex := instruction.Accounts[2]
destinationAccountIndex := instruction.Accounts[3]
baseVaultAccountIndex := instruction.Accounts[4]
quoteVaultAccountIndex := instruction.Accounts[5]
tokenAMint := tx.rawTx.accountList[instruction.Accounts[6]]
tokenBMint := tx.rawTx.accountList[instruction.Accounts[7]]
payer := tx.rawTx.accountList[instruction.Accounts[8]]
baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("metaora damm get base vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("metaora damm get quote vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
baseReserve, _ := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount)
baseMint := tokenAMint
quoteMint := tokenBMint
baseTokenProgram := baseVaultTokenBalance.ProgramIDAccount
quoteTokenProgram := quoteVaultTokenBalance.ProgramIDAccount
baseMintDecimals := uint8(baseVaultTokenBalance.UITokenAmount.Decimals)
quoteMintDecimals := uint8(quoteVaultTokenBalance.UITokenAmount.Decimals)
userInputTokenBalance := getAccountBalanceAfterTx(tx.rawTx, sourceAccountIndex)
userOutputTokenBalance := getAccountBalanceAfterTx(tx.rawTx, destinationAccountIndex)
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var loadedEvent bool
var swapEvent meteoraDammSwapEvent
for i, innerInstruction := range inners {
if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && bytes.Equal(innerInstruction.Data[:16], meteoraDammSwapDiscriminator) {
err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&swapEvent)
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(i) + 1 + prefixLen
}
if err != nil {
return nil, offset, fmt.Errorf("failed to deserialize swap event: %w", err)
}
loadedEvent = true
break
}
}
if !loadedEvent {
return nil, increaseOffset(offset), fmt.Errorf("failed to get swap event")
}
var baseAmount decimal.Decimal
var quoteAmount decimal.Decimal
var userBase decimal.Decimal
var userQuote decimal.Decimal
event := "buy"
if swapEvent.TradeDirection == 0 {
// A -> B
// sell base/A; buy quote/B
event = "sell"
userBase = userInputTokenBalance
userQuote = userOutputTokenBalance
baseAmount = decimal.NewFromUint64(swapEvent.SwapResult.IncludedFeeInputAmount)
quoteAmount = decimal.NewFromUint64(swapEvent.ExcludedTransferFeeAmountOut)
} else if swapEvent.TradeDirection == 1 {
// B -> A
// sell quote/B; buy base/A
userBase = userOutputTokenBalance
userQuote = userInputTokenBalance
baseAmount = decimal.NewFromUint64(swapEvent.ExcludedTransferFeeAmountOut)
quoteAmount = decimal.NewFromUint64(swapEvent.SwapResult.IncludedFeeInputAmount)
} else {
return nil, offset, fmt.Errorf("invalid trade direction")
}
swap := Swap{
Program: SolProgramMeteoraAmmV2,
Event: event,
Pool: swapEvent.Pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
Creator: solana.PublicKey{},
BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals,
User: payer,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
}
if swapMode, fixedAmount, limitAmount, ok := meteoraDammSwapAmountInfo(event, swapEvent.Params); ok {
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
}
return []Swap{swap}, offset, nil
}
type MeteoraDammV2LiquidityData struct {
LiquidityDelta [16]byte `json:"liquidityDelta"`
TokenAAmounthreshold uint64 `json:"tokenAAmounthreshold"`
TokenBAmounthreshold uint64 `json:"tokenBAmounthreshold"`
}
type MeteoraDammV2AddLiquidityEvent struct {
Pool solana.PublicKey `json:"pool"`
Position solana.PublicKey `json:"position"`
Owner solana.PublicKey `json:"owner"`
Params *MeteoraDammV2LiquidityData `json:"params"`
TokenAAmount uint64 `json:"tokenAAmount"`
TokenBAmount uint64 `json:"tokenBAmount"`
TotalAmountA uint64 `json:"totalAmountA"`
TotalAmountB uint64 `json:"totalAmountB"`
}
type MeteoraDammV2RemoveLiquidityEvent struct {
Pool solana.PublicKey `json:"pool"`
Position solana.PublicKey `json:"position"`
Owner solana.PublicKey `json:"owner"`
Params *MeteoraDammV2LiquidityData `json:"params"`
TokenAAmount uint64 `json:"tokenAAmount"`
TokenBAmount uint64 `json:"tokenBAmount"`
}
func meteoraDammV2AddLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 8 {
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
}
tokenAMint := tx.rawTx.accountList[instruction.Accounts[6]]
tokenBMint := tx.rawTx.accountList[instruction.Accounts[7]]
baseVaultAccountIndex := instruction.Accounts[4]
quoteVaultAccountIndex := instruction.Accounts[5]
baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("metaora damm get base vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("metaora damm get quote vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
baseReserve, _ := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount)
baseTokenProgram := baseVaultTokenBalance.ProgramIDAccount
quoteTokenProgram := quoteVaultTokenBalance.ProgramIDAccount
baseMintDecimals := uint8(baseVaultTokenBalance.UITokenAmount.Decimals)
quoteMintDecimals := uint8(quoteVaultTokenBalance.UITokenAmount.Decimals)
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var loadedEvent bool
var liquidityEvent MeteoraDammV2AddLiquidityEvent
for i, innerInstruction := range inners {
if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && bytes.Equal(innerInstruction.Data[:16], meteoraDammAddLiquidityDiscriminator[:]) {
err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&liquidityEvent)
if err != nil {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(i) + 1 + prefixLen
}
return nil, offset, fmt.Errorf("failed to deserialize add liquidity event: %w", err)
}
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(i) + 1 + prefixLen
}
loadedEvent = true
break
}
}
if !loadedEvent {
return nil, increaseOffset(offset), fmt.Errorf("failed to get add liquidity event")
}
swap := Swap{
Program: SolProgramMeteoraDLMM,
Event: "add_liquidity",
Pool: liquidityEvent.Pool,
BaseMint: tokenAMint,
QuoteMint: tokenBMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals,
User: liquidityEvent.Owner,
BaseAmount: decimal.NewFromUint64(liquidityEvent.TokenAAmount),
QuoteAmount: decimal.NewFromUint64(liquidityEvent.TokenBAmount),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
EntryContract: entryContract,
}
return []Swap{swap}, offset, nil
}
func meteoraDammV2RemoveLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 8 {
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
}
tokenAMint := tx.rawTx.accountList[instruction.Accounts[7]]
tokenBMint := tx.rawTx.accountList[instruction.Accounts[8]]
baseVaultAccountIndex := instruction.Accounts[5]
quoteVaultAccountIndex := instruction.Accounts[6]
baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("metaora damm get base vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("metaora damm get quote vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
baseReserve, _ := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount)
baseTokenProgram := baseVaultTokenBalance.ProgramIDAccount
quoteTokenProgram := quoteVaultTokenBalance.ProgramIDAccount
baseMintDecimals := uint8(baseVaultTokenBalance.UITokenAmount.Decimals)
quoteMintDecimals := uint8(quoteVaultTokenBalance.UITokenAmount.Decimals)
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta damm get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var loadedEvent bool
var liquidityEvent MeteoraDammV2RemoveLiquidityEvent
for i, innerInstruction := range inners {
if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && bytes.Equal(innerInstruction.Data[:16], meteoraDammRemoveLiquidityDiscriminator[:]) {
err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&liquidityEvent)
if err != nil {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(i) + 1 + prefixLen
}
return nil, offset, fmt.Errorf("failed to deserialize remove liquidity event: %w", err)
}
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(i) + 1 + prefixLen
}
loadedEvent = true
break
}
}
if !loadedEvent {
return nil, increaseOffset(offset), fmt.Errorf("failed to get remove liquidity event")
}
swap := Swap{
Program: SolProgramMeteoraDLMM,
Event: "remove_liquidity",
Pool: liquidityEvent.Pool,
BaseMint: tokenAMint,
QuoteMint: tokenBMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals,
User: liquidityEvent.Owner,
BaseAmount: decimal.NewFromUint64(liquidityEvent.TokenAAmount),
QuoteAmount: decimal.NewFromUint64(liquidityEvent.TokenBAmount),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
EntryContract: entryContract,
}
return []Swap{swap}, offset, nil
}

1365
orcawhirpool.go Normal file

File diff suppressed because it is too large Load Diff

23
orcawhirpool_test.go Normal file
View File

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

203
parser.go
View File

@@ -2,20 +2,67 @@ package pump_parser
import (
"errors"
"log"
"slices"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
var swapPrograms = map[solana.PublicKey]swapParser{
var defaultSwapPrograms = map[solana.PublicKey]swapParser{
pumpAmmProgram: pumpAmmParser,
pumpProgram: pumpParser,
}
var errTxParserPrograms = map[solana.PublicKey]swapParser{
pumpAmmProgram: pumpAmmParser,
pumpProgram: pumpParser,
}
var swapPrograms = cloneSwapPrograms(defaultSwapPrograms)
type ParserOption func(*parserConfig)
type parserConfig struct {
enableMeteoraDlmm bool
}
func EnableAllParsers() {
programs := cloneSwapPrograms(defaultSwapPrograms)
programs[meteoraDlmmProgram] = metaoradlmmParser
programs[metaoraPoolProgramID] = metaoraPoolParser
programs[metaoraBcProgramID] = metaoraBcParser
programs[meteoraDammV2Program] = metaoraDammParser
programs[orcaProgramID] = orcaWhirPoolParser
programs[raydiumV4Program] = raydiumV4Parser
programs[raydiumClmmProgramID] = raydiumClmmParser
programs[raydiumCPmmProgramID] = raydiumCPmmParser
programs[raydiumLaunchLabProgramID] = raydiumLaunchLabParser
swapPrograms = programs
}
func InitParser(opts ...ParserOption) {
cfg := parserConfig{}
for _, opt := range opts {
opt(&cfg)
}
programs := cloneSwapPrograms(defaultSwapPrograms)
if cfg.enableMeteoraDlmm {
programs[meteoraDlmmProgram] = metaoradlmmParser
}
swapPrograms = programs
}
func WithMeteoraDlmm() ParserOption {
return func(cfg *parserConfig) {
cfg.enableMeteoraDlmm = true
}
}
var actionPrograms = map[solana.PublicKey]actionParser{
systemProgram: systemParser,
budgGetProgram: budgetParser,
chainLinkProgram: chainLinkParser,
}
func ParseRawTx(rawTx *RawTx) (*Tx, error) {
@@ -34,6 +81,13 @@ func (tx *Tx) Parser() error {
return errors.New("rawTx is nil")
}
accountList := tx.rawTx.getAccountList()
if tx.rawTx.Meta.Err == nil {
for _, acc := range tx.rawTx.Transaction.Message.Instructions {
if accountList[acc.ProgramIDIndex] == solana.VoteProgramID {
tx.Vote = true
}
}
}
tx.TxHash = (*[64]byte)((tx.rawTx.Transaction.Signatures[0][:]))
tx.Signer = tx.rawTx.GetSigner()
@@ -42,16 +96,65 @@ func (tx *Tx) Parser() error {
tx.BlockAt = tx.rawTx.BlockTime
tx.CuFee = decimal.NewFromUint64(tx.rawTx.Meta.Fee)
tx.ComputeUnitsConsumed = tx.rawTx.Meta.ComputeUnitsConsumed
tx.BeforeSolBalance = decimal.NewFromUint64(tx.rawTx.Meta.PreBalances[0]).Div(decimal.NewFromInt(1e9))
tx.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[0]).Div(decimal.NewFromInt(1e9))
tx.Token = make(map[solana.PublicKey]TokenMeta)
if tx.rawTx.Meta.Err != nil {
tx.Err = tx.rawTx.Meta.Err
if tx.Err.UnKnown != "" {
return nil
}
if len(tx.rawTx.Transaction.Message.Instructions) <= int(tx.Err.Index) {
return nil
}
programIdx := tx.rawTx.Transaction.Message.Instructions[tx.Err.Index].ProgramIDIndex
if len(accountList) <= programIdx {
return nil
}
programAccount := accountList[programIdx]
parserFunc, exists := errTxParserPrograms[programAccount]
if !exists {
return nil
}
// parse failed tx
swaps, _, err := parserFunc(tx, tx.rawTx.Transaction.Message.Instructions[tx.Err.Index], InnerInstructions{}, [2]uint{uint(tx.Err.Index), uint(0)})
if err != nil {
return nil
//fmt.Printf("parser failed tx error: %s, block: %d tx: %s\n", err, tx.Block, tx.GetTxHash())
}
if len(swaps) > 0 {
for i := range swaps {
swaps[i].InstrIdx = tx.Err.Index
}
tx.Swaps = swaps
}
for i, instr := range tx.rawTx.Transaction.Message.Instructions {
if p, exists := actionPrograms[accountList[instr.ProgramIDIndex]]; exists {
_, err := p(tx, instr, InnerInstructions{}, [2]uint{uint(i), uint(0)})
if err != nil {
if errors.Is(err, InstructionIgnoredError) {
continue
}
continue
}
}
}
return nil
}
var innersMap = make(map[int]InnerInstructions)
for _, inner := range tx.rawTx.Meta.InnerInstructions {
innersMap[inner.Index] = inner
}
txIndex := 0
for i, instr := range tx.rawTx.Transaction.Message.Instructions {
txIndex += 1
if i > 0 {
txIndex += len(innersMap[i-1].Instructions)
}
programAccount := accountList[instr.ProgramIDIndex]
if p, exists := swapPrograms[programAccount]; exists {
swaps, _, err := p(tx, instr, innersMap[i], [2]uint{uint(i), uint(0)})
@@ -61,7 +164,21 @@ func (tx *Tx) Parser() error {
}
return err
}
tx.Swaps = append(tx.Swaps, swaps...)
for k, swap := range swaps {
swap.TxIndex = txIndex + k
if !swap.User.IsOnCurve() {
swap.AfterSOLBalance = tx.AfterSOLBalance
swap.User = tx.rawTx.accountList[0]
} else {
userIdx := slices.Index(tx.rawTx.accountList, swap.User)
if userIdx >= 0 {
swap.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[userIdx]).Div(decimal.NewFromInt(1e9))
}
}
swap.InstrIdx = uint8(i)
tx.Swaps = append(tx.Swaps, swap)
}
} else if p, exists := actionPrograms[programAccount]; exists {
_, err := p(tx, instr, innersMap[i], [2]uint{uint(i), uint(0)})
if err != nil {
@@ -76,7 +193,7 @@ func (tx *Tx) Parser() error {
innerLength := len(innersMap[i].Instructions)
for j := 1; j <= innerLength; {
if j <= 0 || j > innerLength {
log.Printf("inner instruction index is out if range, block: %d, tx: %s, outerIndex: %d, innerIndex: %d", tx.Block, tx.GetTxHash(), ii, j)
//log.Printf("inner instruction index is out if range, block: %d, tx: %s, outerIndex: %d, innerIndex: %d", tx.Block, tx.GetTxHash(), ii, j)
break
}
innerInstr := innersMap[i].Instructions[j-1]
@@ -91,9 +208,33 @@ func (tx *Tx) Parser() error {
}
return err
}
tx.Swaps = append(tx.Swaps, swaps...)
for k, swap := range swaps {
swap.TxIndex = txIndex + k + j
// identify okxDexRoutersV2 and okxAggregatorV2 is user
//if !swap.User.IsOnCurve() && (swap.EntryContract.Equals(okxDexRoutersV2) || swap.EntryContract.Equals(okxAggregatorV2)) {
// swap.User = tx.rawTx.accountList[0]
//}
if !swap.User.IsOnCurve() {
swap.AfterSOLBalance = tx.AfterSOLBalance
swap.User = tx.rawTx.accountList[0]
} else {
userIdx := slices.Index(tx.rawTx.accountList, swap.User)
if userIdx >= 0 {
swap.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[userIdx]).Div(decimal.NewFromInt(1e9))
}
}
swap.InstrIdx = uint8(i)
swap.InnerIdx = uint8(j)
tx.Swaps = append(tx.Swaps, swap)
}
// tx.Swaps = append(tx.Swaps, swaps...)
if ii == int(offset[0]) && j == int(offset[1]) {
j = j + 1
} else {
j = int(offset[1])
ii = int(offset[0])
}
} else if p, exists := actionPrograms[innerProgramAccount]; exists {
offset, err := p(tx, innerInstr, innersMap[i], [2]uint{uint(i), uint(j)})
if err != nil {
@@ -114,6 +255,58 @@ func (tx *Tx) Parser() error {
}
}
}
// update swaps same program+pair with last reserve balance
if len(tx.Swaps) > 1 {
pairKey := func(s Swap) solana.PublicKey {
// Match pair selection used by downstream consumers.
if s.Program == SolProgramPump {
return s.BaseMint
}
return s.Pool
}
lastReserve := make(map[solana.PublicKey]reserveSnapshot, len(tx.Swaps))
for _, swap := range tx.Swaps {
lastReserve[pairKey(swap)] = reserveSnapshot{
baseMint: swap.BaseMint,
quoteMint: swap.QuoteMint,
baseReserve: swap.BaseReserve,
quoteReserve: swap.QuoteReserve,
}
}
for i := range tx.Swaps {
key := pairKey(tx.Swaps[i])
if v, ok := lastReserve[key]; ok {
if tx.Swaps[i].BaseMint == v.baseMint && tx.Swaps[i].QuoteMint == v.quoteMint {
tx.Swaps[i].BaseReserve = v.baseReserve
tx.Swaps[i].QuoteReserve = v.quoteReserve
} else if tx.Swaps[i].BaseMint == v.quoteMint && tx.Swaps[i].QuoteMint == v.baseMint {
tx.Swaps[i].BaseReserve = v.quoteReserve
tx.Swaps[i].QuoteReserve = v.baseReserve
}
//else {
// tx.Swaps[i].BaseReserve = v.baseReserve
// tx.Swaps[i].QuoteReserve = v.quoteReserve
//}
}
}
}
return nil
}
type reserveSnapshot struct {
baseMint solana.PublicKey
quoteMint solana.PublicKey
baseReserve decimal.Decimal
quoteReserve decimal.Decimal
}
func cloneSwapPrograms(src map[solana.PublicKey]swapParser) map[solana.PublicKey]swapParser {
dst := make(map[solana.PublicKey]swapParser, len(src))
for k, v := range src {
dst[k] = v
}
return dst
}

212
pump.go
View File

@@ -27,7 +27,6 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct
decode := instruction.Data
if len(decode) < 8 {
offset[1] += 1
return nil, increaseOffset(offset), fmt.Errorf("pump program instruction data too short, offset, %d, %d", offset[0], offset[1])
}
@@ -35,10 +34,19 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct
switch discriminator {
case pumpBuyV2Discriminator, pumpBuyDiscriminator, pumpSellDiscriminator:
if tx.Err != nil {
return failedTxBuyOrSellParser(tx, instruction, innerInstructions, offset)
}
return BuyOrSellParser(tx, instruction, innerInstructions, offset)
case pumpCreateDiscriminator, pumpCreateV2Discriminator:
if tx.Err != nil {
return nil, increaseOffset(offset), InstructionIgnoredError
}
return CreateParser(tx, instruction, innerInstructions, offset)
case pumpMigrateDiscriminator:
if tx.Err != nil {
return nil, increaseOffset(offset), InstructionIgnoredError
}
return MigrateParser(tx, instruction, innerInstructions, offset)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
@@ -80,6 +88,7 @@ type PumpCreateEvent struct {
TokenTotalSupply uint64
TokenProgram solana.PublicKey
IsMayhemMode bool
IsCashbackEnabled bool
}
func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -101,7 +110,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] += uint(innerIndex) + 1 + prefixLen
offset[1] = uint(innerIndex) + 1 + prefixLen
}
if err != nil {
return nil, offset, fmt.Errorf("pump create event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
@@ -149,6 +158,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
BaseReserve: decimal.NewFromUint64(createEvent.RealTokenReserves),
QuoteReserve: decimal.Zero,
Mayhem: createEvent.IsMayhemMode,
Cashback: createEvent.IsCashbackEnabled,
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
EntryContract: entryContract,
@@ -177,6 +187,16 @@ type PumpTradeEvent struct {
CreatorFeeBasisPoints uint64
CreatorFee uint64
TrackVolume bool
TotalUnclaimedTokens uint64
TotalClaimedTokens uint64
CurrentSolVolume uint64
LastUpdateTimestamp int64
IxName string
MayhemMode bool
CashbackFeeBasisPoints uint64
Cashback uint64
}
type PumpTradeFeeArg struct {
@@ -192,6 +212,154 @@ type CompleteEvent struct {
Timestamp int64
}
type PumpTradeArgs struct {
Discriminator [8]byte
Amount1 uint64
Amount2 uint64
}
func pumpTradeAmountInfoFromArgs(args PumpTradeArgs) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
switch {
case bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]):
return SwapModeExactOut, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
default:
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
}
}
func pumpCompleteMatchesTradeEvent(completeEvent CompleteEvent, tradeEvent PumpTradeEvent, bondingCurve solana.PublicKey) bool {
if completeEvent.Mint != tradeEvent.Mint {
return false
}
if completeEvent.User != tradeEvent.User {
return false
}
if completeEvent.BondingCurve != bondingCurve {
return false
}
return true
}
func normalizePumpQuoteSideMint(s *Swap) {
if s.FixedAmountSide == SwapAmountSideQuote && s.FixedMint.IsZero() {
s.FixedMint = wSolMint
}
if s.LimitAmountSide == SwapAmountSideQuote && s.LimitMint.IsZero() {
s.LimitMint = wSolMint
}
if s.ActualLimitAmountSide == SwapAmountSideQuote && s.LimitMint.IsZero() {
s.LimitMint = wSolMint
}
}
func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if tx.Err == nil || tx.Err.UnKnown != "" {
return nil, increaseOffset(offset), fmt.Errorf("tx pump failed but error is nil, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Variant != InstructionError {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but error variant is not instruction error, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Enum != Custom && tx.Err.Enum != ComputationalBudgetExceeded && tx.Err.Enum != ProgramFailedToComplete {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but error is not custom or computational budget exceeded, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Enum == Custom {
if !(tx.Err.CustomCode == 1 ||
tx.Err.CustomCode == 6042 ||
tx.Err.CustomCode == 6041 ||
tx.Err.CustomCode == 6040 ||
tx.Err.CustomCode == 6023 || tx.Err.CustomCode == 6021 || tx.Err.CustomCode == 6003 || tx.Err.CustomCode == 6002) {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but custom error code is unexpected, offset, %d, %d, code: %d", offset[0], offset[1], tx.Err.CustomCode)
}
}
result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
user := result.accountList[instruction.Accounts[6]]
ataUserIdx := instruction.Accounts[5]
userIndex := instruction.Accounts[6]
mint := result.accountList[instruction.Accounts[2]]
var args PumpTradeArgs
err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump buy/sell decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var event string
var (
solAmount, tokenAmount uint64
)
if bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) {
event = "buy_failed"
solAmount = args.Amount1
tokenAmount = args.Amount2
} else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) {
event = "buy_failed"
solAmount = args.Amount2
tokenAmount = args.Amount1
} else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) {
event = "sell_failed"
solAmount = args.Amount2
tokenAmount = args.Amount1
} else {
return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction discriminator, offset, %d, %d", offset[0], offset[1])
}
var baseTokenProgram solana.PublicKey
if event == "buy_failed" {
baseTokenProgram = result.accountList[instruction.Accounts[8]]
} else {
baseTokenProgram = result.accountList[instruction.Accounts[9]]
}
if !user.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, mint)
//&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount
if !userBaseAmount.IsZero() {
user = result.accountList[0]
userIndex = 0
ataUserIdx = ataIndex
}
}
userBase := getAccountBalanceAfterTx(result, ataUserIdx)
userQuote, _ := GetSolAfterTx(result, userIndex)
bcIdx := instruction.Accounts[3]
bcAtaIndex := instruction.Accounts[4]
solReserves, _ := GetSolAfterTx(result, bcIdx)
tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex)
swaps := []Swap{
{
Program: SolProgramPump,
Event: event,
Pool: result.accountList[instruction.Accounts[3]],
BaseMint: mint,
QuoteMint: solana.PublicKey{},
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: solana.PublicKey{},
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
User: user,
BaseAmount: decimal.NewFromUint64(tokenAmount),
QuoteAmount: decimal.NewFromUint64(solAmount),
BaseReserve: tokenReserves,
QuoteReserve: decimal.NewFromUint64(solReserves),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
EntryContract: entryContract,
},
}
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
normalizePumpQuoteSideMint(&swaps[0])
}
return swaps, offset, nil
}
func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
@@ -211,6 +379,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
completeEvent CompleteEvent
completed bool
newoffset [2]uint
tradeFound bool
)
var prefixLen = offset[1]
@@ -218,10 +387,13 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("pump create get inner instructions error: %v,offset, %d, %d", err, offset[0], offset[1])
}
if !entryContract.Equals(axiomOuterContract) {
if instruction.StackHeight != nil && *instruction.StackHeight > 2 {
for _, innerInstr := range innerInstructions.Instructions {
if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 {
entryContract = result.accountList[innerInstr.ProgramIDIndex]
break
}
}
}
}
@@ -236,6 +408,9 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
}
if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) {
if bytes.Equal(innerInstr.Data[8:16], pumpTradeEventDiscriminator[8:16]) {
if tradeFound {
break
}
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&tradeEvent)
if offset[1] == 0 {
newoffset = [2]uint{offset[0] + 1, offset[1]}
@@ -245,19 +420,31 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
if err != nil {
return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
expectedIsBuy := !bytes.Equal(instruction.Data[:8], pumpSellDiscriminator[:])
if tradeEvent.IsBuy != expectedIsBuy {
tradeEvent = PumpTradeEvent{}
continue
}
tradeFound = true
if !tradeEvent.IsBuy {
break
}
} else if bytes.Equal(innerInstr.Data[8:16], pumpCompleteEventDiscriminator[:]) {
if !tradeFound {
continue
}
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&completeEvent)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, result.accountList[instruction.Accounts[3]]) {
break
}
if offset[1] == 0 {
newoffset = [2]uint{offset[0] + 1, offset[1]}
} else {
newoffset = [2]uint{offset[0], prefixLen + uint(innerIndex) + 1}
}
if err != nil {
return nil, newoffset, fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
completed = true
break
}
@@ -270,6 +457,11 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
offset = [2]uint{newoffset[0], newoffset[1]}
var args PumpTradeArgs
if err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump buy/sell decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
event := ""
baseTokenProgram := solana.TokenProgramID
if tradeEvent.IsBuy {
@@ -312,6 +504,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
solAmount = solAmount - fee
}
}
isCashbackCoin := tradeEvent.CashbackFeeBasisPoints > 0 || tradeEvent.Cashback > 0
swaps := []Swap{
{
Program: SolProgramPump,
@@ -333,8 +526,13 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
EntryContract: entryContract,
Cashback: isCashbackCoin,
},
}
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
normalizePumpQuoteSideMint(&swaps[0])
}
if completed {
swaps = append(swaps, Swap{
Program: SolProgramPump,
@@ -348,6 +546,8 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
User: user,
BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves),
QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
@@ -473,6 +673,8 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction
BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount),
QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount),
Mayhem: createEvent.IsMayhemMode,
MigrateTopProgram: pumpAmmProgram,
MigrateToPool: migrateEvent.Pool,
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
EntryContract: entryContract,

View File

@@ -1,14 +1,41 @@
package pump_parser
import (
"encoding/binary"
"encoding/hex"
"fmt"
"testing"
agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/mr-tron/base58"
)
type legacyPumpTradeEvent struct {
Mint solana.PublicKey
SolAmount uint64
TokenAmount uint64
IsBuy bool
User solana.PublicKey
Timestamp int64
VirtualSolReserves uint64
VirtualTokenReserves uint64
RealSolReserves uint64
RealTokenReserves uint64
FeeRecipient solana.PublicKey
FeeBasisPoints uint64
Fee uint64
Creator solana.PublicKey
CreatorFeeBasisPoints uint64
CreatorFee uint64
TrackVolume bool
TotalUnclaimedTokens uint64
TotalClaimedTokens uint64
CurrentSolVolume uint64
LastUpdateTimestamp int64
IxName string
}
func TestTradeEvent(t *testing.T) {
hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e"
d, err := hex.DecodeString(hexData)
@@ -16,16 +43,60 @@ func TestTradeEvent(t *testing.T) {
t.Errorf("Failed to decode base64 data: %v", err)
}
var tradeEvent PumpTradeEvent
var tradeEvent legacyPumpTradeEvent
err = agbinary.NewBorshDecoder(d[16:]).Decode(&tradeEvent)
if err != nil {
t.Errorf("Failed to deserialize trade event: %v", err)
t.Fatalf("Failed to deserialize trade event: %v", err)
}
if tradeEvent.IxName != "buy_exact_sol_in" {
t.Fatalf("IxName = %q, want buy_exact_sol_in", tradeEvent.IxName)
}
if tradeEvent.SolAmount != 11725956 {
t.Fatalf("SolAmount = %d, want 11725956", tradeEvent.SolAmount)
}
if !tradeEvent.IsBuy {
t.Fatalf("IsBuy = false, want true")
}
t.Logf("Trade Event: %+v", tradeEvent)
xx, err := base58.Decode("3Bxs48EzTZB4tzRd")
fmt.Println(len(xx), err)
}
func TestCal(t *testing.T) {
//e445a52e51cb9a1db94afc7d1bd7bc6f5e99e54b
// . b94afc7d1bd7bc6f
s := calculateDiscriminator("global:initialize_with_permission")
fmt.Println(hex.EncodeToString(s[:]))
s2, _ := base58.Decode("6ApXSNCamGdm")
s3 := binary.LittleEndian.Uint64(s2[1:])
fmt.Println(s2, s3)
fmt.Println(solana.MustPublicKeyFromBase58("BM9CcyErJcu2mjrFvUsRRrD3snGeHDDVirJLvL6EjvMN").IsOnCurve())
}
func TestPumpCompleteMatchesTradeEvent(t *testing.T) {
mint := solana.MustPublicKeyFromBase58("8GNGkNnfBuoTP3QRnmdNzSYuuE15M8tvcNvxNsV4pump")
user := solana.MustPublicKeyFromBase58("DS95KxqUCCjwQaXhD7fhKatXbivwWDNrJdNV5ZcubGdz")
bondingCurve := solana.MustPublicKeyFromBase58("Gz5EX3X7kUDS48baijJKubQDKy3BBKpnMJQ3f3W1e9jA")
tradeEvent := PumpTradeEvent{
Mint: mint,
User: user,
}
completeEvent := CompleteEvent{
Mint: mint,
User: user,
BondingCurve: bondingCurve,
}
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, bondingCurve) {
t.Fatal("pumpCompleteMatchesTradeEvent() = false, want true")
}
completeEvent.User = solana.MustPublicKeyFromBase58("3g89wLRwJ5P22fkCdPJBAP7iiYAo6yY96geQvMYj6tYm")
if pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, bondingCurve) {
t.Fatal("pumpCompleteMatchesTradeEvent() = true for mismatched user")
}
}

View File

@@ -41,6 +41,8 @@ type ammBuyEvent struct {
LastUpdateTimestamp int64
MinBaseAmountOut uint64
IxName string
CashbackFeeBasisPoints uint64
Cashback uint64
}
type ammCreatePoolEvent struct {
@@ -113,6 +115,8 @@ type ammSellEvent struct {
CoinCreator solana.PublicKey
CoinCreatorFeeBasisPoints uint64
CoinCreatorFee uint64
CashbackFeeBasisPoints uint64
Cashback uint64
}
type ammWithdrawEvent struct {
@@ -148,14 +152,29 @@ func pumpAmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
discriminator := *(*[8]byte)(decode[:8])
switch discriminator {
case pumpAmmCreateDiscriminator:
if tx.Err != nil {
return nil, increaseOffset(offset), InstructionIgnoredError
}
return ammCreatePoolParser(tx, instruction, innerInstructions, offset)
case pumpAmmBuyDiscriminator, pumpAmmBuyV2Discriminator:
if tx.Err != nil {
return failedTxAmmBuyParser(tx, instruction, innerInstructions, offset)
}
return ammBuyParser(tx, instruction, innerInstructions, offset)
case pumpAmmSellDiscriminator:
if tx.Err != nil {
return failedTxAmmSellParser(tx, instruction, innerInstructions, offset)
}
return ammSellParser(tx, instruction, innerInstructions, offset)
case pumpAmmDepositDiscriminator:
if tx.Err != nil {
return nil, increaseOffset(offset), InstructionIgnoredError
}
return depositParse(tx, instruction, innerInstructions, offset)
case pumpAmmWithdrawDiscriminator:
if tx.Err != nil {
return nil, increaseOffset(offset), InstructionIgnoredError
}
return withdrawParse(tx, instruction, innerInstructions, offset)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
@@ -180,7 +199,7 @@ func ammCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions Inne
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] += uint(innerIndex) + 1 + prefixLen
offset[1] = uint(innerIndex) + 1 + prefixLen
}
if err != nil {
return nil, offset, fmt.Errorf("pump amm create pool event decode error: %v, offset: %d, %d", err, offset[0], prefixLen)
@@ -236,6 +255,271 @@ func ammCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions Inne
}
type PumpSwapArgs struct {
Discriminator [8]byte
Amount1 uint64
Amount2 uint64
}
func pumpAmmSwapAmountInfoFromArgs(args PumpSwapArgs) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
switch {
case bytes.Equal(args.Discriminator[:], pumpAmmBuyV2Discriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpAmmBuyDiscriminator[:]):
return SwapModeExactOut, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpAmmSellDiscriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
default:
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
}
}
func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
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])
}
if tx.Err.Variant != InstructionError {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but error variant is not instruction error, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Enum != Custom && tx.Err.Enum != ComputationalBudgetExceeded && tx.Err.Enum != ProgramFailedToComplete {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but error is not custom or computational budget exceeded, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Enum == Custom {
if !(tx.Err.CustomCode == 1 || tx.Err.CustomCode == 6004 ||
tx.Err.CustomCode == 6040 ||
tx.Err.CustomCode == 6039 ||
tx.Err.CustomCode == 6016 ||
tx.Err.CustomCode == 6014) {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but custom error code is unexpected, offset, %d, %d, code: %d", offset[0], offset[1], tx.Err.CustomCode)
}
}
result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var args PumpSwapArgs
err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm buy failed decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var event string
var (
quoteAmount, tokenAmount uint64
)
if bytes.Equal(args.Discriminator[:], pumpAmmBuyV2Discriminator[:]) {
event = "buy_failed"
quoteAmount = args.Amount1
tokenAmount = args.Amount2
} else if bytes.Equal(args.Discriminator[:], pumpAmmBuyDiscriminator[:]) {
event = "buy_failed"
quoteAmount = args.Amount2
tokenAmount = args.Amount1
} else {
return nil, increaseOffset(offset), fmt.Errorf("unknown pump amm trade instruction discriminator, offset, %d, %d", offset[0], offset[1])
}
baseMint := result.accountList[instruction.Accounts[3]]
quoteMint := result.accountList[instruction.Accounts[4]]
baseTokenProgram := result.accountList[instruction.Accounts[11]]
quoteTokenProgram := result.accountList[instruction.Accounts[12]]
poolBaseAccountIdx := instruction.Accounts[7]
poolQuoteAccountIdx := instruction.Accounts[8]
var (
baseMintDecimals uint8
quoteMintDecimals uint8
)
for _, meta := range result.Meta.PostTokenBalances {
if meta.AccountIndex == poolBaseAccountIdx {
baseMintDecimals = uint8(meta.UITokenAmount.Decimals)
} else if meta.AccountIndex == poolQuoteAccountIdx {
quoteMintDecimals = uint8(meta.UITokenAmount.Decimals)
}
}
if _, exists := tx.Token[baseMint]; !exists && !baseMint.Equals(wSolMint) {
tx.Token[baseMint] = TokenMeta{
Mint: baseMint,
Decimals: baseMintDecimals,
TokenProgram: baseTokenProgram,
}
}
if _, exists := tx.Token[quoteMint]; !exists && !quoteMint.Equals(wSolMint) {
tx.Token[quoteMint] = TokenMeta{
Mint: quoteMint,
Decimals: quoteMintDecimals,
TokenProgram: quoteTokenProgram,
}
}
var eventUser = tx.rawTx.accountList[instruction.Accounts[1]]
baseMintAtaUserIdx := instruction.Accounts[5]
userIndex := instruction.Accounts[1]
if !eventUser.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, baseMint)
// && userBaseAmount.BigInt().Uint64() == event.BaseAmountOut
if !userBaseAmount.IsZero() {
eventUser = result.accountList[0]
userIndex = 0
baseMintAtaUserIdx = ataIndex
}
}
userBase := getAccountBalanceAfterTx(result, baseMintAtaUserIdx)
userQuote := GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint)
if quoteMint.Equals(wSolMint) {
userBalance, _ := GetSolAfterTx(result, userIndex)
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
}
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
swap := Swap{
Program: SolProgramPumpAMM,
Event: event,
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals,
User: eventUser,
BaseAmount: decimal.NewFromUint64(tokenAmount),
QuoteAmount: decimal.NewFromUint64(quoteAmount),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
}
if swapMode, fixedAmount, limitAmount, ok := pumpAmmSwapAmountInfoFromArgs(args); ok {
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
}
return []Swap{swap}, offset, nil
}
func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if 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])
}
if tx.Err.Variant != InstructionError {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but error variant is not instruction error, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Enum != Custom && tx.Err.Enum != ComputationalBudgetExceeded && tx.Err.Enum != ProgramFailedToComplete {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but error is not custom or computational budget exceeded, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Enum == Custom {
if !(tx.Err.CustomCode == 1 || tx.Err.CustomCode == 6004 ||
tx.Err.CustomCode == 6040 ||
tx.Err.CustomCode == 6039 ||
tx.Err.CustomCode == 6016 ||
tx.Err.CustomCode == 6014) {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but custom error code is unexpected, offset, %d, %d, code: %d", offset[0], offset[1], tx.Err.CustomCode)
}
}
result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var args PumpSwapArgs
err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm buy failed decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var event string
var (
quoteAmount, tokenAmount uint64
)
if bytes.Equal(args.Discriminator[:], pumpAmmSellDiscriminator[:]) {
event = "sell_failed"
tokenAmount = args.Amount1
quoteAmount = args.Amount2
} else {
return nil, increaseOffset(offset), fmt.Errorf("unknown pump amm trade instruction discriminator, offset, %d, %d", offset[0], offset[1])
}
baseMint := result.accountList[instruction.Accounts[3]]
quoteMint := result.accountList[instruction.Accounts[4]]
baseTokenProgram := result.accountList[instruction.Accounts[11]]
quoteTokenProgram := result.accountList[instruction.Accounts[12]]
poolBaseAccountIdx := instruction.Accounts[7]
poolQuoteAccountIdx := instruction.Accounts[8]
var (
baseMintDecimals uint8
quoteMintDecimals uint8
)
for _, meta := range result.Meta.PostTokenBalances {
if meta.AccountIndex == poolBaseAccountIdx {
baseMintDecimals = uint8(meta.UITokenAmount.Decimals)
} else if meta.AccountIndex == poolQuoteAccountIdx {
quoteMintDecimals = uint8(meta.UITokenAmount.Decimals)
}
}
if _, exists := tx.Token[baseMint]; !exists && !baseMint.Equals(wSolMint) {
tx.Token[baseMint] = TokenMeta{
Mint: baseMint,
Decimals: baseMintDecimals,
TokenProgram: baseTokenProgram,
}
}
if _, exists := tx.Token[quoteMint]; !exists && !quoteMint.Equals(wSolMint) {
tx.Token[quoteMint] = TokenMeta{
Mint: quoteMint,
Decimals: quoteMintDecimals,
TokenProgram: quoteTokenProgram,
}
}
var eventUser = tx.rawTx.accountList[instruction.Accounts[1]]
baseMintAtaUserIdx := instruction.Accounts[5]
userIndex := instruction.Accounts[1]
if !eventUser.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, baseMint)
// && userBaseAmount.BigInt().Uint64() == event.BaseAmountIn
if !userBaseAmount.IsZero() {
eventUser = result.accountList[0]
userIndex = 0
baseMintAtaUserIdx = ataIndex
}
}
userBase := getAccountBalanceAfterTx(result, baseMintAtaUserIdx)
userQuote := GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint)
if quoteMint.Equals(wSolMint) {
userBalance, _ := GetSolAfterTx(result, userIndex)
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
}
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
swap := Swap{
Program: SolProgramPumpAMM,
Event: event,
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals,
User: eventUser,
BaseAmount: decimal.NewFromUint64(tokenAmount),
QuoteAmount: decimal.NewFromUint64(quoteAmount),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
}
if swapMode, fixedAmount, limitAmount, ok := pumpAmmSwapAmountInfoFromArgs(args); ok {
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
}
return []Swap{swap}, offset, nil
}
func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
@@ -245,10 +529,13 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
}
if !entryContract.Equals(axiomOuterContract) {
if instruction.StackHeight != nil && *instruction.StackHeight > 2 {
for _, innerInstr := range innerInstructions.Instructions {
if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 {
entryContract = result.accountList[innerInstr.ProgramIDIndex]
break
}
}
}
}
@@ -262,7 +549,7 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] += uint(innerIndex) + 1 + prefixLen
offset[1] = uint(innerIndex) + 1 + prefixLen
}
if err != nil {
return nil, offset, fmt.Errorf("pump amm buy pool event decode error: %v, offset: %d, %d", err, offset[0], prefixLen)
@@ -328,8 +615,12 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
userBalance, _ := GetSolAfterTx(result, userIndex)
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
}
return []Swap{
{
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
quoteAmount := decimal.NewFromUint64(event.UserQuoteAmountIn)
if event.IxName == "buy" {
quoteAmount = decimal.NewFromUint64(event.QuoteAmountIn)
}
swap := Swap{
Program: SolProgramPumpAMM,
Event: "buy",
Pool: event.Pool,
@@ -342,15 +633,29 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
QuoteMintDecimals: quoteMintDecimals,
User: eventUser,
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountIn),
QuoteAmount: quoteAmount,
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
Cashback: isCashbackCoin,
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
if bytes.Equal(instruction.Data[:8], pumpAmmBuyV2Discriminator[:]) {
swap.SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(event.UserQuoteAmountIn),
decimal.NewFromUint64(event.MinBaseAmountOut),
)
} else {
swap.SetSwapAmountInfo(
SwapModeExactOut,
decimal.NewFromUint64(event.BaseAmountOut),
decimal.NewFromUint64(event.MaxQuoteAmountIn),
)
}
return []Swap{swap}, offset, nil
}
func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -363,10 +668,13 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
return nil, increaseOffset(offset), fmt.Errorf("pumpamm sell get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
}
if !entryContract.Equals(axiomOuterContract) {
if instruction.StackHeight != nil && *instruction.StackHeight > 2 {
for _, innerInstr := range innerInstructions.Instructions {
if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 {
entryContract = result.accountList[innerInstr.ProgramIDIndex]
break
}
}
}
}
@@ -380,7 +688,7 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] += uint(innerIndex) + 1 + prefixLen
offset[1] = uint(innerIndex) + 1 + prefixLen
}
if err != nil {
return nil, offset, fmt.Errorf("pump amm sell pool event decode error: %v, offset: %d, %d", err, offset[0], prefixLen)
@@ -446,8 +754,8 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
userBalance, _ := GetSolAfterTx(result, userIndex)
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
}
return []Swap{
{
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
swap := Swap{
Program: SolProgramPumpAMM,
Event: "sell",
Pool: event.Pool,
@@ -464,11 +772,17 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn),
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
Cashback: isCashbackCoin,
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(event.BaseAmountIn),
decimal.NewFromUint64(event.MinQuoteAmountOut),
)
return []Swap{swap}, offset, nil
}
func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -491,7 +805,7 @@ func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstru
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] += uint(innerIndex) + 1 + prefixLen
offset[1] = uint(innerIndex) + 1 + prefixLen
}
if err != nil {
return nil, offset, fmt.Errorf("pump amm deposit pool event decode error: %v, offset: %d, %d", err, offset[0], prefixLen)
@@ -589,7 +903,7 @@ func withdrawParse(tx *Tx, instruction Instruction, innerInstructions InnerInstr
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] += uint(innerIndex) + 1 + prefixLen
offset[1] = uint(innerIndex) + 1 + prefixLen
}
if err != nil {
return nil, offset, fmt.Errorf("pump amm withdraw pool event decode error: %v, offset: %d, %d", err, offset[0], prefixLen)

174
rawtx.go
View File

@@ -73,6 +73,10 @@ func (tx *RawTx) GetAccountLust() []solana.PublicKey {
return tx.getAccountList()
}
func (tx *RawTx) GetAccountList() []solana.PublicKey {
return tx.getAccountList()
}
func (tx *RawTx) TxHash() string {
if len(tx.Transaction.Signatures) > 0 {
return tx.Transaction.Signatures[0].String()
@@ -146,7 +150,7 @@ func (tb *TokenBalance) ParseAccount() {
}
type Meta struct {
Err interface{} `json:"err"`
Err *TransactionParsedError `json:"err"`
Fee uint64 `json:"fee"`
InnerInstructions []InnerInstructions `json:"innerInstructions"`
LoadedAddresses LoadedAddresses `json:"loadedAddresses"`
@@ -156,6 +160,7 @@ type Meta struct {
PreBalances []uint64 `json:"preBalances"`
PreTokenBalances []TokenBalance `json:"preTokenBalances"`
Rewards []interface{} `json:"rewards"`
ComputeUnitsConsumed uint64 `json:"computeUnitsConsumed"`
}
type Header struct {
NumReadonlySignedAccounts int `json:"numReadonlySignedAccounts"`
@@ -293,6 +298,16 @@ func InstructionsFromRpc(instructions []solana.CompiledInstruction) []Instructio
return instrs
}
type RpcTransactionErr []interface{}
func marshalRpcTransactionErr(err any) string {
e, _ := json.Marshal(err)
if len(e) == 0 {
return "UnKnown"
}
return string(e)
}
func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, slot uint64, index int64) (*RawTx, error) {
created := int64(0)
if blockTime != nil {
@@ -320,12 +335,68 @@ func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, s
yTx, _ := tx.GetTransaction()
if meta.Err != nil {
e, _ := json.Marshal(meta.Err)
sTx.Meta.Err = string(e)
if iErr, ok := meta.Err.(map[string]any); ok {
instructionError := iErr["InstructionError"]
if instructionError == nil {
sTx.Meta.Err = &TransactionParsedError{
UnKnown: marshalRpcTransactionErr(meta.Err),
}
} else {
if oErr, ok := instructionError.([]any); ok {
if len(oErr) <= 1 {
sTx.Meta.Err = &TransactionParsedError{
UnKnown: marshalRpcTransactionErr(meta.Err),
}
} else if instrIdx, ok := oErr[0].(float64); ok {
sTx.Meta.Err = &TransactionParsedError{
Index: uint8(instrIdx),
Variant: InstructionError,
}
errDetail, ok := oErr[1].(string)
if ok {
if errDetail == "ComputationalBudgetExceeded" || errDetail == "ProgramFailedToComplete" {
sTx.Meta.Err.Enum = ComputationalBudgetExceeded
} else {
sTx.Meta.Err.UnKnown = errDetail
}
} else {
errDetail2, ok := oErr[1].(map[string]any)
if ok && len(errDetail2) > 0 && errDetail2["Custom"] != nil {
custom, ok := errDetail2["Custom"].(float64)
if ok {
sTx.Meta.Err.Enum = Custom
sTx.Meta.Err.CustomCode = uint32(custom)
} else {
sTx.Meta.Err.UnKnown = marshalRpcTransactionErr(meta.Err)
}
} else {
sTx.Meta.Err.UnKnown = marshalRpcTransactionErr(meta.Err)
}
}
} else {
sTx.Meta.Err = &TransactionParsedError{
UnKnown: marshalRpcTransactionErr(meta.Err),
}
}
} else {
sTx.Meta.Err = &TransactionParsedError{
UnKnown: marshalRpcTransactionErr(meta.Err),
}
}
}
} else {
sTx.Meta.Err = &TransactionParsedError{
UnKnown: marshalRpcTransactionErr(meta.Err),
}
}
}
sTx.Meta.Fee = meta.Fee
//sTx.Meta.InnerInstructions = meta.InnerInstructions
if meta.ComputeUnitsConsumed != nil {
sTx.Meta.ComputeUnitsConsumed = *meta.ComputeUnitsConsumed
}
for _, innerInstr := range meta.InnerInstructions {
var instrs []Instruction
for _, instr := range innerInstr.Instructions {
@@ -514,6 +585,79 @@ func intSliceFromUint16Slice(in []uint16) []int {
return out
}
func getAtaIdxByOwner(result *RawTx, owner solana.PublicKey, mint solana.PublicKey) (int, error) {
var preBalance *TokenBalance
for _, pre := range result.Meta.PreTokenBalances {
if pre.MintAccount == mint && pre.OwnerAccount != nil && pre.OwnerAccount.Equals(owner) {
preBalance = &pre
break
}
}
var postBalance *TokenBalance
for _, post := range result.Meta.PostTokenBalances {
if post.MintAccount == mint && post.OwnerAccount != nil && post.OwnerAccount.Equals(owner) {
// post.ParseAccount()
postBalance = &post
break
}
}
if preBalance == nil && postBalance == nil {
return 0, fmt.Errorf("account not found")
}
if preBalance != nil && postBalance != nil && preBalance.AccountIndex != postBalance.AccountIndex {
return 0, fmt.Errorf("ata index not match")
}
if postBalance == nil {
return preBalance.AccountIndex, nil
}
return postBalance.AccountIndex, nil
}
func getAtaByOwner(result *RawTx, owner solana.PublicKey, mint solana.PublicKey) (*TokenBalance, error) {
var preBalance *TokenBalance
for _, pre := range result.Meta.PreTokenBalances {
if pre.MintAccount == mint && pre.OwnerAccount != nil && pre.OwnerAccount.Equals(owner) {
preBalance = &pre
break
}
}
var postBalance *TokenBalance
for _, post := range result.Meta.PostTokenBalances {
if post.MintAccount == mint && post.OwnerAccount != nil && post.OwnerAccount.Equals(owner) {
// post.ParseAccount()
postBalance = &post
break
}
}
if preBalance == nil && postBalance == nil {
return nil, fmt.Errorf("account not found")
}
if preBalance != nil && postBalance != nil && preBalance.AccountIndex != postBalance.AccountIndex {
return nil, fmt.Errorf("ata index not match")
}
if postBalance == nil {
preBalance.ParseAccount()
return &TokenBalance{
AccountIndex: preBalance.AccountIndex,
MintAccount: preBalance.MintAccount,
OwnerAccount: preBalance.OwnerAccount,
ProgramIDAccount: preBalance.ProgramIDAccount,
Mint: preBalance.Mint,
Owner: preBalance.Owner,
ProgramID: preBalance.ProgramID,
UITokenAmount: UITokenAmount{
Amount: "0",
Decimals: preBalance.UITokenAmount.Decimals,
UIAmount: 0,
UIAmountString: "0",
},
}, nil
}
return postBalance, nil
}
func getTokenBalanceAfterTx(result *RawTx, accountIndex int) (*TokenBalance, error) {
var preBalance *TokenBalance
for _, pre := range result.Meta.PreTokenBalances {
@@ -581,9 +725,13 @@ func tokenBalanceChange(result *RawTx, accountIndex int, tokenProgram, mint sola
break
}
}
var err error
if ataIndex == 0 {
ataIndex, err = getAtaIdxByOwner(result, result.accountList[accountIndex], mint)
if err != nil {
return decimal.Zero, ataIndex
}
}
before := decimal.Zero
after := decimal.Zero
@@ -625,10 +773,13 @@ func GetTokenBalanceAfterTx(result *RawTx, accountIndex int, tokenProgram, mint
break
}
}
var x *TokenBalance
var err error
if ataIndex == 0 {
return decimal.Zero
x, err = getAtaByOwner(result, result.accountList[accountIndex], mint)
} else {
x, err = getTokenBalanceAfterTx(result, ataIndex)
}
x, err := getTokenBalanceAfterTx(result, ataIndex)
if err != nil {
return decimal.Zero
}
@@ -716,17 +867,14 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT
if meta.Err != nil && len(meta.Err.GetErr()) > 0 {
// If the transaction has an error, we set the error in the Meta
transError, err := DecodeTransactionError(meta.Err.GetErr())
if err != nil {
sTx.Meta.Err = err
} else {
sTx.Meta.Err = transError
}
sTx.Meta.Err = ParseTransactionErrorFromGeyser(meta.Err.GetErr())
// sTx.Meta.Err = meta.Err.GetErr()
}
sTx.Meta.Fee = meta.Fee
//sTx.Meta.InnerInstructions = meta.InnerInstructions
if meta.ComputeUnitsConsumed != nil {
sTx.Meta.ComputeUnitsConsumed = *meta.ComputeUnitsConsumed
}
for _, innerInstr := range meta.InnerInstructions {
var instrs []Instruction
for _, instr := range innerInstr.Instructions {

1742
rawtx_binary.go Normal file

File diff suppressed because it is too large Load Diff

434
rawtx_binary_test.go Normal file
View File

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

394
raydiumclmm.go Normal file
View File

@@ -0,0 +1,394 @@
package pump_parser
import (
"encoding/binary"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func decodeRaydiumClmmSwapArgs(data []byte) (amountSpecified uint64, otherAmountThreshold uint64, swapMode SwapMode, err error) {
if len(data) < 41 {
return 0, 0, SwapModeUnknown, fmt.Errorf("raydium clmm swap instruction data too short")
}
amountSpecified = binary.LittleEndian.Uint64(data[8:16])
otherAmountThreshold = binary.LittleEndian.Uint64(data[16:24])
isBaseInput := data[40] != 0
swapMode = SwapModeExactOut
if isBaseInput {
swapMode = SwapModeExactIn
}
return amountSpecified, otherAmountThreshold, swapMode, nil
}
func raydiumClmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumClmmProgramID) {
return nil, increaseOffset(offset), fmt.Errorf("raydiumClmm instruction not found, offset, %d, %d", offset[0], offset[1])
}
decode := instruction.Data
if len(decode) < 8 {
return nil, increaseOffset(offset), fmt.Errorf("raydiumClmm program instruction data too short, offset, %d, %d", offset[0], offset[1])
}
discriminator := *(*[8]byte)(decode[:8])
switch discriminator {
case raydiumClmmCreatePoolDiscriminator:
return raydiumClmmCreatePoolParser(tx, instruction, innerInstructions, offset)
case raydiumClmmIncreaseLiquidityDiscriminator, raydiumClmmIncreaseLiquidityV2Discriminator, raydiumClmmOpenPositionDiscriminator, raydiumClmmOpenPositionV2Discriminator, raydiumClmmOpenPositionWithToken22NftDiscriminator:
return raydiumClmmAddLiquidityParser(tx, instruction, innerInstructions, offset)
case raydiumClmmDecreaseLiquidityDiscriminator, raydiumClmmDecreaseLiquidityV2Discriminator:
return raydiumClmmDecreaseLiquidityParser(tx, instruction, innerInstructions, offset)
case raydiumClmmCollectFundFeeDiscriminator, raydiumClmmCollectProtocolFeeDiscriminator:
return raydiumClmmCollectFeeParser(tx, instruction, innerInstructions, offset)
case raydiumClmmSwapDiscriminator, raydiumClmmSwapV2Discriminator:
return raydiumClmmSwapParser(tx, instruction, innerInstructions, offset)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
}
func raydiumClmmCreatePoolParser(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 raydiumClmm create pool instruction, offset, %d, %d", offset[0], offset[1])
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
pool := tx.rawTx.accountList[instruction.Accounts[2]]
creator := tx.rawTx.accountList[instruction.Accounts[0]]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[5])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[6])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
}
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
offset[1] += 9
return []Swap{
{
Program: SolProgramRaydiumCLMM,
Event: "create",
Pool: pool,
BaseMint: baseTokenBalance.MintAccount,
QuoteMint: quoteTokenBalance.MintAccount,
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
Creator: creator,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
User: tx.rawTx.accountList[instruction.Accounts[0]],
EntryContract: entryContract,
},
}, offset, nil
}
func raydiumClmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
discriminator := *(*[8]byte)(instruction.Data[:8])
var (
accountMin int
market solana.PublicKey
//token0 solana.PublicKey
//token1 solana.PublicKey
lpToken solana.PublicKey
vault0 int
vault1 int
)
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
switch discriminator {
case raydiumClmmIncreaseLiquidityDiscriminator:
accountMin = 12
market = tx.rawTx.accountList[instruction.Accounts[2]]
vault0 = instruction.Accounts[9]
vault1 = instruction.Accounts[10]
case raydiumClmmIncreaseLiquidityV2Discriminator:
accountMin = 15
market = tx.rawTx.accountList[instruction.Accounts[2]]
vault0 = instruction.Accounts[9]
vault1 = instruction.Accounts[10]
//token0 = tx.rawTx.accountList[instruction.Accounts[13]]
//token1 = tx.rawTx.accountList[instruction.Accounts[14]]
case raydiumClmmOpenPositionDiscriminator:
accountMin = 19
market = tx.rawTx.accountList[instruction.Accounts[5]]
vault0 = instruction.Accounts[12]
vault1 = instruction.Accounts[13]
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
case raydiumClmmOpenPositionV2Discriminator:
accountMin = 22
market = tx.rawTx.accountList[instruction.Accounts[5]]
vault0 = instruction.Accounts[12]
vault1 = instruction.Accounts[13]
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
//token0 = tx.rawTx.accountList[instruction.Accounts[20]]
//token1 = tx.rawTx.accountList[instruction.Accounts[21]]
case raydiumClmmOpenPositionWithToken22NftDiscriminator:
accountMin = 20
market = tx.rawTx.accountList[instruction.Accounts[4]]
vault0 = instruction.Accounts[11]
vault1 = instruction.Accounts[12]
lpToken = tx.rawTx.accountList[instruction.Accounts[2]]
//token0 = tx.rawTx.accountList[instruction.Accounts[18]]
//token1 = tx.rawTx.accountList[instruction.Accounts[19]]
default:
return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator")
}
if len(instruction.Accounts) < accountMin {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for raydiumClmm add liquidity instruction, offset, %d, %d", offset[0], offset[1])
}
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
}
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
offset[1] += 2
return []Swap{
{
Program: SolProgramRaydiumCLMM,
Event: "add_liquidity",
Pool: market,
BaseMint: baseTokenBalance.MintAccount,
QuoteMint: quoteTokenBalance.MintAccount,
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
LpMint: lpToken,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
User: tx.rawTx.accountList[instruction.Accounts[0]],
EntryContract: entryContract,
},
}, offset, nil
}
func raydiumClmmDecreaseLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
discriminator := *(*[8]byte)(instruction.Data[:8])
var (
accountMin int
market solana.PublicKey
vault0 int
vault1 int
)
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
if discriminator == raydiumClmmDecreaseLiquidityDiscriminator {
accountMin = 14
} else if discriminator == raydiumClmmDecreaseLiquidityV2Discriminator {
accountMin = 16
}
if len(instruction.Accounts) < accountMin {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for decrease liquidity instruction")
}
market = tx.rawTx.accountList[instruction.Accounts[3]]
vault0 = instruction.Accounts[5]
vault1 = instruction.Accounts[6]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
}
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
offset[1] += 2
return []Swap{
{
Program: SolProgramRaydiumCLMM,
Event: "remove_liquidity",
Pool: market,
BaseMint: baseTokenBalance.MintAccount,
QuoteMint: quoteTokenBalance.MintAccount,
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
User: tx.rawTx.accountList[instruction.Accounts[0]],
EntryContract: entryContract,
},
}, offset, nil
}
func raydiumClmmCollectFeeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 11 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for CollectFeeParser instruction")
}
pool := tx.rawTx.accountList[instruction.Accounts[1]]
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
vault0 := instruction.Accounts[3]
vault1 := instruction.Accounts[4]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
}
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
offset[1] += 2
return []Swap{
{
Program: SolProgramRaydiumCLMM,
Event: "remove_liquidity",
Pool: pool,
BaseMint: baseTokenBalance.MintAccount,
QuoteMint: quoteTokenBalance.MintAccount,
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
User: tx.rawTx.accountList[instruction.Accounts[0]],
EntryContract: entryContract,
},
}, offset, nil
}
func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
discriminator := *(*[8]byte)(instruction.Data[:8])
var (
pool solana.PublicKey
accountMin int
tokenInVault int
tokenOutVault int
userTokenInAccount int
userTokenOutAccount int
)
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amountSpecified, otherAmountThreshold, swapMode, err := decodeRaydiumClmmSwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
if discriminator == raydiumClmmSwapDiscriminator {
accountMin = 9
pool = tx.rawTx.accountList[instruction.Accounts[2]]
userTokenInAccount = instruction.Accounts[3]
userTokenOutAccount = instruction.Accounts[4]
tokenInVault = instruction.Accounts[5]
tokenOutVault = instruction.Accounts[6]
} else if discriminator == raydiumClmmSwapV2Discriminator {
accountMin = 13
pool = tx.rawTx.accountList[instruction.Accounts[2]]
userTokenInAccount = instruction.Accounts[3]
userTokenOutAccount = instruction.Accounts[4]
tokenInVault = instruction.Accounts[5]
tokenOutVault = instruction.Accounts[6]
}
if len(instruction.Accounts) < accountMin {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction")
}
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, tokenInVault)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get tokenIn vault balance after tx: %w", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, tokenOutVault)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get tokenOut vault balance after tx: %w", err)
}
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
baseTokenProgram := baseTokenBalance.ProgramIDAccount
quoteTokenProgram := quoteTokenBalance.ProgramIDAccount
baseMint := baseTokenBalance.MintAccount
quoteMint := quoteTokenBalance.MintAccount
baseMintDecimals := uint8(baseTokenBalance.UITokenAmount.Decimals)
quoteMintDecimals := uint8(quoteTokenBalance.UITokenAmount.Decimals)
userBase := getAccountBalanceAfterTx(tx.rawTx, userTokenInAccount)
userQuote := getAccountBalanceAfterTx(tx.rawTx, userTokenOutAccount)
inners, err := getInnerInstructions(innerInstructions, offset[1])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get inner instructions: %w", err)
}
if len(inners) < 2 {
return nil, increaseOffset(offset), fmt.Errorf("not enough inner instructions for swap instruction")
}
baseVaultAccount := tx.rawTx.accountList[tokenInVault]
quoteVaultAccount := tx.rawTx.accountList[tokenOutVault]
userBaseAccount := tx.rawTx.accountList[userTokenInAccount]
userQuoteAccount := tx.rawTx.accountList[userTokenOutAccount]
var baseAmount, quoteAmount decimal.Decimal
var baseFound, quoteFound bool
for i := 0; i < 2; i++ {
inner := inners[i]
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to parse token transfer: %w", err)
}
if from.Equals(userBaseAccount) && to.Equals(baseVaultAccount) && !baseFound {
baseAmount = decimal.NewFromUint64(amount)
baseFound = true
} else if from.Equals(quoteVaultAccount) && to.Equals(userQuoteAccount) && !quoteFound {
quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true
}
}
if !baseFound || !quoteFound {
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions")
}
offset[1] += 2
swap := Swap{
Program: SolProgramRaydiumCLMM,
Event: "sell",
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals,
User: tx.rawTx.accountList[instruction.Accounts[0]],
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
}
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
}

444
raydiumcpmm.go Normal file
View File

@@ -0,0 +1,444 @@
package pump_parser
import (
"bytes"
"fmt"
agbinary "github.com/gagliardetto/binary"
"github.com/shopspring/decimal"
)
type raydiumCPmmSwapBaseInputArgs struct {
AmountIn uint64
MinimumAmountOut uint64
}
type raydiumCPmmSwapBaseOutputArgs struct {
MaxAmountIn uint64
AmountOut uint64
}
func raydiumCPmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumCPmmProgramID) {
return nil, increaseOffset(offset), fmt.Errorf("raydiumCPmm instruction not found, offset, %d, %d", offset[0], offset[1])
}
decode := instruction.Data
if len(decode) < 8 {
return nil, increaseOffset(offset), fmt.Errorf("raydiumCPmm program instruction data too short, offset, %d, %d", offset[0], offset[1])
}
discriminator := *(*[8]byte)(decode[:8])
switch discriminator {
case raydiumCPmmInitializeDiscriminator, raydiumCPmmInitializeWithPermissionDiscriminator:
return raydiumCPmmCreatePoolParser(tx, instruction, innerInstructions, offset)
case raydiumCPmmDepositDiscriminator:
return raydiumCPmmDepositParser(tx, instruction, innerInstructions, offset)
case raydiumCPmmWithdrawDiscriminator:
return raydiumCPmmWithdrawParser(tx, instruction, innerInstructions, offset)
case raydiumCPmmCollectProtocolFeeDiscriminator, raydiumCPmmCollectFundFeeDiscriminator:
return raydiumCPmmCollectParser(tx, instruction, innerInstructions, offset)
case raydiumCPmmSwapBaseInputDiscriminator, raydiumCPmmSwapBaseOutputDiscriminator:
return raydiumCPmmSwapParser(tx, instruction, innerInstructions, offset)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
}
func raydiumCPmmCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 20 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction")
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
pool := tx.rawTx.accountList[instruction.Accounts[3]]
lpMint := tx.rawTx.accountList[instruction.Accounts[6]]
creator := tx.rawTx.accountList[instruction.Accounts[0]]
vault0 := instruction.Accounts[10]
vault1 := instruction.Accounts[11]
if bytes.Equal(instruction.Data[:8], raydiumCPmmInitializeWithPermissionDiscriminator[:]) {
pool = tx.rawTx.accountList[instruction.Accounts[4]]
lpMint = tx.rawTx.accountList[instruction.Accounts[7]]
creator = tx.rawTx.accountList[instruction.Accounts[1]]
vault0 = instruction.Accounts[11]
vault1 = instruction.Accounts[12]
}
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
}
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
offset[1] += 13
return []Swap{
{
Program: SolProgramRaydiumCPMM,
Event: "create",
Pool: pool,
BaseMint: baseTokenBalance.MintAccount,
QuoteMint: quoteTokenBalance.MintAccount,
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
Creator: creator,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
LpMint: lpMint,
User: tx.rawTx.accountList[instruction.Accounts[0]],
EntryContract: entryContract,
},
}, increaseOffset(offset), nil
}
func raydiumCPmmDepositParser(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 deposit instruction")
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
market := tx.rawTx.accountList[instruction.Accounts[2]]
token0User := tx.rawTx.accountList[instruction.Accounts[4]]
token1User := tx.rawTx.accountList[instruction.Accounts[5]]
token0Vault := tx.rawTx.accountList[instruction.Accounts[6]]
token1Vault := tx.rawTx.accountList[instruction.Accounts[7]]
token0 := tx.rawTx.accountList[instruction.Accounts[10]]
token1 := tx.rawTx.accountList[instruction.Accounts[11]]
lpTokenMint := tx.rawTx.accountList[instruction.Accounts[12]]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[6])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[7])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
}
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
inners, err := getInnerInstructions(innerInstructions, offset[1])
var baseFound, quoteFound bool
var baseAmount, quoteAmount decimal.Decimal
for _, inner := range inners {
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
if err != nil {
continue
}
if from.Equals(token0User) && to.Equals(token0Vault) && !baseFound {
baseFound = true
baseAmount = decimal.NewFromUint64(amount)
} else if from.Equals(token1User) && to.Equals(token1Vault) && !quoteFound {
quoteFound = true
quoteAmount = decimal.NewFromUint64(amount)
}
if baseFound && quoteFound {
break
}
}
if !baseFound || !quoteFound {
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions")
}
offset[1] += 3
return []Swap{
{
Program: SolProgramRaydiumCPMM,
Event: "add_liquidity",
Pool: market,
BaseMint: token0,
QuoteMint: token1,
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
LpMint: lpTokenMint,
User: tx.rawTx.accountList[instruction.Accounts[0]],
EntryContract: entryContract,
},
}, offset, nil
}
func raydiumCPmmWithdrawParser(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 deposit instruction")
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
market := tx.rawTx.accountList[instruction.Accounts[2]]
token0User := tx.rawTx.accountList[instruction.Accounts[4]]
token1User := tx.rawTx.accountList[instruction.Accounts[5]]
token0Vault := tx.rawTx.accountList[instruction.Accounts[6]]
token1Vault := tx.rawTx.accountList[instruction.Accounts[7]]
token0 := tx.rawTx.accountList[instruction.Accounts[10]]
token1 := tx.rawTx.accountList[instruction.Accounts[11]]
lpTokenMint := tx.rawTx.accountList[instruction.Accounts[12]]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[6])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[7])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
}
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
inners, err := getInnerInstructions(innerInstructions, offset[1])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get inner instructions: %v", err)
}
var baseFound, quoteFound bool
var baseAmount, quoteAmount decimal.Decimal
for _, inner := range inners {
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
if err != nil {
continue
}
if to.Equals(token0User) && from.Equals(token0Vault) && !baseFound {
baseFound = true
baseAmount = decimal.NewFromUint64(amount)
} else if to.Equals(token1User) && from.Equals(token1Vault) && !quoteFound {
quoteFound = true
quoteAmount = decimal.NewFromUint64(amount)
}
if baseFound && quoteFound {
break
}
}
if !baseFound || !quoteFound {
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions")
}
offset[1] += 3
return []Swap{
{
Program: SolProgramRaydiumCPMM,
Event: "remove_liquidity",
Pool: market,
BaseMint: token0,
QuoteMint: token1,
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
LpMint: lpTokenMint,
User: tx.rawTx.accountList[instruction.Accounts[0]],
EntryContract: entryContract,
},
}, offset, nil
}
func raydiumCPmmCollectParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 12 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for deposit instruction")
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
market := tx.rawTx.accountList[instruction.Accounts[2]]
token0User := tx.rawTx.accountList[instruction.Accounts[8]]
token1User := tx.rawTx.accountList[instruction.Accounts[9]]
token0Vault := tx.rawTx.accountList[instruction.Accounts[4]]
token1Vault := tx.rawTx.accountList[instruction.Accounts[5]]
token0 := tx.rawTx.accountList[instruction.Accounts[6]]
token1 := tx.rawTx.accountList[instruction.Accounts[7]]
inners, err := getInnerInstructions(innerInstructions, offset[1])
var baseFound, quoteFound bool
var baseAmount, quoteAmount decimal.Decimal
for _, inner := range inners {
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
if err != nil {
continue
}
if to.Equals(token0User) && from.Equals(token0Vault) && !baseFound {
baseFound = true
baseAmount = decimal.NewFromUint64(amount)
} else if to.Equals(token1User) && from.Equals(token1Vault) && !quoteFound {
quoteFound = true
quoteAmount = decimal.NewFromUint64(amount)
}
if baseFound && quoteFound {
break
}
}
if !baseFound && !quoteFound {
return nil, increaseOffset(offset), InstructionIgnoredError
}
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[4])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[5])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
}
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
event := "remove_liquidity"
if !baseFound || !quoteFound {
offset[1] += 1
event = "remove_liquidity_one_side"
} else {
offset[1] += 2
}
return []Swap{
{
Program: SolProgramRaydiumCPMM,
Event: event,
Pool: market,
BaseMint: token0,
QuoteMint: token1,
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
User: tx.rawTx.accountList[instruction.Accounts[0]],
EntryContract: entryContract,
},
}, offset, nil
}
func raydiumCPmmSwapParser(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 SwapBaseInputParser instruction")
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
discriminator := *(*[8]byte)(instruction.Data[:8])
var swapMode SwapMode
var fixedAmount decimal.Decimal
var limitAmount decimal.Decimal
switch discriminator {
case raydiumCPmmSwapBaseInputDiscriminator:
var args raydiumCPmmSwapBaseInputArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode raydium cpmm swap_base_input args: %w", err)
}
swapMode = SwapModeExactIn
fixedAmount = decimal.NewFromUint64(args.AmountIn)
limitAmount = decimal.NewFromUint64(args.MinimumAmountOut)
case raydiumCPmmSwapBaseOutputDiscriminator:
var args raydiumCPmmSwapBaseOutputArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode raydium cpmm swap_base_output args: %w", err)
}
swapMode = SwapModeExactOut
fixedAmount = decimal.NewFromUint64(args.AmountOut)
limitAmount = decimal.NewFromUint64(args.MaxAmountIn)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
market := tx.rawTx.accountList[instruction.Accounts[3]]
// Get token accounts from instruction
tokenIn := tx.rawTx.accountList[instruction.Accounts[4]]
tokenOut := tx.rawTx.accountList[instruction.Accounts[5]]
user := tx.rawTx.accountList[instruction.Accounts[0]]
user0 := instruction.Accounts[4]
user1 := instruction.Accounts[5]
inputVault := tx.rawTx.accountList[instruction.Accounts[6]]
outputVault := tx.rawTx.accountList[instruction.Accounts[7]]
vault0 := instruction.Accounts[6]
vault1 := instruction.Accounts[7]
inputTokenMint := tx.rawTx.accountList[instruction.Accounts[10]]
outputTokenMint := tx.rawTx.accountList[instruction.Accounts[11]]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get input amount: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get output amount: %v", err)
}
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
userBase := getAccountBalanceAfterTx(tx.rawTx, user0)
userQuote := getAccountBalanceAfterTx(tx.rawTx, user1)
inners, err := getInnerInstructions(innerInstructions, offset[1])
var baseFound, quoteFound bool
var baseAmount, quoteAmount decimal.Decimal
for _, inner := range inners {
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
if err != nil {
continue
}
if from.Equals(tokenIn) && to.Equals(inputVault) && !baseFound {
baseFound = true
baseAmount = decimal.NewFromUint64(amount)
} else if from.Equals(outputVault) && to.Equals(tokenOut) && !quoteFound {
quoteFound = true
quoteAmount = decimal.NewFromUint64(amount)
}
if baseFound && quoteFound {
break
}
}
if !baseFound || !quoteFound {
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions")
}
offset[1] += 2
swap := Swap{
Program: SolProgramRaydiumCPMM,
Event: "sell",
Pool: market,
BaseMint: inputTokenMint,
QuoteMint: outputTokenMint,
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
User: user,
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
}
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
return []Swap{swap}, offset, nil
}

498
raydiumlaunchlab.go Normal file
View File

@@ -0,0 +1,498 @@
package pump_parser
import (
"bytes"
"fmt"
agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func raydiumLaunchLabParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumLaunchLabProgramID) {
return nil, increaseOffset(offset), fmt.Errorf("raydiumLaunchLab instruction not found, offset, %d, %d", offset[0], offset[1])
}
decode := instruction.Data
if len(decode) < 8 {
return nil, increaseOffset(offset), fmt.Errorf("raydiumLaunchLab program instruction data too short, offset, %d, %d", offset[0], offset[1])
}
discriminator := *(*[8]byte)(decode[:8])
switch discriminator {
case raydiumLaunchLabInitializeWithToken2022PoolDiscriminator, raydiumLaunchLabInitializeV2PoolDiscriminator:
return raydiumLaunchLabInitializeParser(tx, instruction, innerInstructions, offset)
case raydiumLaunchLabMigrateToAmmDiscriminator:
return raydiumLaunchLabMigrateToAmmParser(tx, instruction, innerInstructions, offset)
case raydiumLaunchLabMigrateToCpmmDiscriminator:
return raydiumLaunchLabMigrateToCpmmParser(tx, instruction, innerInstructions, offset)
case raydiumLaunchLabSellExactInDiscriminator,
raydiumLaunchLabSellExactOutDiscriminator,
raydiumLaunchLabBuyExactInDiscriminator,
raydiumLaunchLabBuyExactOutDiscriminator:
return raydiumLaunchLabSwapParser(tx, instruction, innerInstructions, offset)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
}
type VestingParam struct {
TotalLockedAmount uint64
CliffPeriod uint64
UnlockPeriod uint64
}
type CurveParamKind uint8
const (
CurveParamConstant CurveParamKind = 0
CurveParamFixed CurveParamKind = 1
CurveParamLinear CurveParamKind = 2
)
type CurveParam struct {
// rust enum ConstantCurve/FixedCurve/LinearCurve
Kind CurveParamKind
Constant *ConstantCurve
Fixed *FixedCurve
Linear *LinearCurve
}
func (c *CurveParam) TotalSupply() uint64 {
switch c.Kind {
case CurveParamConstant:
return c.Constant.TotalSupply
case CurveParamFixed:
return c.Fixed.TotalSupply
case CurveParamLinear:
return c.Linear.TotalSupply
default:
return 0
}
}
// UnmarshalWithDecoder 让 agbinary/borsh 解码时走自定义逻辑
func (c *CurveParam) UnmarshalWithDecoder(dec *agbinary.Decoder) error {
var tag uint8
if err := dec.Decode(&tag); err != nil {
return fmt.Errorf("decode CurveParam tag: %w", err)
}
c.Kind = CurveParamKind(tag)
c.Constant, c.Fixed, c.Linear = nil, nil, nil
switch c.Kind {
case CurveParamConstant:
var v ConstantCurve
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("decode ConstantCurve: %w", err)
}
c.Constant = &v
case CurveParamFixed:
var v FixedCurve
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("decode FixedCurve: %w", err)
}
c.Fixed = &v
case CurveParamLinear:
var v LinearCurve
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("decode LinearCurve: %w", err)
}
c.Linear = &v
default:
return fmt.Errorf("unknown CurveParam tag: %d", tag)
}
return nil
}
type ConstantCurve struct {
TotalSupply uint64
TotalBaseSell uint64
TotalQuoteFundRaising uint64
MigrateType uint8
}
type FixedCurve struct {
TotalSupply uint64
TotalQuoteFundRaising uint64
MigrateType uint8
}
type LinearCurve struct {
TotalSupply uint64
TotalQuoteFundRaising uint64
MigrateType uint8
}
type BaseMintParam struct {
Decimals uint8
Name string
Symbol string
Uri string
}
type RaydiumLaunchLabCreateEvent struct {
Pool solana.PublicKey
Creator solana.PublicKey
Config solana.PublicKey
BaseMintParam BaseMintParam
CurveParam CurveParam
VestingParam VestingParam
ammFeeOn uint8 // 0 or 1, QuoteToken/BaseToken fee on amm swap
}
func raydiumLaunchLabInitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 15 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction")
}
user := tx.rawTx.accountList[instruction.Accounts[0]]
creator := tx.rawTx.accountList[instruction.Accounts[1]]
pool := tx.rawTx.accountList[instruction.Accounts[5]]
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
baseMint := tx.rawTx.accountList[instruction.Accounts[6]]
quoteMint := tx.rawTx.accountList[instruction.Accounts[7]]
baseVaultIdx := instruction.Accounts[8]
quoteVaultIdx := instruction.Accounts[9]
var (
baseTokenProgram solana.PublicKey
quoteTokenProgram solana.PublicKey
)
if bytes.Equal(instruction.Data[:8], raydiumLaunchLabInitializeWithToken2022PoolDiscriminator[:]) {
baseTokenProgram = tx.rawTx.accountList[instruction.Accounts[10]]
quoteTokenProgram = tx.rawTx.accountList[instruction.Accounts[11]]
} else if bytes.Equal(instruction.Data[:8], raydiumLaunchLabInitializeV2PoolDiscriminator[:]) {
baseTokenProgram = tx.rawTx.accountList[instruction.Accounts[11]]
quoteTokenProgram = tx.rawTx.accountList[instruction.Accounts[12]]
}
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
var programName string
if platformConfig.Equals(bonkPlatformConfig) {
programName = SolProgramRaydiumLaunchLabBonk
} else {
programName = SolProgramRaydiumLaunchLab
}
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
baseDecimals := uint8(baseTokenBalance.UITokenAmount.Decimals)
var createEvent RaydiumLaunchLabCreateEvent
inners, err := getInnerInstructions(innerInstructions, offset[1])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get inner instructions: %v", err)
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
loadedEvent := false
var prefixLen uint = offset[1]
for innerIndex, innerInstruction := range inners {
if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 &&
bytes.Equal(innerInstruction.Data[:8], pumpEventDiscriminator[:]) &&
bytes.Equal(innerInstruction.Data[8:16], raydiumLaunchLabCreatePoolEvnet[:]) &&
len(innerInstruction.Accounts) == 1 {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
}
err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&createEvent)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize create event: %w", err)
}
loadedEvent = true
break
}
}
if !loadedEvent {
return nil, increaseOffset(offset), fmt.Errorf("failed to get create event")
}
totalSupply := decimal.NewFromUint64(createEvent.CurveParam.TotalSupply()).Div(decimal.New(1, int32(baseDecimals)))
tx.Token[baseMint] = TokenMeta{
Mint: baseMint,
TokenProgram: baseTokenProgram,
Decimals: baseDecimals,
Name: createEvent.BaseMintParam.Name,
Symbol: createEvent.BaseMintParam.Symbol,
Url: createEvent.BaseMintParam.Uri,
TotalSupply: &totalSupply,
}
return []Swap{{
Program: programName,
Event: "create",
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
Creator: creator,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
User: user,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
EntryContract: entryContract,
}}, offset, nil
}
func raydiumLaunchLabMigrateToCpmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 27 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for migrate instruction")
}
var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
var programName string
if platformConfig.Equals(bonkPlatformConfig) {
programName = SolProgramRaydiumLaunchLabBonk
} else {
programName = SolProgramRaydiumLaunchLab
}
baseTokenProgram := tx.rawTx.accountList[instruction.Accounts[22]]
quoteTokenProgram := tx.rawTx.accountList[instruction.Accounts[23]]
baseMint := tx.rawTx.accountList[instruction.Accounts[1]]
quoteMint := tx.rawTx.accountList[instruction.Accounts[2]]
pool := tx.rawTx.accountList[instruction.Accounts[17]]
baseVaultIdx := instruction.Accounts[19]
quoteVaultIdx := instruction.Accounts[20]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
offset[1] += 1
return []Swap{
{
Program: programName,
Event: "migrate",
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
User: tx.rawTx.accountList[instruction.Accounts[0]],
//BaseAmount: decimal.Decimal{},
//QuoteAmount: decimal.Decimal{},
MigrateTopProgram: tx.rawTx.accountList[instruction.Accounts[4]],
MigrateToPool: tx.rawTx.accountList[instruction.Accounts[5]],
EntryContract: entryContract,
},
}, offset, nil
}
func raydiumLaunchLabMigrateToAmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 27 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for migrate instruction")
}
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
var programName string
if platformConfig.Equals(bonkPlatformConfig) {
programName = SolProgramRaydiumLaunchLabBonk
} else {
programName = SolProgramRaydiumLaunchLab
}
var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
baseMint := tx.rawTx.accountList[instruction.Accounts[1]]
quoteMint := tx.rawTx.accountList[instruction.Accounts[2]]
pool := tx.rawTx.accountList[instruction.Accounts[23]]
baseVaultIdx := instruction.Accounts[25]
quoteVaultIdx := instruction.Accounts[26]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
baseTokenProgram := baseTokenBalance.ProgramIDAccount
quoteTokenProgram := quoteTokenBalance.ProgramIDAccount
offset[1] += 1
return []Swap{
{
Program: programName,
Event: "migrate",
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
User: tx.rawTx.accountList[instruction.Accounts[0]],
//BaseAmount: decimal.Decimal{},
//QuoteAmount: decimal.Decimal{},
MigrateTopProgram: tx.rawTx.accountList[instruction.Accounts[12]],
MigrateToPool: tx.rawTx.accountList[instruction.Accounts[13]],
EntryContract: entryContract,
},
}, offset, nil
}
type RaydiumLaunchLabSwapEvent struct {
PoolState solana.PublicKey
TotalBaseSell uint64
VirtualBase uint64
VirtualQuote uint64
RealBaseBefore uint64
RealQuoteBefore uint64
RealBaseAfter uint64
RealQuoteAfter uint64
AmountIn uint64
AmountOut uint64
ProtocolFee uint64
PlatformFee uint64
CreatorFee uint64
ShareFee uint64
TradeDirection uint8 // 0: buy 1: sell
PoolStatus uint8 // 0 Fund, 1 Migrate, 2 Trade
}
type raydiumLaunchLabSwapArgs struct {
Amount uint64
OtherAmountThreshold uint64
}
func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
var programName string
if platformConfig.Equals(bonkPlatformConfig) {
programName = SolProgramRaydiumLaunchLabBonk
} else {
programName = SolProgramRaydiumLaunchLab
}
discriminator := *(*[8]byte)(instruction.Data[:8])
var swapMode SwapMode
var fixedAmount decimal.Decimal
var limitAmount decimal.Decimal
var swapArgs raydiumLaunchLabSwapArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&swapArgs); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode raydium launchlab swap args: %w", err)
}
switch discriminator {
case raydiumLaunchLabSellExactInDiscriminator, raydiumLaunchLabBuyExactInDiscriminator:
swapMode = SwapModeExactIn
fixedAmount = decimal.NewFromUint64(swapArgs.Amount)
limitAmount = decimal.NewFromUint64(swapArgs.OtherAmountThreshold)
case raydiumLaunchLabSellExactOutDiscriminator, raydiumLaunchLabBuyExactOutDiscriminator:
swapMode = SwapModeExactOut
fixedAmount = decimal.NewFromUint64(swapArgs.Amount)
limitAmount = decimal.NewFromUint64(swapArgs.OtherAmountThreshold)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
user := tx.rawTx.accountList[instruction.Accounts[0]]
pool := tx.rawTx.accountList[instruction.Accounts[4]]
userBaseIdx := instruction.Accounts[5]
userQuoteIdx := instruction.Accounts[6]
baseVaultIdx := instruction.Accounts[7]
quoteVaultIdx := instruction.Accounts[8]
baseMint := tx.rawTx.accountList[instruction.Accounts[9]]
quoteMint := tx.rawTx.accountList[instruction.Accounts[10]]
baseTokenProgram := tx.rawTx.accountList[instruction.Accounts[11]]
quoteTokenProgram := tx.rawTx.accountList[instruction.Accounts[12]]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
baseMintDecimals := uint8(baseTokenBalance.UITokenAmount.Decimals)
quoteMintDecimals := uint8(quoteTokenBalance.UITokenAmount.Decimals)
inners, err := getInnerInstructions(innerInstructions, offset[1])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get inner instructions: %v", err)
}
var swapEvent RaydiumLaunchLabSwapEvent
loadedEvent := false
var prefixLen uint = offset[1]
for innerIndex, innerInstruction := range inners {
if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 &&
bytes.Equal(innerInstruction.Data[:8], pumpEventDiscriminator[:]) &&
bytes.Equal(innerInstruction.Data[8:16], raydiumLaunchLabTradeEvnet[:]) &&
len(innerInstruction.Accounts) == 1 {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
}
err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&swapEvent)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize swap event: %w", err)
}
loadedEvent = true
break
}
}
if !loadedEvent {
return nil, increaseOffset(offset), fmt.Errorf("failed to get swap event")
}
var event string
var baseAmount, quoteAmount decimal.Decimal
if swapEvent.TradeDirection == 0 {
event = "buy"
baseAmount = decimal.NewFromInt(int64(swapEvent.AmountOut))
quoteAmount = decimal.NewFromInt(int64(swapEvent.AmountIn))
} else {
event = "sell"
baseAmount = decimal.NewFromInt(int64(swapEvent.AmountIn))
quoteAmount = decimal.NewFromInt(int64(swapEvent.AmountOut))
}
baseReserve := decimal.NewFromInt(int64(swapEvent.RealBaseAfter))
quoteReserve := decimal.NewFromInt(int64(swapEvent.RealQuoteAfter))
userBase := getAccountBalanceAfterTx(tx.rawTx, userBaseIdx)
userQuote := getAccountBalanceAfterTx(tx.rawTx, userQuoteIdx)
swap := Swap{
Program: programName,
Event: event,
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals,
User: user,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
Mayhem: false,
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
}
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
return []Swap{swap}, offset, nil
}

523
raydiumv4.go Normal file
View File

@@ -0,0 +1,523 @@
package pump_parser
import (
"encoding/binary"
"fmt"
"github.com/shopspring/decimal"
)
func decodeRaydiumV4SwapArgs(data []byte) (amountSpecified uint64, otherAmountThreshold uint64, swapMode SwapMode, err error) {
if len(data) < 17 {
return 0, 0, SwapModeUnknown, fmt.Errorf("raydium v4 swap instruction data too short")
}
switch data[0] {
case raydiumV4SwapBaseInDiscriminator, raydiumV4SwapBaseInV2Discriminator:
return binary.LittleEndian.Uint64(data[1:9]), binary.LittleEndian.Uint64(data[9:17]), SwapModeExactIn, nil
case raydiumV4SwapBaseOutDiscriminator, raydiumV4SwapBaseOutV2Discriminator:
return binary.LittleEndian.Uint64(data[9:17]), binary.LittleEndian.Uint64(data[1:9]), SwapModeExactOut, nil
default:
return 0, 0, SwapModeUnknown, InstructionIgnoredError
}
}
func raydiumV4Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumV4Program) {
return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 instruction not found, offset, %d, %d", offset[0], offset[1])
}
decode := instruction.Data
if len(decode) < 1 {
return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 program instruction data too short, offset, %d, %d", offset[0], offset[1])
}
discriminator := decode[0]
switch discriminator {
case raydiumV4InitializePoolDiscriminator:
return raydiumv4InitializeParser(tx, instruction, innerInstructions, offset)
case raydiumV4AddLiquidityDiscriminator:
return raydiumv4AddLiquidityParser(tx, instruction, innerInstructions, offset)
case raydiumV4RemoveLiquidityDiscriminator:
return raydiumv4RemoveLiquidityParser(tx, instruction, innerInstructions, offset)
case raydiumV4WithdrawPNLDiscriminator:
return raydiumv4WithdrawPNLParser(tx, instruction, innerInstructions, offset)
case raydiumV4SwapBaseInDiscriminator, raydiumV4SwapBaseOutDiscriminator:
return raydiumv4SwapParser(tx, instruction, innerInstructions, offset)
case raydiumV4SwapBaseInV2Discriminator, raydiumV4SwapBaseOutV2Discriminator:
return raydiumv4SwapV2Parser(tx, instruction, innerInstructions, offset)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
}
func raydiumv4InitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
accountsLen := len(instruction.Accounts)
if accountsLen != 21 {
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 initialize instruction, offset %d, %d", offset[0], offset[1])
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
//who := tx.rawTx.accountList[instruction.Accounts[accountsLen-1]]
user := tx.rawTx.accountList[instruction.Accounts[accountsLen-4]]
baseVaultIdx := instruction.Accounts[10]
quoteVaultIdx := instruction.Accounts[11]
baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
offset[1] += 30
return []Swap{
{
Program: SolProgramRaydiumV4,
Event: "create",
Pool: tx.rawTx.accountList[instruction.Accounts[4]],
BaseMint: baseTokenbalance.MintAccount,
QuoteMint: quoteTokenbalance.MintAccount,
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
Creator: user,
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
User: user,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
EntryContract: entryContract,
},
}, offset, nil
}
func raydiumv4AddLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
accountsLen := len(instruction.Accounts)
if accountsLen != 14 && accountsLen != 15 {
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 add liquidity instruction, offset %d, %d", offset[0], offset[1])
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
baseVaultAccountIndex := instruction.Accounts[6]
quoteVaultAccountIndex := instruction.Accounts[7]
userBaseVaultAccountIndex := instruction.Accounts[9]
userQuoteVaultAccountIndex := instruction.Accounts[10]
baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
inners, err := getInnerInstructions(innerInstructions, offset[1])
if err != nil {
return nil, increaseOffset(offset), err
}
var nextIndex int
var baseFound, quoteFound bool
var baseAmount, quoteAmount decimal.Decimal
for i, inner := range inners {
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
if err != nil {
continue
}
if from.Equals(tx.rawTx.accountList[userBaseVaultAccountIndex]) && to.Equals(tx.rawTx.accountList[baseVaultAccountIndex]) && !baseFound {
baseAmount = decimal.NewFromUint64(amount)
baseFound = true
} else if from.Equals(tx.rawTx.accountList[userQuoteVaultAccountIndex]) && to.Equals(tx.rawTx.accountList[quoteVaultAccountIndex]) && !quoteFound {
quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true
}
if baseFound && quoteFound {
nextIndex = i + 1
break
}
}
if !baseFound || !quoteFound {
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer inner instruction for add liquidity, offset %d, %d", offset[0], offset[1])
}
offset[1] += uint(nextIndex + 1)
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
return []Swap{
{
Program: SolProgramRaydiumV4,
Event: "remove_liquidity",
Pool: tx.rawTx.accountList[instruction.Accounts[1]],
BaseMint: baseTokenbalance.MintAccount,
QuoteMint: quoteTokenbalance.MintAccount,
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
User: tx.rawTx.accountList[instruction.Accounts[12]],
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
EntryContract: entryContract,
},
}, offset, nil
}
func raydiumv4RemoveLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
accountsLen := len(instruction.Accounts)
const AccountLen = 20
if accountsLen != AccountLen && accountsLen != AccountLen+1 && accountsLen != AccountLen+2 && accountsLen != AccountLen+3 {
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 add liquidity instruction, offset %d, %d", offset[0], offset[1])
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
baseVaultAccountIndex := instruction.Accounts[6]
quoteVaultAccountIndex := instruction.Accounts[7]
userBaseVaultAccountIndex := instruction.Accounts[14]
userQuoteVaultAccountIndex := instruction.Accounts[16]
if accountsLen == AccountLen+2 || accountsLen == AccountLen+3 {
userBaseVaultAccountIndex = instruction.Accounts[16]
userQuoteVaultAccountIndex = instruction.Accounts[17]
}
baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
inners, err := getInnerInstructions(innerInstructions, offset[1])
if err != nil {
return nil, increaseOffset(offset), err
}
var nextIndex int
var baseFound, quoteFound bool
var baseAmount, quoteAmount decimal.Decimal
for i, inner := range inners {
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
if err != nil {
continue
}
if to.Equals(tx.rawTx.accountList[userBaseVaultAccountIndex]) && from.Equals(tx.rawTx.accountList[baseVaultAccountIndex]) && !baseFound {
baseAmount = decimal.NewFromUint64(amount)
baseFound = true
} else if to.Equals(tx.rawTx.accountList[userQuoteVaultAccountIndex]) && from.Equals(tx.rawTx.accountList[quoteVaultAccountIndex]) && !quoteFound {
quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true
}
if baseFound && quoteFound {
nextIndex = i + 1
break
}
}
if !baseFound || !quoteFound {
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer inner instruction for add liquidity, offset %d, %d", offset[0], offset[1])
}
offset[1] += uint(nextIndex + 1)
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
return []Swap{
{
Program: SolProgramRaydiumV4,
Event: "remove_liquidity",
Pool: tx.rawTx.accountList[instruction.Accounts[1]],
BaseMint: baseTokenbalance.MintAccount,
QuoteMint: quoteTokenbalance.MintAccount,
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
User: tx.rawTx.accountList[0],
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
EntryContract: entryContract,
},
}, offset, nil
}
func raydiumv4WithdrawPNLParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
accountsLen := len(instruction.Accounts)
if accountsLen != 17 && accountsLen != 18 {
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 WithdrawPNL instruction, offset %d, %d", offset[0], offset[1])
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
baseVaultAccountIndex := instruction.Accounts[5]
quoteVaultAccountIndex := instruction.Accounts[6]
userBaseVaultAccountIndex := instruction.Accounts[7]
userQuoteVaultAccountIndex := instruction.Accounts[8]
baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
inners, err := getInnerInstructions(innerInstructions, offset[1])
if err != nil {
return nil, increaseOffset(offset), err
}
var nextIndex int
var baseFound, quoteFound bool
var baseAmount, quoteAmount decimal.Decimal
for i, inner := range inners {
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
if err != nil {
continue
}
if to.Equals(tx.rawTx.accountList[userBaseVaultAccountIndex]) && from.Equals(tx.rawTx.accountList[baseVaultAccountIndex]) && !baseFound {
baseAmount = decimal.NewFromUint64(amount)
baseFound = true
} else if to.Equals(tx.rawTx.accountList[userQuoteVaultAccountIndex]) && from.Equals(tx.rawTx.accountList[quoteVaultAccountIndex]) && !quoteFound {
quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true
}
if baseFound && quoteFound {
nextIndex = i + 1
break
}
}
if !baseFound || !quoteFound {
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer inner instruction for with pnl, offset %d, %d", offset[0], offset[1])
}
offset[1] += uint(nextIndex + 1)
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
return []Swap{
{
Program: SolProgramRaydiumV4,
Event: "remove_liquidity",
Pool: tx.rawTx.accountList[instruction.Accounts[1]],
BaseMint: baseTokenbalance.MintAccount,
QuoteMint: quoteTokenbalance.MintAccount,
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
User: tx.rawTx.accountList[instruction.Accounts[9]],
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
EntryContract: entryContract,
},
}, offset, nil
}
func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
accountsLen := len(instruction.Accounts)
if accountsLen != 17 && accountsLen != 18 {
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 swap instruction, offset %d, %d", offset[0], offset[1])
}
user := tx.rawTx.accountList[instruction.Accounts[accountsLen-1]]
userSrcIdx := instruction.Accounts[accountsLen-3]
userDestIdx := instruction.Accounts[accountsLen-2]
vaultBaseIdx := instruction.Accounts[4]
vaultQuoteIdx := instruction.Accounts[5]
if accountsLen == 18 {
vaultBaseIdx = instruction.Accounts[5]
vaultQuoteIdx = instruction.Accounts[6]
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amountSpecified, otherAmountThreshold, swapMode, err := decodeRaydiumV4SwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
userSourceTokenAccount := tx.rawTx.accountList[userSrcIdx]
userDestinationTokenAccount := tx.rawTx.accountList[userDestIdx]
baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, vaultBaseIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, vaultQuoteIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
inners, err := getInnerInstructions(innerInstructions, offset[1])
if err != nil {
return nil, increaseOffset(offset), err
}
var nextIndex int
var srcFound, destFound bool
var baseAmount, quoteAmount decimal.Decimal
var event string
for i, inner := range inners {
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
if err != nil {
continue
}
if from.Equals(userSourceTokenAccount) && !srcFound {
if to.Equals(tx.rawTx.accountList[vaultBaseIdx]) {
event = "sell"
baseAmount = decimal.NewFromUint64(amount)
srcFound = true
} else if to.Equals(tx.rawTx.accountList[vaultQuoteIdx]) {
event = "buy"
quoteAmount = decimal.NewFromUint64(amount)
srcFound = true
}
} else if to.Equals(userDestinationTokenAccount) && !destFound {
if from.Equals(tx.rawTx.accountList[vaultQuoteIdx]) {
event = "sell"
quoteAmount = decimal.NewFromUint64(amount)
destFound = true
} else if from.Equals(tx.rawTx.accountList[vaultBaseIdx]) {
event = "buy"
baseAmount = decimal.NewFromUint64(amount)
destFound = true
}
}
if srcFound && destFound {
nextIndex = i + 1
break
}
}
if !srcFound || !destFound {
return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 failed to find token transfer inner instruction for swap, offset %d, %d", offset[0], offset[1])
}
offset[1] += uint(nextIndex + 1)
userBase := getAccountBalanceAfterTx(tx.rawTx, userSrcIdx)
userQuote := getAccountBalanceAfterTx(tx.rawTx, userDestIdx)
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
swap := Swap{
Program: SolProgramRaydiumV4,
Event: event,
Pool: ammAccount,
BaseMint: baseTokenbalance.MintAccount,
QuoteMint: quoteTokenbalance.MintAccount,
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
User: user,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
Mayhem: false,
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
}
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
}
func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
accountsLen := len(instruction.Accounts)
if accountsLen < 8 {
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 swapv2 instruction, offset %d, %d", offset[0], offset[1])
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amountSpecified, otherAmountThreshold, swapMode, err := decodeRaydiumV4SwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
// Raydium's documented V2 layout uses the first 8 accounts. Routed CPI calls
// may append extra readonly accounts (for example the Raydium program id) at
// the tail, so we only require the canonical prefix here.
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
user := tx.rawTx.accountList[instruction.Accounts[7]]
userSourceTokenAccount := tx.rawTx.accountList[instruction.Accounts[5]]
userDestinationTokenAccount := tx.rawTx.accountList[instruction.Accounts[6]]
baseVaultIdx := instruction.Accounts[3]
quoteVaultIdx := instruction.Accounts[4]
baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
inners, err := getInnerInstructions(innerInstructions, offset[1])
if err != nil {
return nil, increaseOffset(offset), err
}
var nextIndex int
var srcFound, destFound bool
var baseAmount, quoteAmount decimal.Decimal
var event string
for i, inner := range inners {
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
if err != nil {
continue
}
if from.Equals(userSourceTokenAccount) && !srcFound {
if to.Equals(tx.rawTx.accountList[baseVaultIdx]) {
event = "sell"
baseAmount = decimal.NewFromUint64(amount)
srcFound = true
} else if to.Equals(tx.rawTx.accountList[quoteVaultIdx]) {
event = "buy"
quoteAmount = decimal.NewFromUint64(amount)
srcFound = true
}
} else if to.Equals(userDestinationTokenAccount) && !destFound {
if from.Equals(tx.rawTx.accountList[quoteVaultIdx]) {
event = "sell"
quoteAmount = decimal.NewFromUint64(amount)
destFound = true
} else if from.Equals(tx.rawTx.accountList[baseVaultIdx]) {
event = "buy"
baseAmount = decimal.NewFromUint64(amount)
destFound = true
}
}
if srcFound && destFound {
nextIndex = i + 1
break
}
}
if !srcFound || !destFound {
return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 failed to find token transfer inner instruction for swapv2, offset %d, %d", offset[0], offset[1])
}
offset[1] += uint(nextIndex + 1)
userBase := getAccountBalanceAfterTx(tx.rawTx, instruction.Accounts[5])
userQuote := getAccountBalanceAfterTx(tx.rawTx, instruction.Accounts[6])
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
swap := Swap{
Program: SolProgramRaydiumV4,
Event: event,
Pool: ammAccount,
BaseMint: baseTokenbalance.MintAccount,
QuoteMint: quoteTokenbalance.MintAccount,
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
User: user,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
Mayhem: false,
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
}
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
}

144
raydiumv4_test.go Normal file
View File

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

230
swap_amounts.go Normal file
View File

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

387
swap_amounts_oracle_test.go Normal file
View File

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

206
swap_amounts_test.go Normal file
View File

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

View File

@@ -34,13 +34,15 @@ func TransferParser(result *RawTx, instruction Instruction, offset [2]uint, tx *
from := result.accountList[result.Transaction.Message.Instructions[offset[0]].Accounts[0]]
to := result.accountList[instruction.Accounts[1]]
if result.Meta.Err == nil {
if offset[1] == 0 {
tx.Transfer = append(tx.Transfer, SolTransfer{
tx.SolTransfer = append(tx.SolTransfer, SolTransfer{
From: from,
To: to,
Amount: decimal.NewFromInt(int64(lamports)), // solana decimals
})
}
}
// load platform by to address
platform, ok := platformFeeAddresses[to]
if ok {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

108
tx.go
View File

@@ -10,6 +10,11 @@ type Swap struct {
Program string
Event string
TxIndex int
InstrIdx uint8
InnerIdx uint8
Pool solana.PublicKey
BaseMint solana.PublicKey
QuoteMint solana.PublicKey
@@ -26,13 +31,49 @@ type Swap struct {
BaseAmount decimal.Decimal
QuoteAmount decimal.Decimal
SwapMode SwapMode
FixedAmount decimal.Decimal
FixedAmountSide SwapAmountSide
FixedMint solana.PublicKey
LimitAmountType SwapLimitType
LimitAmount decimal.Decimal
LimitAmountSide SwapAmountSide
LimitMint solana.PublicKey
ActualLimitAmount decimal.Decimal
ActualLimitAmountSide SwapAmountSide
SlippageBps decimal.Decimal
BaseReserve decimal.Decimal
QuoteReserve decimal.Decimal
Mayhem bool
Cashback bool
UserBaseBalance decimal.Decimal
UserQuoteBalance decimal.Decimal
EntryContract solana.PublicKey
MigrateToPool solana.PublicKey
MigrateTopProgram solana.PublicKey
LpMint solana.PublicKey
AfterSOLBalance decimal.Decimal
//For meteora dlmm
ActiveBinId int32
StartBinId int32
EndBinId int32
RemoveBp int32
PositionAccount solana.PublicKey
FeeAmount decimal.Decimal
FeeBps string
LpFeeAmount decimal.Decimal
FeeSide string
FeeMint solana.PublicKey
FeeTokenProgram solana.PublicKey
FeeMintDecimals uint8
ConsumeUnit uint64
}
type platformInfo struct {
@@ -50,14 +91,20 @@ type SolTransfer struct {
To solana.PublicKey
Amount decimal.Decimal
}
type ChainLink struct {
Timestamp int64
Price decimal.Decimal
}
type Tx struct {
rawTx *RawTx
Vote bool
Signer solana.PublicKey
Err interface{} `json:"err,omitempty"`
Err *TransactionParsedError `json:"err,omitempty"`
Swaps []Swap `json:"swaps,omitempty"`
SolTransfer []SolTransfer `json:"sol_transfer,omitempty"`
Block uint64 `json:"block"`
ChainLink ChainLink `json:"chain_link"`
BlockIndex uint64 `json:"index"`
TxHash *[64]byte `json:"-"`
BlockAt int64 `json:"block_at"`
@@ -76,9 +123,16 @@ type Tx struct {
// update tokenInfo
Token map[solana.PublicKey]TokenMeta `gorm:"-"`
ComputeUnitsConsumed uint64 `json:"compute_units_consumed"`
CuLimit uint32 `json:"cu_limit"`
// todo pool info ??
}
func (tx *Tx) GetRawTx() *RawTx {
return tx.rawTx
}
func (tx *Tx) SetRawTx(t *RawTx) {
tx.rawTx = t
}
@@ -112,7 +166,6 @@ func (tx *Tx) GetTxHash() string {
func (tx *Tx) CheckPlatform(swap Swap) (string, decimal.Decimal) {
// hasSolProgramRaydiumLaunchLabBonk
rawTx := tx.rawTx
var platform string
var platformFee decimal.Decimal
if len(tx.Platform) == 0 {
@@ -124,32 +177,14 @@ func (tx *Tx) CheckPlatform(swap Swap) (string, decimal.Decimal) {
platformFee = info.PlatformFee
break
}
if swap.Event == "buy" && swap.Program == SolProgramRaydiumLaunchLabBonk {
for _, p := range tx.Platform {
switch p.Platform {
case PlatformAxiom:
if !checkBonkAxiomBuy(rawTx) {
platform = PlatformFake
quoteAmount := swap.QuoteAmount
if swap.BaseMint.Equals(solana.WrappedSol) {
quoteAmount = swap.BaseAmount
}
case PlatformGMGN:
if !checkBonkGmgnBuy(rawTx) {
platform = PlatformFake
}
}
}
}
if platform != "" &&
platform != PlatformFake {
if (swap.QuoteMint.Equals(wSolMint) || swap.QuoteMint.IsZero()) &&
platformFee.LessThan(swap.QuoteAmount.Div(decimal.New(1, int32(swap.QuoteMintDecimals))).Div(decimal.NewFromInt(10000)).Mul(decimal.NewFromInt(9))) {
platform != PlatformFake &&
platformFee.LessThan(quoteAmount.Mul(decimal.NewFromInt(9)).Div(decimal.New(10000, 9))) {
platform = PlatformFake
} else if swap.BaseMint.Equals(wSolMint) &&
platformFee.LessThan(swap.QuoteAmount.Div(decimal.New(1, int32(swap.QuoteMintDecimals))).Div(decimal.NewFromInt(10000)).Mul(decimal.NewFromInt(9))) {
platform = PlatformFake
}
}
if platform == "" {
platform = PlatformNone
@@ -184,6 +219,29 @@ func (s Swap) CheckEntryContract() string {
return EntryContractUnknown
}
func (tx *Tx) LoadAfterSOLBalance(swap Swap) decimal.Decimal {
if swap.User.Equals(tx.Signer) {
return tx.AfterSOLBalance
}
found := false
makerIndex := 0
for i, account := range tx.rawTx.getAccountList() {
if account == swap.User {
found = true
makerIndex = i
break
}
}
if found && makerIndex < len(tx.rawTx.Meta.PostBalances) {
return decimal.NewFromInt(
int64(tx.rawTx.Meta.PostBalances[makerIndex]),
).Div(decimal.NewFromInt(1000000000)) // sol decimals
}
return decimal.Zero
}
func (s Swap) CheckEntryContractV2() string {
name, ok := entryContractAddresses[s.EntryContract]
if ok {

2206
tx_binary.go Normal file

File diff suppressed because it is too large Load Diff

146
tx_binary_realdata_test.go Normal file
View File

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

903
tx_binary_test.go Normal file
View File

@@ -0,0 +1,903 @@
package pump_parser
import (
"bytes"
"io"
"testing"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func TestTxBinaryRoundTrip(t *testing.T) {
txHash := [64]byte{}
for i := range txHash {
txHash[i] = byte(i + 1)
}
original := &Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 123456789,
BlockIndex: 42,
TxHash: &txHash,
CuFee: decimal.NewFromInt(5000),
CUPrice: decimal.RequireFromString("0.123456"),
BeforeSolBalance: decimal.RequireFromString("1.500000000"),
AfterSOLBalance: decimal.RequireFromString("1.234567890"),
ComputeUnitsConsumed: 345678,
CuLimit: 400000,
Platform: map[string]platformInfo{
PlatformGMGN: {
Platform: PlatformGMGN,
PlatformFee: decimal.RequireFromString("0.010000000"),
},
PlatformPhoton: {
Platform: PlatformPhoton,
PlatformFee: decimal.RequireFromString("0.020000000"),
},
},
MevAgent: map[string]mevInfo{
MevAgentJito: {
MevAgent: MevAgentJito,
MevAgentFee: decimal.RequireFromString("0.030000000"),
},
},
Swaps: []Swap{
{
Program: SolProgramPump,
Event: TxEventBuy,
TxIndex: 7,
InstrIdx: 2,
InnerIdx: 1,
Pool: mustPubKey("11111111111111111111111111111111"),
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
BaseAmount: decimal.NewFromInt(1200),
QuoteAmount: decimal.NewFromInt(3400),
SwapMode: SwapModeExactIn,
FixedAmount: decimal.NewFromInt(3400),
FixedAmountSide: SwapAmountSideQuote,
FixedMint: solana.WrappedSol,
LimitAmountType: SwapLimitTypeMinOut,
LimitAmount: decimal.NewFromInt(1000),
LimitAmountSide: SwapAmountSideBase,
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
ActualLimitAmount: decimal.NewFromInt(1200),
ActualLimitAmountSide: SwapAmountSideBase,
SlippageBps: decimal.RequireFromString("833.3333"),
BaseReserve: decimal.NewFromInt(5555),
QuoteReserve: decimal.NewFromInt(9999),
Mayhem: true,
Cashback: false,
UserBaseBalance: decimal.NewFromInt(777),
UserQuoteBalance: decimal.NewFromInt(888),
EntryContract: mustPubKey("ComputeBudget111111111111111111111111111111"),
MigrateToPool: mustPubKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
MigrateTopProgram: mustPubKey("AddressLookupTab1e1111111111111111111111111"),
LpMint: mustPubKey("4Nd1mJf8JQhRVTfJxW2YxXLNQKhPYo1JzN1u2KAPY1Hn"),
AfterSOLBalance: decimal.RequireFromString("0.321000000"),
ActiveBinId: 11,
FeeAmount: decimal.NewFromInt(99),
FeeBps: "123",
FeeSide: "base",
ConsumeUnit: 9999,
},
},
}
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 decoded.Signer != original.Signer {
t.Fatalf("Signer = %s, want %s", decoded.Signer, original.Signer)
}
if decoded.Block != original.Block {
t.Fatalf("Block = %d, want %d", decoded.Block, original.Block)
}
if decoded.BlockIndex != original.BlockIndex {
t.Fatalf("BlockIndex = %d, want %d", decoded.BlockIndex, original.BlockIndex)
}
if decoded.TxHash == nil {
t.Fatal("TxHash = nil, want non-nil")
}
if *decoded.TxHash != *original.TxHash {
t.Fatalf("TxHash mismatch")
}
if !decoded.CuFee.Equal(original.CuFee) {
t.Fatalf("CuFee = %s, want %s", decoded.CuFee, original.CuFee)
}
if !decoded.CUPrice.Equal(original.CUPrice) {
t.Fatalf("CUPrice = %s, want %s", decoded.CUPrice, original.CUPrice)
}
if decoded.BeforeSolBalance.StringFixed(9) != original.BeforeSolBalance.StringFixed(9) {
t.Fatalf("BeforeSolBalance = %s, want %s", decoded.BeforeSolBalance, original.BeforeSolBalance)
}
if decoded.AfterSOLBalance.StringFixed(9) != original.AfterSOLBalance.StringFixed(9) {
t.Fatalf("AfterSOLBalance = %s, want %s", decoded.AfterSOLBalance, original.AfterSOLBalance)
}
if decoded.CuLimit != original.CuLimit {
t.Fatalf("CuLimit = %d, want %d", decoded.CuLimit, original.CuLimit)
}
if decoded.ComputeUnitsConsumed != original.ComputeUnitsConsumed {
t.Fatalf("ComputeUnitsConsumed = %d, want %d", decoded.ComputeUnitsConsumed, original.ComputeUnitsConsumed)
}
if len(decoded.Platform) != len(original.Platform) {
t.Fatalf("Platform len = %d, want %d", len(decoded.Platform), len(original.Platform))
}
if !decoded.Platform[PlatformGMGN].PlatformFee.Equal(original.Platform[PlatformGMGN].PlatformFee) {
t.Fatalf("Platform fee mismatch")
}
if len(decoded.MevAgent) != len(original.MevAgent) {
t.Fatalf("MevAgent len = %d, want %d", len(decoded.MevAgent), len(original.MevAgent))
}
if !decoded.MevAgent[MevAgentJito].MevAgentFee.Equal(original.MevAgent[MevAgentJito].MevAgentFee) {
t.Fatalf("MevAgent fee mismatch")
}
if len(decoded.Swaps) != 1 {
t.Fatalf("Swaps len = %d, want 1", len(decoded.Swaps))
}
swap := decoded.Swaps[0]
if swap.Program != original.Swaps[0].Program {
t.Fatalf("swap.Program = %s, want %s", swap.Program, original.Swaps[0].Program)
}
if swap.Event != original.Swaps[0].Event {
t.Fatalf("swap.Event = %s, want %s", swap.Event, original.Swaps[0].Event)
}
if swap.TxIndex != original.Swaps[0].TxIndex {
t.Fatalf("swap.TxIndex = %d, want %d", swap.TxIndex, original.Swaps[0].TxIndex)
}
if !swap.BaseAmount.Equal(original.Swaps[0].BaseAmount) {
t.Fatalf("swap.BaseAmount = %s, want %s", swap.BaseAmount, original.Swaps[0].BaseAmount)
}
if !swap.QuoteAmount.Equal(original.Swaps[0].QuoteAmount) {
t.Fatalf("swap.QuoteAmount = %s, want %s", swap.QuoteAmount, original.Swaps[0].QuoteAmount)
}
if !swap.FixedAmount.Equal(original.Swaps[0].FixedAmount) {
t.Fatalf("swap.FixedAmount = %s, want %s", swap.FixedAmount, original.Swaps[0].FixedAmount)
}
if !swap.LimitAmount.Equal(original.Swaps[0].LimitAmount) {
t.Fatalf("swap.LimitAmount = %s, want %s", swap.LimitAmount, original.Swaps[0].LimitAmount)
}
if !swap.ActualLimitAmount.Equal(original.Swaps[0].ActualLimitAmount) {
t.Fatalf("swap.ActualLimitAmount = %s, want %s", swap.ActualLimitAmount, original.Swaps[0].ActualLimitAmount)
}
if swap.SlippageBps.String() != "833" {
t.Fatalf("swap.SlippageBps = %s, want 833", swap.SlippageBps)
}
if !swap.BaseReserve.Equal(original.Swaps[0].BaseReserve) {
t.Fatalf("swap.BaseReserve = %s, want %s", swap.BaseReserve, original.Swaps[0].BaseReserve)
}
if !swap.QuoteReserve.Equal(original.Swaps[0].QuoteReserve) {
t.Fatalf("swap.QuoteReserve = %s, want %s", swap.QuoteReserve, original.Swaps[0].QuoteReserve)
}
if !swap.UserBaseBalance.Equal(original.Swaps[0].UserBaseBalance) {
t.Fatalf("swap.UserBaseBalance = %s, want %s", swap.UserBaseBalance, original.Swaps[0].UserBaseBalance)
}
if !swap.UserQuoteBalance.Equal(original.Swaps[0].UserQuoteBalance) {
t.Fatalf("swap.UserQuoteBalance = %s, want %s", swap.UserQuoteBalance, original.Swaps[0].UserQuoteBalance)
}
if swap.AfterSOLBalance.StringFixed(9) != original.Swaps[0].AfterSOLBalance.StringFixed(9) {
t.Fatalf("swap.AfterSOLBalance = %s, want %s", swap.AfterSOLBalance, original.Swaps[0].AfterSOLBalance)
}
if swap.ActiveBinId != 0 {
t.Fatalf("swap.ActiveBinId = %d, want 0", swap.ActiveBinId)
}
if !swap.FeeAmount.IsZero() {
t.Fatalf("swap.FeeAmount = %s, want 0", swap.FeeAmount)
}
if swap.FeeBps != "" {
t.Fatalf("swap.FeeBps = %q, want empty", swap.FeeBps)
}
if swap.FeeSide != "" {
t.Fatalf("swap.FeeSide = %q, want empty", swap.FeeSide)
}
if swap.ConsumeUnit != 0 {
t.Fatalf("swap.ConsumeUnit = %d, want 0", swap.ConsumeUnit)
}
}
func TestTxBinaryRejectsUnknownProgramEnum(t *testing.T) {
txBinary := &TxBinary{
SchemaVersion: txBinarySchemaVersionCurrent,
EnumVersion: txBinaryEnumVersionV1,
Swaps: []SwapBinary{
{Program: "unknown_program"},
},
}
if _, err := txBinary.MarshalBinary(); err == nil {
t.Fatal("MarshalBinary() error = nil, want error")
}
}
func TestTxBinaryAcceptsKnownEventEnums(t *testing.T) {
events := []string{
TxEventAddLP,
TxEventRemoveLP,
TxEventBuy,
TxEventSell,
TxEventBuyFailed,
TxEventSellFailed,
TxEventBurn,
TxEventCreate,
TxEventComplete,
TxEventMigrate,
TxEventDeposit,
TxEventWithdraw,
TxEventOpen,
TxEventClose,
TxEventClaimFee,
TxEventAddLiquidity,
TxEventAddLiquidityOneSide,
TxEventRemoveLiquidity,
TxEventRemoveLiquidityOneSide,
}
for _, event := range events {
t.Run(event, func(t *testing.T) {
txBinary := &TxBinary{
SchemaVersion: txBinarySchemaVersionCurrent,
EnumVersion: txBinaryEnumVersionV1,
AddressTable: []solana.PublicKey{
mustPubKey("11111111111111111111111111111111"),
mustPubKey("So11111111111111111111111111111111111111112"),
solana.TokenProgramID,
mustPubKey("BPFLoader1111111111111111111111111111111111"),
mustPubKey("SysvarRent111111111111111111111111111111111"),
},
Swaps: []SwapBinary{
{
Program: SolProgramPump,
Event: event,
Pool: 0,
BaseMint: 1,
QuoteMint: 1,
BaseTokenProgram: 2,
QuoteTokenProgram: 2,
Creator: 3,
User: 4,
FixedMint: 1,
LimitMint: 1,
},
},
}
encoded, err := txBinary.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary() error = %v", err)
}
var decoded TxBinary
if err := decoded.UnmarshalBinary(encoded); err != nil {
t.Fatalf("UnmarshalBinary() error = %v", err)
}
if got := decoded.Swaps[0].Event; got != event {
t.Fatalf("decoded event = %q, want %q", got, event)
}
})
}
}
func TestTxBinaryPreservesFractionalReserves(t *testing.T) {
tx := &Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 1,
BlockIndex: 1,
CuFee: decimal.NewFromInt(1),
CUPrice: decimal.RequireFromString("0.000001"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
ComputeUnitsConsumed: 1,
CuLimit: 1,
Swaps: []Swap{
{
Program: SolProgramMeteoraPools,
Event: TxEventAddLiquidity,
Pool: mustPubKey("11111111111111111111111111111111"),
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
BaseAmount: decimal.NewFromInt(10),
QuoteAmount: decimal.NewFromInt(20),
SwapMode: SwapModeExactIn,
FixedAmount: decimal.NewFromInt(20),
FixedAmountSide: SwapAmountSideQuote,
FixedMint: solana.WrappedSol,
LimitAmountType: SwapLimitTypeMinOut,
LimitAmount: decimal.NewFromInt(9),
LimitAmountSide: SwapAmountSideBase,
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
ActualLimitAmount: decimal.NewFromInt(10),
ActualLimitAmountSide: SwapAmountSideBase,
BaseReserve: decimal.RequireFromString("123.4"),
QuoteReserve: decimal.RequireFromString("710079483.625409498"),
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
},
},
}
encoded, err := EncodeTxBinary(tx)
if err != nil {
t.Fatalf("EncodeTxBinary() error = %v", err)
}
decoded, err := DecodeTxBinary(encoded)
if err != nil {
t.Fatalf("DecodeTxBinary() error = %v", err)
}
if got := decoded.Swaps[0].BaseReserve.String(); got != "123.4" {
t.Fatalf("BaseReserve = %s, want 123.4", got)
}
diff := decoded.Swaps[0].QuoteReserve.Sub(decimal.RequireFromString("710079483.625409498")).Abs()
if diff.GreaterThan(decimal.RequireFromString("0.0000001")) {
t.Fatalf("QuoteReserve diff = %s, want <= 0.0000001", diff)
}
}
func TestTxBinaryCanonicalizesOnSideEventAlias(t *testing.T) {
tx := &Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 1,
BlockIndex: 1,
CuFee: decimal.NewFromInt(1),
CUPrice: decimal.RequireFromString("0.000001"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
ComputeUnitsConsumed: 1,
CuLimit: 1,
Swaps: []Swap{
{
Program: SolProgramOrcaWhirPool,
Event: "remove_liquidity_on_side",
Pool: mustPubKey("11111111111111111111111111111111"),
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
BaseAmount: decimal.NewFromInt(10),
QuoteAmount: decimal.Zero,
SwapMode: SwapModeExactIn,
FixedAmount: decimal.NewFromInt(10),
FixedAmountSide: SwapAmountSideBase,
FixedMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
LimitAmountType: SwapLimitTypeMinOut,
LimitAmount: decimal.Zero,
LimitAmountSide: SwapAmountSideQuote,
ActualLimitAmount: decimal.Zero,
ActualLimitAmountSide: SwapAmountSideQuote,
BaseReserve: decimal.RequireFromString("123.4"),
QuoteReserve: decimal.RequireFromString("456.7"),
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
},
},
}
encoded, err := EncodeTxBinary(tx)
if err != nil {
t.Fatalf("EncodeTxBinary() error = %v", err)
}
decoded, err := DecodeTxBinary(encoded)
if err != nil {
t.Fatalf("DecodeTxBinary() error = %v", err)
}
if got := decoded.Swaps[0].Event; got != TxEventRemoveLiquidityOneSide {
t.Fatalf("Event = %q, want %q", got, TxEventRemoveLiquidityOneSide)
}
}
func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
tx1 := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 1,
BlockIndex: 1,
CuFee: decimal.NewFromInt(1000),
CUPrice: decimal.RequireFromString("0.123456"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
ComputeUnitsConsumed: 100,
CuLimit: 200000,
Swaps: []Swap{
{
Program: SolProgramPump,
Event: TxEventBuy,
TxIndex: 1,
InstrIdx: 0,
InnerIdx: 0,
Pool: mustPubKey("11111111111111111111111111111111"),
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
BaseAmount: decimal.NewFromInt(10),
QuoteAmount: decimal.NewFromInt(20),
SwapMode: SwapModeExactIn,
FixedAmount: decimal.NewFromInt(20),
FixedAmountSide: SwapAmountSideQuote,
FixedMint: solana.WrappedSol,
LimitAmountType: SwapLimitTypeMinOut,
LimitAmount: decimal.NewFromInt(9),
LimitAmountSide: SwapAmountSideBase,
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
ActualLimitAmount: decimal.NewFromInt(10),
ActualLimitAmountSide: SwapAmountSideBase,
SlippageBps: decimal.RequireFromString("100.2"),
BaseReserve: decimal.NewFromInt(100),
QuoteReserve: decimal.NewFromInt(200),
UserBaseBalance: decimal.NewFromInt(1),
UserQuoteBalance: decimal.NewFromInt(2),
EntryContract: solana.PublicKey{},
MigrateToPool: solana.PublicKey{},
MigrateTopProgram: solana.PublicKey{},
LpMint: solana.PublicKey{},
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
},
},
}
tx2 := tx1
tx2.Block = 2
tx2.BlockIndex = 2
tx2.CuFee = decimal.NewFromInt(2000)
tx2.AfterSOLBalance = decimal.RequireFromString("0.700000000")
tx2.Swaps = []Swap{tx1.Swaps[0]}
tx2.Swaps[0].TxIndex = 2
tx2.Swaps[0].BaseAmount = decimal.NewFromInt(30)
tx2.Swaps[0].QuoteAmount = decimal.NewFromInt(40)
batchEncoded, err := EncodeTxsBinary([]Tx{tx1, tx2})
if err != nil {
t.Fatalf("EncodeTxsBinary() error = %v", err)
}
decoded, err := DecodeTxsBinary(batchEncoded)
if err != nil {
t.Fatalf("DecodeTxsBinary() error = %v", err)
}
if len(decoded) != 2 {
t.Fatalf("decoded len = %d, want 2", len(decoded))
}
if decoded[0].Signer != tx1.Signer || decoded[1].Signer != tx2.Signer {
t.Fatalf("decoded signer mismatch")
}
if decoded[0].Swaps[0].Pool != tx1.Swaps[0].Pool || decoded[1].Swaps[0].Pool != tx2.Swaps[0].Pool {
t.Fatalf("decoded shared address mismatch")
}
single1, err := EncodeTxBinary(&tx1)
if err != nil {
t.Fatalf("EncodeTxBinary(tx1) error = %v", err)
}
single2, err := EncodeTxBinary(&tx2)
if err != nil {
t.Fatalf("EncodeTxBinary(tx2) error = %v", err)
}
if len(batchEncoded) >= len(single1)+len(single2) {
t.Fatalf("batch encoded = %d, want smaller than singles sum %d", len(batchEncoded), len(single1)+len(single2))
}
}
func TestDecodeTxsBinaryReader(t *testing.T) {
tx1 := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 100,
BlockIndex: 7,
CuFee: decimal.NewFromInt(111),
CUPrice: decimal.RequireFromString("0.123456"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.500000000"),
ComputeUnitsConsumed: 1234,
CuLimit: 250000,
Swaps: []Swap{
{
Program: SolProgramPump,
Event: TxEventBuy,
TxIndex: 3,
InstrIdx: 1,
InnerIdx: 2,
Pool: mustPubKey("11111111111111111111111111111111"),
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
BaseAmount: decimal.NewFromInt(100),
QuoteAmount: decimal.NewFromInt(200),
SwapMode: SwapModeExactIn,
FixedAmount: decimal.NewFromInt(200),
FixedAmountSide: SwapAmountSideQuote,
FixedMint: solana.WrappedSol,
LimitAmountType: SwapLimitTypeMinOut,
LimitAmount: decimal.NewFromInt(90),
LimitAmountSide: SwapAmountSideBase,
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
ActualLimitAmount: decimal.NewFromInt(100),
ActualLimitAmountSide: SwapAmountSideBase,
SlippageBps: decimal.RequireFromString("99.6"),
BaseReserve: decimal.NewFromInt(1000),
QuoteReserve: decimal.NewFromInt(2000),
UserBaseBalance: decimal.NewFromInt(10),
UserQuoteBalance: decimal.NewFromInt(20),
AfterSOLBalance: decimal.RequireFromString("0.400000000"),
},
},
}
tx2 := tx1
tx2.Block = 101
tx2.BlockIndex = 8
tx2.CuFee = decimal.NewFromInt(222)
tx2.AfterSOLBalance = decimal.RequireFromString("0.300000000")
tx2.Swaps = []Swap{tx1.Swaps[0]}
tx2.Swaps[0].TxIndex = 4
tx2.Swaps[0].BaseAmount = decimal.NewFromInt(300)
encoded, err := EncodeTxsBinary([]Tx{tx1, tx2})
if err != nil {
t.Fatalf("EncodeTxsBinary() error = %v", err)
}
var decoded []*Tx
for tx, err := range DecodeTxsBinaryReader(bytes.NewReader(encoded)) {
if err != nil {
t.Fatalf("DecodeTxsBinaryReader() error = %v", err)
}
decoded = append(decoded, tx)
}
if len(decoded) != 2 {
t.Fatalf("decoded len = %d, want 2", len(decoded))
}
if decoded[0].Signer != tx1.Signer || decoded[1].Signer != tx2.Signer {
t.Fatalf("decoded signer mismatch")
}
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
t.Fatalf("decoded block mismatch")
}
if decoded[0].Swaps[0].BaseAmount.Cmp(tx1.Swaps[0].BaseAmount) != 0 {
t.Fatalf("decoded tx1 swap base amount = %s, want %s", decoded[0].Swaps[0].BaseAmount, tx1.Swaps[0].BaseAmount)
}
if decoded[1].Swaps[0].BaseAmount.Cmp(tx2.Swaps[0].BaseAmount) != 0 {
t.Fatalf("decoded tx2 swap base amount = %s, want %s", decoded[1].Swaps[0].BaseAmount, tx2.Swaps[0].BaseAmount)
}
}
func TestDecodeTxsBinaryReaderEarlyStop(t *testing.T) {
tx := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 1,
BlockIndex: 1,
CuFee: decimal.NewFromInt(1),
CUPrice: decimal.RequireFromString("0.000001"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.999999999"),
ComputeUnitsConsumed: 1,
CuLimit: 1,
}
encoded, err := EncodeTxsBinary([]Tx{tx, tx, tx})
if err != nil {
t.Fatalf("EncodeTxsBinary() error = %v", err)
}
count := 0
for decodedTx, err := range DecodeTxsBinaryReader(bytes.NewReader(encoded)) {
if err != nil {
t.Fatalf("DecodeTxsBinaryReader() error = %v", err)
}
if decodedTx == nil {
t.Fatal("decoded tx is nil")
}
count++
break
}
if count != 1 {
t.Fatalf("count = %d, want 1", count)
}
}
func TestMergeTxsBinaryBytes(t *testing.T) {
tx1 := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 11,
BlockIndex: 1,
CuFee: decimal.NewFromInt(10),
CUPrice: decimal.RequireFromString("0.000123"),
BeforeSolBalance: decimal.RequireFromString("1.100000000"),
AfterSOLBalance: decimal.RequireFromString("1.000000000"),
ComputeUnitsConsumed: 10,
CuLimit: 100,
Swaps: []Swap{
{
Program: SolProgramPump,
Event: TxEventBuy,
TxIndex: 1,
Pool: mustPubKey("11111111111111111111111111111111"),
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
FixedMint: solana.WrappedSol,
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
EntryContract: solana.PublicKey{},
MigrateToPool: solana.PublicKey{},
MigrateTopProgram: solana.PublicKey{},
LpMint: solana.PublicKey{},
},
},
}
tx2 := Tx{
Signer: mustPubKey("SysvarRent111111111111111111111111111111111"),
Block: 12,
BlockIndex: 2,
CuFee: decimal.NewFromInt(20),
CUPrice: decimal.RequireFromString("0.000456"),
BeforeSolBalance: decimal.RequireFromString("2.200000000"),
AfterSOLBalance: decimal.RequireFromString("2.000000000"),
ComputeUnitsConsumed: 20,
CuLimit: 200,
Swaps: []Swap{
{
Program: SolProgramPump,
Event: TxEventSell,
TxIndex: 2,
Pool: mustPubKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
BaseMint: mustPubKey("4Nd1mJf8JQhRVTfJxW2YxXLNQKhPYo1JzN1u2KAPY1Hn"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("ComputeBudget111111111111111111111111111111"),
User: mustPubKey("So11111111111111111111111111111111111111112"),
FixedMint: solana.WrappedSol,
LimitMint: mustPubKey("4Nd1mJf8JQhRVTfJxW2YxXLNQKhPYo1JzN1u2KAPY1Hn"),
EntryContract: solana.PublicKey{},
MigrateToPool: solana.PublicKey{},
MigrateTopProgram: solana.PublicKey{},
LpMint: solana.PublicKey{},
},
},
}
batch1, err := EncodeTxsBinary([]Tx{tx1})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch1) error = %v", err)
}
batch2, err := EncodeTxsBinary([]Tx{tx2})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch2) error = %v", err)
}
merged, err := MergeTxsBinaryBytes([][]byte{batch1, batch2})
if err != nil {
t.Fatalf("MergeTxsBinaryBytes() error = %v", err)
}
var mergedBinary TxsBinary
if err := mergedBinary.UnmarshalBinary(merged); err != nil {
t.Fatalf("UnmarshalBinary(merged) error = %v", err)
}
if len(mergedBinary.Txs) != 2 {
t.Fatalf("merged tx count = %d, want 2", len(mergedBinary.Txs))
}
if len(mergedBinary.AddressTable) >= len(mustTxBinary(t, batch1).AddressTable)+len(mustTxBinary(t, batch2).AddressTable) {
t.Fatalf("merged address table was not deduplicated")
}
decoded, err := DecodeTxsBinary(merged)
if err != nil {
t.Fatalf("DecodeTxsBinary(merged) error = %v", err)
}
if len(decoded) != 2 {
t.Fatalf("decoded len = %d, want 2", len(decoded))
}
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
t.Fatalf("decoded block mismatch")
}
}
func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
tx1 := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 21,
BlockIndex: 1,
CuFee: decimal.NewFromInt(1),
CUPrice: decimal.RequireFromString("0.000001"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
ComputeUnitsConsumed: 11,
CuLimit: 111,
}
tx2 := tx1
tx2.Block = 22
tx2.BlockIndex = 2
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
tx3 := tx1
tx3.Block = 23
tx3.BlockIndex = 3
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
batch1, err := EncodeTxsBinary([]Tx{tx1})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch1) error = %v", err)
}
batch2, err := EncodeTxsBinary([]Tx{tx2})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch2) error = %v", err)
}
batch3, err := EncodeTxsBinary([]Tx{tx3})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch3) error = %v", err)
}
source1 := &testTxsBinarySource{
data: append(append([]byte{}, batch1...), batch2...),
}
source2 := &testTxsBinarySource{
data: batch3,
}
var out bytes.Buffer
if err := MergeTxsBinarySourcesToWriter([]TxsBinaryReaderSource{source1, source2}, &out); err != nil {
t.Fatalf("MergeTxsBinarySourcesToWriter() error = %v", err)
}
if source1.opens != 2 || source2.opens != 2 {
t.Fatalf("source opens = (%d, %d), want (2, 2)", source1.opens, source2.opens)
}
decoded, err := DecodeTxsBinary(out.Bytes())
if err != nil {
t.Fatalf("DecodeTxsBinary(merged) error = %v", err)
}
if len(decoded) != 3 {
t.Fatalf("decoded len = %d, want 3", len(decoded))
}
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block || decoded[2].Block != tx3.Block {
t.Fatalf("decoded block order mismatch")
}
}
func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) {
tx1 := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 31,
BlockIndex: 1,
CuFee: decimal.NewFromInt(1),
CUPrice: decimal.RequireFromString("0.000001"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
ComputeUnitsConsumed: 11,
CuLimit: 111,
}
tx2 := tx1
tx2.Block = 32
tx2.BlockIndex = 2
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
tx3 := tx1
tx3.Block = 33
tx3.BlockIndex = 3
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
batch1, err := EncodeTxsBinary([]Tx{tx1})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch1) error = %v", err)
}
batch2, err := EncodeTxsBinary([]Tx{tx2})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch2) error = %v", err)
}
batch3, err := EncodeTxsBinary([]Tx{tx3})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch3) error = %v", err)
}
source := &testTxsBinarySource{
data: append(
append(
append([]byte{}, testBatchHeader(false)...),
batch1...,
),
append(
append(testBatchHeader(true), batch2...),
append(testBatchHeader(false), batch3...)...,
)...,
),
}
var out bytes.Buffer
err = MergeTxsBinarySourcesToWriterWithOptions(
[]TxsBinaryReaderSource{source},
&out,
TxsBinaryMergeOptions{
BatchHeaderFunc: func(ctx *TxsBinaryBatchHeaderContext) (bool, error) {
header := make([]byte, 5)
if _, err := io.ReadFull(ctx.Reader, header); err != nil {
return false, err
}
if !bytes.Equal(header[:4], []byte("BHDR")) {
return false, io.ErrUnexpectedEOF
}
return header[4] == 1, nil
},
},
)
if err != nil {
t.Fatalf("MergeTxsBinarySourcesToWriterWithOptions() error = %v", err)
}
decoded, err := DecodeTxsBinary(out.Bytes())
if err != nil {
t.Fatalf("DecodeTxsBinary(merged) error = %v", err)
}
if len(decoded) != 2 {
t.Fatalf("decoded len = %d, want 2", len(decoded))
}
if decoded[0].Block != tx1.Block || decoded[1].Block != tx3.Block {
t.Fatalf("decoded block order mismatch after skip")
}
if source.opens != 2 {
t.Fatalf("source.opens = %d, want 2", source.opens)
}
}
func mustPubKey(value string) solana.PublicKey {
return solana.MustPublicKeyFromBase58(value)
}
func mustTxBinary(t *testing.T, data []byte) *TxsBinary {
t.Helper()
var txsBinary TxsBinary
if err := txsBinary.UnmarshalBinary(data); err != nil {
t.Fatalf("UnmarshalBinary() error = %v", err)
}
return &txsBinary
}
type testTxsBinarySource struct {
data []byte
opens int
}
func (s *testTxsBinarySource) OpenTxsBinaryReader() (io.ReadCloser, error) {
s.opens++
return io.NopCloser(bytes.NewReader(s.data)), nil
}
func testBatchHeader(skip bool) []byte {
header := []byte("BHDR\x00")
if skip {
header[4] = 1
}
return header
}