Compare commits
78 Commits
v0.0.3
...
9454c3f6c7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9454c3f6c7 | ||
|
|
39bfeb085f | ||
|
|
10885d5e08 | ||
|
|
2406f6d087 | ||
|
|
8b608889cb | ||
|
|
8d4aad1932 | ||
|
|
5cd3a97d81 | ||
|
|
0a4aabc67f | ||
|
|
d46e8b651c | ||
|
|
43659ea4e4 | ||
|
|
6414e6a25f | ||
|
|
273e87b8ad | ||
|
|
bb858c643e | ||
|
|
a620df5837 | ||
|
|
36da96eeaf | ||
|
|
a765fafddd | ||
|
|
738e417167 | ||
|
|
51f1511c8f | ||
|
|
7dfe003e5b | ||
|
|
fe94888b14 | ||
|
|
1dd843c393 | ||
|
|
d2879efcc6 | ||
|
|
e761fd6f84 | ||
|
|
ab0e87a48a | ||
|
|
fb8d93f426 | ||
|
|
0cc843b370 | ||
|
|
d9a214b4b4 | ||
|
|
047b549d0f | ||
|
|
9327eab010 | ||
|
|
0ef57cf79a | ||
|
|
03030d817d | ||
|
|
401dca225a | ||
|
|
db8c8727f4 | ||
|
|
09de6ba649 | ||
|
|
7a82990770 | ||
|
|
e82bcb3c07 | ||
|
|
a74f769064 | ||
|
|
1e276e8bd2 | ||
|
|
eb2bde98ac | ||
| 66f0d247f5 | |||
| 879b7fefad | |||
| 149dfae378 | |||
| 8c4b43747c | |||
|
|
e9ba16766f | ||
|
|
cd1d681621 | ||
|
|
920c5ba25b | ||
|
|
3d447ef2e8 | ||
|
|
b0d4342fa2 | ||
|
|
972ddc7960 | ||
| bcd442195c | |||
| 0633707142 | |||
| 8e49f01054 | |||
|
|
62cc64a90a | ||
|
|
629ffe2ea7 | ||
|
|
56dac04a2a | ||
|
|
852ad4b382 | ||
|
|
3fdd4c4490 | ||
|
|
40012b531c | ||
|
|
0e30d6b35f | ||
|
|
70d91fdd30 | ||
|
|
9ece4aebb9 | ||
|
|
5da088ce13 | ||
|
|
0eb1628119 | ||
|
|
c25c856a47 | ||
|
|
b4906a2c20 | ||
|
|
21692c2ecc | ||
|
|
6b4cadb118 | ||
|
|
b76d2efc88 | ||
|
|
16b7461ac7 | ||
|
|
6bc84ce126 | ||
|
|
8128a325a9 | ||
|
|
dd76b04b19 | ||
| 91b70e23d6 | |||
|
|
dbfaa39432 | ||
|
|
78d323efd5 | ||
|
|
d22347ce8d | ||
|
|
9898554bf8 | ||
|
|
b44c7372d5 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/.idea
|
||||
156
SLIPPAGE_MAPPING.md
Normal file
156
SLIPPAGE_MAPPING.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Slippage Mapping
|
||||
|
||||
This document describes how `SlippageBps` is derived for each supported swap protocol in this repository.
|
||||
|
||||
## Unified Fields
|
||||
|
||||
Each parsed `Swap` may include these normalized fields:
|
||||
|
||||
- `SwapMode`
|
||||
- `FixedAmount`
|
||||
- `FixedAmountSide`
|
||||
- `FixedMint`
|
||||
- `LimitAmountType`
|
||||
- `LimitAmount`
|
||||
- `LimitAmountSide`
|
||||
- `LimitMint`
|
||||
- `ActualLimitAmount`
|
||||
- `ActualLimitAmountSide`
|
||||
- `SlippageBps`
|
||||
|
||||
## Internal Enum Mapping
|
||||
|
||||
These fields are stored internally as `uint8` enums and serialized as strings in JSON / debug output.
|
||||
|
||||
### `SwapMode`
|
||||
|
||||
| Raw Value | Name | Serialized Value |
|
||||
| --- | --- | --- |
|
||||
| `0` | `SwapModeUnknown` | `""` |
|
||||
| `1` | `SwapModeExactIn` | `"exact_in"` |
|
||||
| `2` | `SwapModeExactOut` | `"exact_out"` |
|
||||
|
||||
### `SwapAmountSide`
|
||||
|
||||
Used by:
|
||||
|
||||
- `FixedAmountSide`
|
||||
- `LimitAmountSide`
|
||||
- `ActualLimitAmountSide`
|
||||
|
||||
| Raw Value | Name | Serialized Value |
|
||||
| --- | --- | --- |
|
||||
| `0` | `SwapAmountSideUnknown` | `""` |
|
||||
| `1` | `SwapAmountSideBase` | `"base"` |
|
||||
| `2` | `SwapAmountSideQuote` | `"quote"` |
|
||||
|
||||
### `SwapLimitType`
|
||||
|
||||
Used by:
|
||||
|
||||
- `LimitAmountType`
|
||||
|
||||
| Raw Value | Name | Serialized Value |
|
||||
| --- | --- | --- |
|
||||
| `0` | `SwapLimitTypeUnknown` | `""` |
|
||||
| `1` | `SwapLimitTypeMinOut` | `"min_out"` |
|
||||
| `2` | `SwapLimitTypeMaxIn` | `"max_in"` |
|
||||
|
||||
## Calculation Rules
|
||||
|
||||
- `exact_in`
|
||||
- `SlippageBps = (actual_out - min_out) / actual_out * 10000`
|
||||
- `exact_out`
|
||||
- `SlippageBps = (max_in - actual_in) / max_in * 10000`
|
||||
|
||||
Interpretation:
|
||||
|
||||
- Positive: execution is better than the user limit
|
||||
- Zero: execution lands exactly on the user limit
|
||||
- `10000`: user limit is effectively unbounded on the constrained side (for example `min_out = 0`)
|
||||
- Negative raw headroom is clamped to `0` because successful-swap storage uses a non-negative bounded metric
|
||||
|
||||
This definition makes `SlippageBps` a bounded "remaining headroom to the user's limit" metric for successful swaps:
|
||||
|
||||
- `exact_in`: how much output headroom remained, measured against the realized output
|
||||
- `exact_out`: how much input headroom remained, measured against the allowed max input
|
||||
|
||||
## Protocol Mapping
|
||||
|
||||
| Protocol | Method Semantics | `SwapMode` | `FixedAmount` | `LimitAmountType` | `LimitAmount` | `ActualLimitAmount` |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| `Pump` | `buy` | `exact_out` | target token amount | `max_in` | max SOL in | actual SOL in |
|
||||
| `Pump` | `buy_exact_sol_in` | `exact_in` | SOL in | `min_out` | min token out | actual token out |
|
||||
| `Pump` | `sell` | `exact_in` | token in | `min_out` | min SOL out | actual SOL out |
|
||||
| `PumpAMM` | `buy` | `exact_out` | target base out | `max_in` | max quote in | actual quote in |
|
||||
| `PumpAMM` | `buy_exact_quote_in` | `exact_in` | quote in | `min_out` | min base out | actual base out |
|
||||
| `PumpAMM` | `sell` | `exact_in` | base in | `min_out` | min quote out | actual quote out |
|
||||
| `MeteoraDLMM` | `swap` / `swap2` / `swap_with_price_impact` | `exact_in` | `AmountIn` | `min_out` | instruction min out | event output |
|
||||
| `MeteoraDLMM` | `swap_exact_out` / `swap_exact_out2` | `exact_out` | `OutAmount` | `max_in` | `MaxInAmount` | event input |
|
||||
| `MeteoraPools` | `swap` | `exact_in` | `InAmount` | `min_out` | `MinimumOutAmount` | actual output side |
|
||||
| `MeteoraBondingCurve` | `swap` / `swap2` | `exact_in` | `AmountIn` | `min_out` | `MinimumAmountOut` | actual output side |
|
||||
| `MeteoraAmmV2` | `swap` / `swap2` exact-in or partial | `exact_in` | params input side | `min_out` | params output threshold | actual output side |
|
||||
| `MeteoraAmmV2` | `swap` / `swap2` exact-out | `exact_out` | params target output | `max_in` | params max input | actual input side |
|
||||
| `RaydiumLaunchLab` | `*_ExactIn` | `exact_in` | `Amount` | `min_out` | `OtherAmountThreshold` | actual output side |
|
||||
| `RaydiumLaunchLab` | `*_ExactOut` | `exact_out` | `Amount` | `max_in` | `OtherAmountThreshold` | actual input side |
|
||||
| `RaydiumCPMM` | `swap_base_input` | `exact_in` | `AmountIn` | `min_out` | `MinimumAmountOut` | actual output side |
|
||||
| `RaydiumCPMM` | `swap_base_output` | `exact_out` | `AmountOut` | `max_in` | `MaxAmountIn` | actual input side |
|
||||
| `RaydiumCLMM` | `swap` / `swap_v2` | `exact_in` or `exact_out` | `amount` | `min_out` or `max_in` | `other_amount_threshold` | opposite-side actual amount |
|
||||
| `RaydiumV4` | `swap_base_in` / `swap_base_in_v2` | `exact_in` | `amount_in` | `min_out` | `minimum_amount_out` | actual output side |
|
||||
| `RaydiumV4` | `swap_base_out` / `swap_base_out_v2` | `exact_out` | `amount_out` | `max_in` | `max_amount_in` | actual input side |
|
||||
| `OrcaWhirlpool` | `swap` / `swap_v2` | `exact_in` or `exact_out` | `amount` | `min_out` or `max_in` | `other_amount_threshold` | opposite-side actual amount |
|
||||
| `OrcaWhirlpool` | `two_hop_swap` / `two_hop_swap_v2` | route-level | route specified amount | `min_out` or `max_in` | route threshold | route final output or total input |
|
||||
|
||||
## Notes
|
||||
|
||||
- `Pump` quote side is normalized to `wSOL` in the slippage fields, even when legacy `Swap.QuoteMint` is not populated.
|
||||
- `OrcaWhirlpool` two-hop instructions use route-level slippage. The normalized slippage fields are attached to the first returned swap entry.
|
||||
- `MeteoraAmmV2` uses `SwapMode.ExactIn`, `SwapMode.PartialFill`, and `SwapMode.ExactOut`. `PartialFill` is treated like exact-in for slippage purposes because it still uses a minimum-output threshold.
|
||||
|
||||
## DAMM v2 Verification
|
||||
|
||||
The `MeteoraAmmV2` mapping has been checked against the program IDL for `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG`.
|
||||
|
||||
- `swap`
|
||||
- instruction arg type: `SwapParameters`
|
||||
- fields: `amountIn`, `minimumAmountOut`
|
||||
- semantics: exact-in
|
||||
- `swap2`:
|
||||
- instruction / event arg type: `SwapParameters2`
|
||||
- `amount0`: "When it's exact in, partial fill, this will be amount_in. When it's exact out, this will be amount_out"
|
||||
- `amount1`: "When it's exact in, partial fill, this will be minimum_amount_out. When it's exact out, this will be maximum_amount_in"
|
||||
- `swapMode`: `ExactIn`, `PartialFill`, `ExactOut`
|
||||
|
||||
The downloaded JSON IDL references `SwapMode` in the field docs but does not inline the enum body itself. In this repository, the raw `swapMode` values are interpreted consistently as:
|
||||
|
||||
- `0 = ExactIn`
|
||||
- `1 = PartialFill`
|
||||
- `2 = ExactOut`
|
||||
|
||||
That means the parser mapping is:
|
||||
|
||||
- `swap2` + `ExactIn` / `PartialFill`
|
||||
- `FixedAmount = amount0`
|
||||
- `LimitAmount = amount1`
|
||||
- `LimitAmountType = min_out`
|
||||
- `swap2` + `ExactOut`
|
||||
- `FixedAmount = amount0`
|
||||
- `LimitAmount = amount1`
|
||||
- `LimitAmountType = max_in`
|
||||
|
||||
## Source Files
|
||||
|
||||
- `Swap` normalized fields: `tx.go`
|
||||
- Shared slippage mapping helpers: `swap_amounts.go`
|
||||
- Protocol parsers:
|
||||
- `pump.go`
|
||||
- `pumpamm.go`
|
||||
- `metaoradlmm.go`
|
||||
- `metaorapool.go`
|
||||
- `meteora_bonding_curve.go`
|
||||
- `meteoradamm.go`
|
||||
- `raydiumlaunchlab.go`
|
||||
- `raydiumcpmm.go`
|
||||
- `raydiumclmm.go`
|
||||
- `raydiumv4.go`
|
||||
- `orcawhirpool.go`
|
||||
@@ -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
120
chainlink.go
Normal 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
|
||||
}
|
||||
368
checking.go
368
checking.go
@@ -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
|
||||
}
|
||||
594
cmd/analyze_rawtx_binary_size/main.go
Normal file
594
cmd/analyze_rawtx_binary_size/main.go
Normal file
@@ -0,0 +1,594 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
pump_parser "github.com/thloyi/pump-parser"
|
||||
)
|
||||
|
||||
type sizeStats struct {
|
||||
total uint64
|
||||
items map[string]uint64
|
||||
}
|
||||
|
||||
type txInnerDataStat struct {
|
||||
TxOrdinal int
|
||||
BlockIndex int
|
||||
IndexWithinBlock uint32
|
||||
Slot uint64
|
||||
Bytes uint64
|
||||
InstructionCount int
|
||||
}
|
||||
|
||||
func newSizeStats() *sizeStats {
|
||||
return &sizeStats{items: make(map[string]uint64)}
|
||||
}
|
||||
|
||||
func (s *sizeStats) add(name string, n uint64) {
|
||||
s.items[name] += n
|
||||
s.total += n
|
||||
}
|
||||
|
||||
func main() {
|
||||
filePath := flag.String("file", "testdata/rawtx-binary/rawtx-blocks-414696178-414696182.prbs", "path to RawTxBlocksBinary .prbs file")
|
||||
flag.Parse()
|
||||
|
||||
raw, err := os.ReadFile(*filePath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "read file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var blocks pump_parser.RawTxBlocksBinary
|
||||
if err := blocks.UnmarshalBinary(raw); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "decode rawtx blocks binary: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stats := analyzeRawTxBlocksBinary(&blocks)
|
||||
if stats.total != uint64(len(raw)) {
|
||||
fmt.Fprintf(os.Stderr, "size accounting mismatch: accounted=%d file=%d\n", stats.total, len(raw))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
printReport(*filePath, len(raw), &blocks, stats)
|
||||
fmt.Println()
|
||||
printInnerInstructionDataDistribution(&blocks)
|
||||
fmt.Println()
|
||||
printBalanceAnalysis(&blocks)
|
||||
}
|
||||
|
||||
func analyzeRawTxBlocksBinary(blocks *pump_parser.RawTxBlocksBinary) *sizeStats {
|
||||
stats := newSizeStats()
|
||||
stats.add("file.magic", 4)
|
||||
stats.add("file.schema_version", 2)
|
||||
stats.add("address_table.count", 4)
|
||||
stats.add("address_table.pubkeys", uint64(len(blocks.AddressTable))*32)
|
||||
stats.add("blocks.count", 4)
|
||||
stats.add("blocks.block_time", uint64(len(blocks.BlockTimes))*8)
|
||||
stats.add("blocks.tx_count", uint64(len(blocks.BlockTxCounts))*4)
|
||||
stats.add("txs.total_count", 4)
|
||||
|
||||
for i := range blocks.Txs {
|
||||
addTx(stats, &blocks.Txs[i])
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
func addTx(stats *sizeStats, tx *pump_parser.RawTxBinary) {
|
||||
stats.add("tx.index_within_block", 4)
|
||||
stats.add("tx.slot", 8)
|
||||
stats.add("tx.version", 1)
|
||||
stats.add("tx.account_key_count", 4)
|
||||
stats.add("tx.account_list.count", 4)
|
||||
stats.add("tx.account_list.refs", uint64(len(tx.AccountList))*4)
|
||||
|
||||
addMeta(stats, &tx.Meta)
|
||||
addTransaction(stats, &tx.Transaction)
|
||||
}
|
||||
|
||||
func addMeta(stats *sizeStats, meta *pump_parser.RawTxMetaBinary) {
|
||||
addErr(stats, meta.Err)
|
||||
stats.add("meta.fee", 8)
|
||||
addInnerInstructions(stats, meta.InnerInstructions)
|
||||
addLamportBalances(stats, meta.PreBalances, meta.PostBalances)
|
||||
addTokenBalances(stats, "meta.token_balances", meta.TokenBalances)
|
||||
stats.add("meta.compute_units_consumed", 8)
|
||||
}
|
||||
|
||||
func addErr(stats *sizeStats, errValue *pump_parser.TransactionParsedError) {
|
||||
stats.add("meta.err.present", 1)
|
||||
if errValue == nil {
|
||||
return
|
||||
}
|
||||
stats.add("meta.err.index", 1)
|
||||
stats.add("meta.err.variant", 4)
|
||||
stats.add("meta.err.enum", 4)
|
||||
stats.add("meta.err.custom_code", 4)
|
||||
}
|
||||
|
||||
func addTransaction(stats *sizeStats, tx *pump_parser.RawTxTransactionBinary) {
|
||||
stats.add("transaction.signature.present", 1)
|
||||
if tx.HasSignature {
|
||||
stats.add("transaction.signature.first", 64)
|
||||
}
|
||||
addHeader(stats)
|
||||
addInstructions(stats, "transaction.instructions", tx.Message.Instructions)
|
||||
addAddressTableLookups(stats, tx.Message.AddressTableLookups)
|
||||
}
|
||||
|
||||
func addHeader(stats *sizeStats) {
|
||||
stats.add("transaction.header.num_readonly_signed", 4)
|
||||
stats.add("transaction.header.num_readonly_unsigned", 4)
|
||||
stats.add("transaction.header.num_required_signatures", 4)
|
||||
}
|
||||
|
||||
func addInnerInstructions(stats *sizeStats, values []pump_parser.InnerInstructions) {
|
||||
stats.add("meta.inner_instructions.count", 4)
|
||||
for _, value := range values {
|
||||
stats.add("meta.inner_instructions.index", 4)
|
||||
addInstructions(stats, "meta.inner_instructions.instructions", value.Instructions)
|
||||
}
|
||||
}
|
||||
|
||||
func addInstructions(stats *sizeStats, prefix string, values []pump_parser.Instruction) {
|
||||
stats.add(prefix+".count", 4)
|
||||
for _, value := range values {
|
||||
stats.add(prefix+".program_id_index", 1)
|
||||
stats.add(prefix+".accounts.count", 4)
|
||||
stats.add(prefix+".accounts.refs", uint64(len(value.Accounts)))
|
||||
stats.add(prefix+".data.length", 4)
|
||||
stats.add(prefix+".data.bytes", uint64(len(value.Data)))
|
||||
stats.add(prefix+".stack_height.present", 1)
|
||||
if value.StackHeight != nil {
|
||||
stats.add(prefix+".stack_height.value", 4)
|
||||
}
|
||||
stats.add(prefix+".log_events.count", 4)
|
||||
for _, event := range value.LogEvents {
|
||||
stats.add(prefix+".log_events.length", 4)
|
||||
stats.add(prefix+".log_events.bytes", uint64(len(event)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addAddressTableLookups(stats *sizeStats, values []pump_parser.RawTxAddressTableLookupBinary) {
|
||||
stats.add("transaction.address_table_lookups.count", 4)
|
||||
for _, value := range values {
|
||||
stats.add("transaction.address_table_lookups.account_key", 4)
|
||||
stats.add("transaction.address_table_lookups.writable.count", 4)
|
||||
stats.add("transaction.address_table_lookups.writable.indexes", uint64(len(value.WritableIndexes)))
|
||||
stats.add("transaction.address_table_lookups.readonly.count", 4)
|
||||
stats.add("transaction.address_table_lookups.readonly.indexes", uint64(len(value.ReadonlyIndexes)))
|
||||
}
|
||||
}
|
||||
|
||||
func addUint64Slice(stats *sizeStats, prefix string, count int) {
|
||||
stats.add(prefix+".count", 4)
|
||||
stats.add(prefix+".values", uint64(count)*8)
|
||||
}
|
||||
|
||||
func addLamportBalances(stats *sizeStats, preBalances []uint64, postBalances []uint64) {
|
||||
stats.add("meta.pre_balances.count_uvarint", uint64(uvarintLen(uint64(len(preBalances)))))
|
||||
for _, value := range preBalances {
|
||||
stats.add("meta.pre_balances.value_uvarint", uint64(uvarintLen(value)))
|
||||
}
|
||||
n := len(preBalances)
|
||||
if len(postBalances) < n {
|
||||
n = len(postBalances)
|
||||
}
|
||||
changed := 0
|
||||
for i := 0; i < n; i++ {
|
||||
if preBalances[i] != postBalances[i] {
|
||||
changed++
|
||||
}
|
||||
}
|
||||
stats.add("meta.post_balance_changes.count_uvarint", uint64(uvarintLen(uint64(changed))))
|
||||
for i := 0; i < n; i++ {
|
||||
if preBalances[i] == postBalances[i] {
|
||||
continue
|
||||
}
|
||||
stats.add("meta.post_balance_changes.index_uvarint", uint64(uvarintLen(uint64(i))))
|
||||
stats.add("meta.post_balance_changes.delta_uvarint", uint64(zigzagDeltaUvarintLen(preBalances[i], postBalances[i])))
|
||||
}
|
||||
}
|
||||
|
||||
func addTokenBalances(stats *sizeStats, prefix string, values []pump_parser.RawTxTokenBalanceBinary) {
|
||||
stats.add(prefix+".count", 4)
|
||||
for _, value := range values {
|
||||
stats.add(prefix+".account_index", 1)
|
||||
stats.add(prefix+".mint_ref", 1)
|
||||
stats.add(prefix+".owner.present", 1)
|
||||
if value.HasOwnerAccount {
|
||||
stats.add(prefix+".owner_ref", 1)
|
||||
}
|
||||
stats.add(prefix+".program_id_ref", 1)
|
||||
stats.add(prefix+".decimals", 1)
|
||||
stats.add(prefix+".pre_amount.present", 1)
|
||||
if value.HasPreAmount {
|
||||
stats.add(prefix+".pre_amount.length", 1)
|
||||
stats.add(prefix+".pre_amount.bytes", uint64(uint256ByteLen(value.PreAmount)))
|
||||
}
|
||||
stats.add(prefix+".post_amount.present", 1)
|
||||
if value.HasPostAmount {
|
||||
stats.add(prefix+".post_amount.length", 1)
|
||||
stats.add(prefix+".post_amount.bytes", uint64(uint256ByteLen(value.PostAmount)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func uint256ByteLen(value string) int {
|
||||
if value == "" || value == "0" {
|
||||
return 0
|
||||
}
|
||||
amount, ok := new(big.Int).SetString(value, 10)
|
||||
if !ok || amount.Sign() <= 0 {
|
||||
return 0
|
||||
}
|
||||
return len(amount.Bytes())
|
||||
}
|
||||
|
||||
func printReport(filePath string, fileSize int, blocks *pump_parser.RawTxBlocksBinary, stats *sizeStats) {
|
||||
type row struct {
|
||||
name string
|
||||
bytes uint64
|
||||
}
|
||||
rows := make([]row, 0, len(stats.items))
|
||||
for name, bytes := range stats.items {
|
||||
rows = append(rows, row{name: name, bytes: bytes})
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
if rows[i].bytes == rows[j].bytes {
|
||||
return rows[i].name < rows[j].name
|
||||
}
|
||||
return rows[i].bytes > rows[j].bytes
|
||||
})
|
||||
|
||||
fmt.Printf("file=%s\n", filePath)
|
||||
fmt.Printf("bytes=%d\n", fileSize)
|
||||
fmt.Printf("schema_version=%d\n", blocks.SchemaVersion)
|
||||
fmt.Printf("blocks=%d\n", len(blocks.BlockTxCounts))
|
||||
fmt.Printf("txs=%d\n", len(blocks.Txs))
|
||||
fmt.Printf("address_table_entries=%d\n", len(blocks.AddressTable))
|
||||
fmt.Println()
|
||||
fmt.Printf("%-56s %12s %8s\n", "field", "bytes", "pct")
|
||||
fmt.Printf("%-56s %12s %8s\n", "-----", "-----", "---")
|
||||
for _, row := range rows {
|
||||
fmt.Printf("%-56s %12d %7.2f%%\n", row.name, row.bytes, float64(row.bytes)*100/float64(fileSize))
|
||||
}
|
||||
}
|
||||
|
||||
func printInnerInstructionDataDistribution(blocks *pump_parser.RawTxBlocksBinary) {
|
||||
stats := collectInnerInstructionDataStats(blocks)
|
||||
values := make([]uint64, 0, len(stats))
|
||||
var total uint64
|
||||
var nonZero int
|
||||
for _, stat := range stats {
|
||||
values = append(values, stat.Bytes)
|
||||
total += stat.Bytes
|
||||
if stat.Bytes > 0 {
|
||||
nonZero++
|
||||
}
|
||||
}
|
||||
sort.Slice(values, func(i, j int) bool { return values[i] < values[j] })
|
||||
|
||||
fmt.Println("inner_instruction_data_bytes_per_tx")
|
||||
fmt.Printf("txs=%d nonzero_txs=%d total_bytes=%d avg=%.2f\n", len(stats), nonZero, total, avg(total, len(stats)))
|
||||
if len(values) > 0 {
|
||||
fmt.Printf("min=%d p50=%d p75=%d p90=%d p95=%d p99=%d max=%d\n",
|
||||
values[0],
|
||||
percentile(values, 0.50),
|
||||
percentile(values, 0.75),
|
||||
percentile(values, 0.90),
|
||||
percentile(values, 0.95),
|
||||
percentile(values, 0.99),
|
||||
values[len(values)-1],
|
||||
)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("%-16s %8s %8s\n", "bucket", "txs", "bytes")
|
||||
fmt.Printf("%-16s %8s %8s\n", "------", "---", "-----")
|
||||
for _, bucket := range innerDataBuckets(stats) {
|
||||
fmt.Printf("%-16s %8d %8d\n", bucket.label, bucket.count, bucket.bytes)
|
||||
}
|
||||
|
||||
sort.Slice(stats, func(i, j int) bool {
|
||||
if stats[i].Bytes == stats[j].Bytes {
|
||||
return stats[i].TxOrdinal < stats[j].TxOrdinal
|
||||
}
|
||||
return stats[i].Bytes > stats[j].Bytes
|
||||
})
|
||||
fmt.Println()
|
||||
fmt.Printf("%-6s %-5s %-8s %-12s %-8s %-10s\n", "rank", "block", "tx_index", "slot", "bytes", "inner_ix")
|
||||
fmt.Printf("%-6s %-5s %-8s %-12s %-8s %-10s\n", "----", "-----", "--------", "----", "-----", "--------")
|
||||
limit := 20
|
||||
if len(stats) < limit {
|
||||
limit = len(stats)
|
||||
}
|
||||
for i := 0; i < limit; i++ {
|
||||
stat := stats[i]
|
||||
fmt.Printf("%-6d %-5d %-8d %-12d %-8d %-10d\n", i+1, stat.BlockIndex, stat.IndexWithinBlock, stat.Slot, stat.Bytes, stat.InstructionCount)
|
||||
}
|
||||
}
|
||||
|
||||
func collectInnerInstructionDataStats(blocks *pump_parser.RawTxBlocksBinary) []txInnerDataStat {
|
||||
out := make([]txInnerDataStat, 0, len(blocks.Txs))
|
||||
txOffset := 0
|
||||
for blockIndex, count := range blocks.BlockTxCounts {
|
||||
for i := uint32(0); i < count; i++ {
|
||||
tx := &blocks.Txs[txOffset]
|
||||
var bytes uint64
|
||||
var instructionCount int
|
||||
for _, inner := range tx.Meta.InnerInstructions {
|
||||
for _, instruction := range inner.Instructions {
|
||||
bytes += uint64(len(instruction.Data))
|
||||
instructionCount++
|
||||
}
|
||||
}
|
||||
out = append(out, txInnerDataStat{
|
||||
TxOrdinal: txOffset,
|
||||
BlockIndex: blockIndex,
|
||||
IndexWithinBlock: tx.IndexWithinBlock,
|
||||
Slot: tx.Slot,
|
||||
Bytes: bytes,
|
||||
InstructionCount: instructionCount,
|
||||
})
|
||||
txOffset++
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func percentile(values []uint64, p float64) uint64 {
|
||||
if len(values) == 0 {
|
||||
return 0
|
||||
}
|
||||
idx := int(float64(len(values)-1) * p)
|
||||
return values[idx]
|
||||
}
|
||||
|
||||
func avg(total uint64, count int) float64 {
|
||||
if count == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(total) / float64(count)
|
||||
}
|
||||
|
||||
type innerDataBucket struct {
|
||||
label string
|
||||
count int
|
||||
bytes uint64
|
||||
}
|
||||
|
||||
func innerDataBuckets(stats []txInnerDataStat) []innerDataBucket {
|
||||
buckets := []innerDataBucket{
|
||||
{label: "0"},
|
||||
{label: "1-63"},
|
||||
{label: "64-127"},
|
||||
{label: "128-255"},
|
||||
{label: "256-511"},
|
||||
{label: "512-1023"},
|
||||
{label: "1024-2047"},
|
||||
{label: "2048-4095"},
|
||||
{label: "4096+"},
|
||||
}
|
||||
for _, stat := range stats {
|
||||
index := 0
|
||||
switch {
|
||||
case stat.Bytes == 0:
|
||||
index = 0
|
||||
case stat.Bytes < 64:
|
||||
index = 1
|
||||
case stat.Bytes < 128:
|
||||
index = 2
|
||||
case stat.Bytes < 256:
|
||||
index = 3
|
||||
case stat.Bytes < 512:
|
||||
index = 4
|
||||
case stat.Bytes < 1024:
|
||||
index = 5
|
||||
case stat.Bytes < 2048:
|
||||
index = 6
|
||||
case stat.Bytes < 4096:
|
||||
index = 7
|
||||
default:
|
||||
index = 8
|
||||
}
|
||||
buckets[index].count++
|
||||
buckets[index].bytes += stat.Bytes
|
||||
}
|
||||
return buckets
|
||||
}
|
||||
|
||||
type balanceValueStats struct {
|
||||
name string
|
||||
count int
|
||||
unique int
|
||||
zeroCount int
|
||||
topValues []balanceTopValue
|
||||
fixedBytes uint64
|
||||
uvarintBytes uint64
|
||||
duplicateCount int
|
||||
}
|
||||
|
||||
type balanceTopValue struct {
|
||||
value uint64
|
||||
count int
|
||||
}
|
||||
|
||||
type balancePairStats struct {
|
||||
txCount int
|
||||
pairCount int
|
||||
lengthMismatchTxs int
|
||||
unchangedCount int
|
||||
changedCount int
|
||||
currentFixedValueBytes uint64
|
||||
bothUvarintBytes uint64
|
||||
preUvarintPostDelta uint64
|
||||
preUvarintChangedDeltas uint64
|
||||
}
|
||||
|
||||
func printBalanceAnalysis(blocks *pump_parser.RawTxBlocksBinary) {
|
||||
preValues := make([]uint64, 0)
|
||||
postValues := make([]uint64, 0)
|
||||
pairs := balancePairStats{}
|
||||
pairs.txCount = len(blocks.Txs)
|
||||
|
||||
for _, tx := range blocks.Txs {
|
||||
preValues = append(preValues, tx.Meta.PreBalances...)
|
||||
postValues = append(postValues, tx.Meta.PostBalances...)
|
||||
preLen := len(tx.Meta.PreBalances)
|
||||
postLen := len(tx.Meta.PostBalances)
|
||||
if preLen != postLen {
|
||||
pairs.lengthMismatchTxs++
|
||||
}
|
||||
n := preLen
|
||||
if postLen < n {
|
||||
n = postLen
|
||||
}
|
||||
pairs.currentFixedValueBytes += uint64(preLen+postLen) * 8
|
||||
pairs.preUvarintChangedDeltas += uint64(uvarintLen(uint64(n)))
|
||||
for i := 0; i < preLen; i++ {
|
||||
pairs.bothUvarintBytes += uint64(uvarintLen(tx.Meta.PreBalances[i]))
|
||||
pairs.preUvarintPostDelta += uint64(uvarintLen(tx.Meta.PreBalances[i]))
|
||||
pairs.preUvarintChangedDeltas += uint64(uvarintLen(tx.Meta.PreBalances[i]))
|
||||
}
|
||||
for i := 0; i < postLen; i++ {
|
||||
pairs.bothUvarintBytes += uint64(uvarintLen(tx.Meta.PostBalances[i]))
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
pre := tx.Meta.PreBalances[i]
|
||||
post := tx.Meta.PostBalances[i]
|
||||
pairs.pairCount++
|
||||
pairs.preUvarintPostDelta += uint64(zigzagDeltaUvarintLen(pre, post))
|
||||
if pre == post {
|
||||
pairs.unchangedCount++
|
||||
continue
|
||||
}
|
||||
pairs.changedCount++
|
||||
pairs.preUvarintChangedDeltas += uint64(uvarintLen(uint64(i)))
|
||||
pairs.preUvarintChangedDeltas += uint64(zigzagDeltaUvarintLen(pre, post))
|
||||
}
|
||||
}
|
||||
|
||||
preStats := collectBalanceValueStats("pre_balances", preValues)
|
||||
postStats := collectBalanceValueStats("post_balances", postValues)
|
||||
combined := append(append([]uint64(nil), preValues...), postValues...)
|
||||
combinedStats := collectBalanceValueStats("pre+post_balances", combined)
|
||||
|
||||
fmt.Println("balance_values_analysis")
|
||||
printBalanceValueStats(preStats)
|
||||
printBalanceValueStats(postStats)
|
||||
printBalanceValueStats(combinedStats)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("balance_encoding_estimates")
|
||||
fmt.Printf("txs=%d pairs=%d length_mismatch_txs=%d unchanged_pairs=%d changed_pairs=%d unchanged_pct=%.2f%%\n",
|
||||
pairs.txCount,
|
||||
pairs.pairCount,
|
||||
pairs.lengthMismatchTxs,
|
||||
pairs.unchangedCount,
|
||||
pairs.changedCount,
|
||||
float64(pairs.unchangedCount)*100/float64(maxInt(pairs.pairCount, 1)),
|
||||
)
|
||||
printEstimate("current_fixed_uint64_values", pairs.currentFixedValueBytes, pairs.currentFixedValueBytes)
|
||||
printEstimate("both_values_uvarint", pairs.bothUvarintBytes, pairs.currentFixedValueBytes)
|
||||
printEstimate("pre_uvarint_post_delta_each_index", pairs.preUvarintPostDelta, pairs.currentFixedValueBytes)
|
||||
printEstimate("pre_uvarint_post_changed_delta_pairs", pairs.preUvarintChangedDeltas, pairs.currentFixedValueBytes)
|
||||
}
|
||||
|
||||
func collectBalanceValueStats(name string, values []uint64) balanceValueStats {
|
||||
freq := make(map[uint64]int)
|
||||
var zeroCount int
|
||||
var uvarintBytes uint64
|
||||
for _, value := range values {
|
||||
freq[value]++
|
||||
if value == 0 {
|
||||
zeroCount++
|
||||
}
|
||||
uvarintBytes += uint64(uvarintLen(value))
|
||||
}
|
||||
top := make([]balanceTopValue, 0, len(freq))
|
||||
var duplicateCount int
|
||||
for value, count := range freq {
|
||||
top = append(top, balanceTopValue{value: value, count: count})
|
||||
if count > 1 {
|
||||
duplicateCount += count - 1
|
||||
}
|
||||
}
|
||||
sort.Slice(top, func(i, j int) bool {
|
||||
if top[i].count == top[j].count {
|
||||
return top[i].value < top[j].value
|
||||
}
|
||||
return top[i].count > top[j].count
|
||||
})
|
||||
if len(top) > 10 {
|
||||
top = top[:10]
|
||||
}
|
||||
return balanceValueStats{
|
||||
name: name,
|
||||
count: len(values),
|
||||
unique: len(freq),
|
||||
zeroCount: zeroCount,
|
||||
topValues: top,
|
||||
fixedBytes: uint64(len(values)) * 8,
|
||||
uvarintBytes: uvarintBytes,
|
||||
duplicateCount: duplicateCount,
|
||||
}
|
||||
}
|
||||
|
||||
func printBalanceValueStats(stats balanceValueStats) {
|
||||
fmt.Printf("%s: count=%d unique=%d duplicate_values=%d zero=%d zero_pct=%.2f%% fixed_bytes=%d uvarint_bytes=%d uvarint_saved=%.2f%%\n",
|
||||
stats.name,
|
||||
stats.count,
|
||||
stats.unique,
|
||||
stats.duplicateCount,
|
||||
stats.zeroCount,
|
||||
float64(stats.zeroCount)*100/float64(maxInt(stats.count, 1)),
|
||||
stats.fixedBytes,
|
||||
stats.uvarintBytes,
|
||||
savedPct(stats.fixedBytes, stats.uvarintBytes),
|
||||
)
|
||||
fmt.Printf("%-22s %-8s %-8s\n", "value", "count", "pct")
|
||||
for _, item := range stats.topValues {
|
||||
fmt.Printf("%-22d %-8d %7.2f%%\n", item.value, item.count, float64(item.count)*100/float64(maxInt(stats.count, 1)))
|
||||
}
|
||||
}
|
||||
|
||||
func printEstimate(name string, bytes uint64, baseline uint64) {
|
||||
fmt.Printf("%-38s %10d saved=%7.2f%%\n", name, bytes, savedPct(baseline, bytes))
|
||||
}
|
||||
|
||||
func savedPct(baseline uint64, value uint64) float64 {
|
||||
if baseline == 0 {
|
||||
return 0
|
||||
}
|
||||
return (float64(baseline) - float64(value)) * 100 / float64(baseline)
|
||||
}
|
||||
|
||||
func uvarintLen(value uint64) int {
|
||||
n := 1
|
||||
for value >= 0x80 {
|
||||
value >>= 7
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func zigzagDeltaUvarintLen(pre uint64, post uint64) int {
|
||||
if post >= pre {
|
||||
return uvarintLen((post - pre) << 1)
|
||||
}
|
||||
return uvarintLen(((pre - post) << 1) - 1)
|
||||
}
|
||||
|
||||
func maxInt(a int, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
302
cmd/collect_yellowstone_rawtx_binary/main.go
Normal file
302
cmd/collect_yellowstone_rawtx_binary/main.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
pump_parser "github.com/thloyi/pump-parser"
|
||||
pb "go.onsig.ai/onsig/yellowstone-proto"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
type collector struct {
|
||||
endpoint string
|
||||
xToken string
|
||||
plaintext bool
|
||||
|
||||
blocks map[uint64][]pump_parser.RawTx
|
||||
seen map[string]struct{}
|
||||
|
||||
totalUpdates uint64
|
||||
txUpdates uint64
|
||||
savedNonVote uint64
|
||||
duplicates uint64
|
||||
voteFiltered uint64
|
||||
convertErrs uint64
|
||||
reconnects uint64
|
||||
|
||||
firstSlot uint64
|
||||
lastSlot uint64
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
endpoint = flag.String("endpoint", "ams.rpc.orbitflare.com:10000", "Yellowstone gRPC endpoint")
|
||||
xToken = flag.String("x-token", os.Getenv("YELLOWSTONE_X_TOKEN"), "Yellowstone x-token; defaults to YELLOWSTONE_X_TOKEN")
|
||||
duration = flag.Duration("duration", 5*time.Minute, "collection duration")
|
||||
output = flag.String("output", "", "output .prbs file path")
|
||||
plaintext = flag.Bool("plaintext", true, "use plaintext gRPC instead of TLS")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *xToken == "" {
|
||||
exitf("missing -x-token or YELLOWSTONE_X_TOKEN")
|
||||
}
|
||||
if *duration <= 0 {
|
||||
exitf("-duration must be positive")
|
||||
}
|
||||
if *output == "" {
|
||||
*output = filepath.Join("testdata", "rawtx-binary", fmt.Sprintf("rawtx-yellowstone-%s.prbs", time.Now().Format("20060102-150405")))
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
ctx, cancel := context.WithTimeout(ctx, *duration)
|
||||
defer cancel()
|
||||
|
||||
c := &collector{
|
||||
endpoint: *endpoint,
|
||||
xToken: *xToken,
|
||||
plaintext: *plaintext,
|
||||
blocks: make(map[uint64][]pump_parser.RawTx),
|
||||
seen: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
started := time.Now()
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.run(ctx)
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
exitf("%v", err)
|
||||
}
|
||||
break loop
|
||||
case <-ctx.Done():
|
||||
if err := <-done; err != nil {
|
||||
exitf("%v", err)
|
||||
}
|
||||
break loop
|
||||
case <-ticker.C:
|
||||
c.printProgress(started)
|
||||
}
|
||||
}
|
||||
|
||||
encoded, decodedCount, err := encodeAndVerify(c.blocks)
|
||||
if err != nil {
|
||||
exitf("raw tx binary encode/decode: %v", err)
|
||||
}
|
||||
if decodedCount != int(c.savedNonVote) {
|
||||
exitf("decoded tx count mismatch: got=%d want=%d", decodedCount, c.savedNonVote)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(*output), 0o755); err != nil {
|
||||
exitf("mkdir output dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(*output, encoded, 0o644); err != nil {
|
||||
exitf("write output: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("output=%s\n", *output)
|
||||
fmt.Printf("duration=%s elapsed=%s\n", *duration, time.Since(started).Truncate(time.Second))
|
||||
fmt.Printf("updates=%d tx_updates=%d converted_nonvote=%d duplicate=%d vote_filtered=%d convert_err=%d reconnects=%d\n",
|
||||
c.totalUpdates, c.txUpdates, c.savedNonVote, c.duplicates, c.voteFiltered, c.convertErrs, c.reconnects)
|
||||
fmt.Printf("slots=%d first_slot=%d last_slot=%d decoded=%d\n", len(c.blocks), c.firstSlot, c.lastSlot, decodedCount)
|
||||
fmt.Printf("bytes=%d bytes_per_tx=%.2f\n", len(encoded), float64(len(encoded))/float64(max(int(c.savedNonVote), 1)))
|
||||
}
|
||||
|
||||
func (c *collector) run(ctx context.Context) error {
|
||||
for ctx.Err() == nil {
|
||||
if err := c.recvOnce(ctx); err != nil && ctx.Err() == nil {
|
||||
c.reconnects++
|
||||
fmt.Fprintf(os.Stderr, "stream_err reconnect=%d err=%v\n", c.reconnects, err)
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
if ctx.Err() == nil {
|
||||
c.reconnects++
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *collector) recvOnce(ctx context.Context) error {
|
||||
conn, err := grpc.NewClient(
|
||||
c.endpoint,
|
||||
c.transportOption(),
|
||||
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: 10 * time.Second,
|
||||
Timeout: time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}),
|
||||
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(64*1024*1024)),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{"x-token": c.xToken}))
|
||||
stream, err := pb.NewGeyserClient(conn).Subscribe(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vote := false
|
||||
subscription := &pb.SubscribeRequest{
|
||||
Transactions: map[string]*pb.SubscribeRequestFilterTransactions{
|
||||
"nonvote": {Vote: &vote},
|
||||
},
|
||||
}
|
||||
if err := stream.Send(subscription); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF || ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
c.totalUpdates++
|
||||
txn := resp.GetTransaction()
|
||||
if txn == nil {
|
||||
continue
|
||||
}
|
||||
c.txUpdates++
|
||||
|
||||
created := time.Now().Unix()
|
||||
if resp.GetCreatedAt() != nil {
|
||||
created = resp.GetCreatedAt().Seconds
|
||||
}
|
||||
rawTx, err := pump_parser.ConvertYellowstoneGrpcTransactionToSolanaTransaction(txn, created)
|
||||
if err != nil {
|
||||
c.convertErrs++
|
||||
continue
|
||||
}
|
||||
txHash := rawTx.TxHash()
|
||||
if txHash != "" {
|
||||
if _, exists := c.seen[txHash]; exists {
|
||||
c.duplicates++
|
||||
continue
|
||||
}
|
||||
c.seen[txHash] = struct{}{}
|
||||
}
|
||||
if c.firstSlot == 0 || rawTx.Slot < c.firstSlot {
|
||||
c.firstSlot = rawTx.Slot
|
||||
}
|
||||
if rawTx.Slot > c.lastSlot {
|
||||
c.lastSlot = rawTx.Slot
|
||||
}
|
||||
if isVoteTx(rawTx) {
|
||||
c.voteFiltered++
|
||||
continue
|
||||
}
|
||||
c.blocks[rawTx.Slot] = append(c.blocks[rawTx.Slot], *rawTx)
|
||||
c.savedNonVote++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *collector) transportOption() grpc.DialOption {
|
||||
if c.plaintext {
|
||||
return grpc.WithTransportCredentials(insecure.NewCredentials())
|
||||
}
|
||||
return grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, ""))
|
||||
}
|
||||
|
||||
func (c *collector) printProgress(started time.Time) {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"progress elapsed=%s updates=%d tx_updates=%d saved_nonvote=%d slots=%d duplicate=%d vote_filtered=%d convert_err=%d reconnects=%d\n",
|
||||
time.Since(started).Truncate(time.Second),
|
||||
c.totalUpdates,
|
||||
c.txUpdates,
|
||||
c.savedNonVote,
|
||||
len(c.blocks),
|
||||
c.duplicates,
|
||||
c.voteFiltered,
|
||||
c.convertErrs,
|
||||
c.reconnects,
|
||||
)
|
||||
}
|
||||
|
||||
func encodeAndVerify(blocks map[uint64][]pump_parser.RawTx) ([]byte, int, error) {
|
||||
slots := make([]uint64, 0, len(blocks))
|
||||
for slot := range blocks {
|
||||
slots = append(slots, slot)
|
||||
}
|
||||
sort.Slice(slots, func(i, j int) bool { return slots[i] < slots[j] })
|
||||
|
||||
ordered := make([][]pump_parser.RawTx, 0, len(slots))
|
||||
for _, slot := range slots {
|
||||
txs := blocks[slot]
|
||||
blockTime := int64(0)
|
||||
if len(txs) > 0 {
|
||||
blockTime = txs[0].BlockTime
|
||||
}
|
||||
for i := range txs {
|
||||
txs[i].BlockTime = blockTime
|
||||
}
|
||||
ordered = append(ordered, txs)
|
||||
}
|
||||
|
||||
encoded, err := pump_parser.EncodeRawTxBlocksBinary(ordered)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
decoded, err := pump_parser.DecodeRawTxBlocksBinary(encoded)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
decodedCount := 0
|
||||
for _, block := range decoded {
|
||||
decodedCount += len(block)
|
||||
}
|
||||
return encoded, decodedCount, nil
|
||||
}
|
||||
|
||||
func isVoteTx(tx *pump_parser.RawTx) bool {
|
||||
if tx == nil {
|
||||
return false
|
||||
}
|
||||
accountList := tx.GetAccountList()
|
||||
for _, instr := range tx.Transaction.Message.Instructions {
|
||||
if instr.ProgramIDIndex >= 0 && instr.ProgramIDIndex < len(accountList) && accountList[instr.ProgramIDIndex] == solana.VoteProgramID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func exitf(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
134
cmd/measure_tx_binary_block/main.go
Normal file
134
cmd/measure_tx_binary_block/main.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
pump_parser "github.com/thloyi/pump-parser"
|
||||
)
|
||||
|
||||
type blockResponse struct {
|
||||
Result blockResult `json:"result"`
|
||||
}
|
||||
|
||||
type blockResult struct {
|
||||
BlockTime *int64 `json:"blockTime"`
|
||||
Transactions []pump_parser.RawTx `json:"transactions"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
filePath = flag.String("file", "", "path to getBlock payload json")
|
||||
slot = flag.Uint64("slot", 0, "block slot")
|
||||
swapsOnly = flag.Bool("swaps-only", false, "only include transactions with swaps > 0")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *filePath == "" || *slot == 0 {
|
||||
fmt.Fprintln(os.Stderr, "usage: measure_tx_binary_block -file /path/block.json -slot 413539056")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(*filePath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "read file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var response blockResponse
|
||||
if err := json.Unmarshal(raw, &response); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unmarshal block payload: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var blockTime *uint64
|
||||
if response.Result.BlockTime != nil {
|
||||
bt := uint64(*response.Result.BlockTime)
|
||||
blockTime = &bt
|
||||
}
|
||||
|
||||
total := len(response.Result.Transactions)
|
||||
converted := 0
|
||||
parsed := 0
|
||||
convertFailures := 0
|
||||
parseFailures := 0
|
||||
encodeFailures := 0
|
||||
filteredOutNoSwaps := 0
|
||||
var totalRawTxBytes int
|
||||
var totalSingleEncoded int
|
||||
minSingleEncoded := -1
|
||||
maxSingleEncoded := 0
|
||||
|
||||
parsedTxs := make([]pump_parser.Tx, 0, total)
|
||||
for i, rawTx := range response.Result.Transactions {
|
||||
transactionJSON, err := json.Marshal(rawTx.Transaction)
|
||||
if err == nil {
|
||||
totalRawTxBytes += len(transactionJSON)
|
||||
}
|
||||
rawTx.BlockTime = 0
|
||||
if blockTime != nil {
|
||||
rawTx.BlockTime = int64(*blockTime)
|
||||
}
|
||||
rawTx.Slot = *slot
|
||||
rawTx.IndexWithinBlock = int64(i)
|
||||
converted++
|
||||
|
||||
tx, err := pump_parser.ParseRawTx(&rawTx)
|
||||
if err != nil {
|
||||
parseFailures++
|
||||
continue
|
||||
}
|
||||
if *swapsOnly && len(tx.Swaps) == 0 {
|
||||
filteredOutNoSwaps++
|
||||
continue
|
||||
}
|
||||
parsed++
|
||||
|
||||
encoded, err := pump_parser.EncodeTxBinary(tx)
|
||||
if err != nil {
|
||||
encodeFailures++
|
||||
continue
|
||||
}
|
||||
size := len(encoded)
|
||||
totalSingleEncoded += size
|
||||
if minSingleEncoded == -1 || size < minSingleEncoded {
|
||||
minSingleEncoded = size
|
||||
}
|
||||
if size > maxSingleEncoded {
|
||||
maxSingleEncoded = size
|
||||
}
|
||||
parsedTxs = append(parsedTxs, *tx)
|
||||
}
|
||||
|
||||
batchEncoded, err := pump_parser.EncodeTxsBinary(parsedTxs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "encode txs binary: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
avgSingleEncoded := 0
|
||||
if parsed > 0 {
|
||||
avgSingleEncoded = totalSingleEncoded / parsed
|
||||
}
|
||||
|
||||
fmt.Printf("block_slot=%d\n", *slot)
|
||||
fmt.Printf("payload_json_bytes=%d\n", len(raw))
|
||||
fmt.Printf("transactions_total=%d\n", total)
|
||||
fmt.Printf("transactions_converted=%d\n", converted)
|
||||
fmt.Printf("transactions_parsed=%d\n", parsed)
|
||||
fmt.Printf("transactions_filtered_no_swaps=%d\n", filteredOutNoSwaps)
|
||||
fmt.Printf("convert_failures=%d\n", convertFailures)
|
||||
fmt.Printf("parse_failures=%d\n", parseFailures)
|
||||
fmt.Printf("encode_failures=%d\n", encodeFailures)
|
||||
fmt.Printf("raw_tx_total_bytes=%d\n", totalRawTxBytes)
|
||||
fmt.Printf("single_txbinary_total_bytes=%d\n", totalSingleEncoded)
|
||||
fmt.Printf("single_txbinary_avg_bytes=%d\n", avgSingleEncoded)
|
||||
fmt.Printf("single_txbinary_min_bytes=%d\n", minSingleEncoded)
|
||||
fmt.Printf("single_txbinary_max_bytes=%d\n", maxSingleEncoded)
|
||||
fmt.Printf("batch_shared_table_bytes=%d\n", len(batchEncoded))
|
||||
if totalSingleEncoded > 0 {
|
||||
fmt.Printf("batch_vs_single_saved_bytes=%d\n", totalSingleEncoded-len(batchEncoded))
|
||||
}
|
||||
}
|
||||
133
cmd/rpc_parse/main.go
Normal file
133
cmd/rpc_parse/main.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
"github.com/gagliardetto/solana-go/rpc"
|
||||
pump_parser "github.com/thloyi/pump-parser"
|
||||
)
|
||||
|
||||
func main() {
|
||||
const rpcURL = "https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"
|
||||
txHash := os.Getenv("TX_HASH")
|
||||
if txHash == "" {
|
||||
txHash = "24wP3rk2ZfSVDB5YGyEQbhuy1jRXKZYkzXDRrDwwoKgzD6G4Kyyh4vmnir9ye98uLVKA5bBMj5Fq4cwgbDxp2Gie"
|
||||
}
|
||||
|
||||
if txHash == "" {
|
||||
fmt.Fprintln(os.Stderr, "txHash is empty; set it in cmd/rpc_parse/main.go")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sig, err := solana.SignatureFromBase58(txHash)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid txHash: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
client := rpc.New(rpcURL)
|
||||
maxSupportedVersion := uint64(0)
|
||||
out, err := client.GetTransaction(context.Background(), sig, &rpc.GetTransactionOpts{
|
||||
Encoding: solana.EncodingBase64,
|
||||
Commitment: rpc.CommitmentConfirmed,
|
||||
MaxSupportedTransactionVersion: &maxSupportedVersion,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "rpc getTransaction error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if out == nil || out.Transaction == nil || out.Meta == nil {
|
||||
fmt.Fprintln(os.Stderr, "rpc getTransaction returned empty response")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rawBinary := out.Transaction.GetBinary()
|
||||
if len(rawBinary) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "rpc getTransaction returned empty transaction data")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
txWithMeta := rpc.TransactionWithMeta{
|
||||
Slot: out.Slot,
|
||||
BlockTime: out.BlockTime,
|
||||
Transaction: rpc.DataBytesOrJSONFromBytes(rawBinary),
|
||||
Meta: out.Meta,
|
||||
Version: out.Version,
|
||||
}
|
||||
|
||||
var blockTime *uint64
|
||||
if out.BlockTime != nil {
|
||||
bt := uint64(*out.BlockTime)
|
||||
blockTime = &bt
|
||||
}
|
||||
|
||||
rawTx, err := pump_parser.FromRpcTransactionWithMeta(txWithMeta, blockTime, out.Slot, 0)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "convert rpc transaction error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pump_parser.EnableAllParsers()
|
||||
|
||||
parsed, err := pump_parser.ParseRawTx(rawTx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "parse raw tx error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(parsed.Swaps) == 0 {
|
||||
fmt.Println("no swaps parsed from tx")
|
||||
return
|
||||
}
|
||||
|
||||
for i, swap := range parsed.Swaps {
|
||||
fmt.Printf("swap[%d]\n", i)
|
||||
fmt.Printf(" program: %s\n", swap.Program)
|
||||
fmt.Printf(" event: %s\n", swap.Event)
|
||||
fmt.Printf(" pool: %s\n", swap.Pool)
|
||||
fmt.Printf(" user: %s\n", swap.User)
|
||||
fmt.Printf(" base_mint: %s (decimals=%d)\n", swap.BaseMint, swap.BaseMintDecimals)
|
||||
fmt.Printf(" quote_mint: %s (decimals=%d)\n", swap.QuoteMint, swap.QuoteMintDecimals)
|
||||
fmt.Printf(" base_amount: %s\n", swap.BaseAmount.String())
|
||||
fmt.Printf(" quote_amount: %s\n", swap.QuoteAmount.String())
|
||||
if swap.SwapMode != pump_parser.SwapModeUnknown {
|
||||
fmt.Printf(" swap_mode: %s\n", swap.SwapMode.String())
|
||||
fmt.Printf(" fixed_amount: %s\n", swap.FixedAmount.String())
|
||||
fmt.Printf(" fixed_amount_side: %s\n", swap.FixedAmountSide.String())
|
||||
fmt.Printf(" fixed_mint: %s\n", swap.FixedMint)
|
||||
fmt.Printf(" limit_amount_type: %s\n", swap.LimitAmountType.String())
|
||||
fmt.Printf(" limit_amount: %s\n", swap.LimitAmount.String())
|
||||
fmt.Printf(" limit_amount_side: %s\n", swap.LimitAmountSide.String())
|
||||
fmt.Printf(" limit_mint: %s\n", swap.LimitMint)
|
||||
fmt.Printf(" actual_limit_amount: %s\n", swap.ActualLimitAmount.String())
|
||||
fmt.Printf(" slippage_bps: %s\n", swap.SlippageBps.String())
|
||||
}
|
||||
if !swap.FeeAmount.IsZero() || swap.FeeSide != "" {
|
||||
fmt.Printf(" fee_amount: %s\n", swap.FeeAmount.String())
|
||||
fmt.Printf(" lp_fee_amount: %s\n", swap.LpFeeAmount.String())
|
||||
fmt.Printf(" fee_side: %s\n", swap.FeeSide)
|
||||
fmt.Printf(" fee_mint: %s (decimals=%d)\n", swap.FeeMint, swap.FeeMintDecimals)
|
||||
fmt.Printf(" fee_token_program: %s\n", swap.FeeTokenProgram)
|
||||
}
|
||||
fmt.Printf(" base_reserve: %s\n", swap.BaseReserve.String())
|
||||
fmt.Printf(" quote_reserve: %s\n", swap.QuoteReserve.String())
|
||||
fmt.Printf(" base_token_program: %s\n", swap.BaseTokenProgram)
|
||||
fmt.Printf(" quote_token_program: %s\n", swap.QuoteTokenProgram)
|
||||
fmt.Printf(" entry_contract: %s\n", swap.EntryContract)
|
||||
fmt.Printf(" user_base_balance: %s\n", swap.UserBaseBalance.String())
|
||||
fmt.Printf(" user_quote_balance: %s\n", swap.UserQuoteBalance.String())
|
||||
fmt.Printf(" active_bin_id: %d\n", swap.ActiveBinId)
|
||||
fmt.Printf(" start_bin_id: %d\n", swap.StartBinId)
|
||||
fmt.Printf(" end_bin_id: %d\n", swap.EndBinId)
|
||||
fmt.Printf(" remove_bp: %d\n", swap.RemoveBp)
|
||||
fmt.Printf(" position_account: %s\n", swap.PositionAccount)
|
||||
if swap.Mayhem {
|
||||
fmt.Printf(" mayhem: true\n")
|
||||
} else {
|
||||
fmt.Printf(" mayhem: false\n")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
21
codex.md
Normal file
21
codex.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Codex Notes
|
||||
|
||||
## Tx Binary enum synchronization
|
||||
|
||||
When adding or renaming transaction-facing enum values, keep the binary format definitions in sync.
|
||||
|
||||
This includes, but is not limited to:
|
||||
|
||||
- tx events
|
||||
- programs
|
||||
- platforms
|
||||
- MEV agents
|
||||
- swap modes, amount sides, limit types, and fee sides
|
||||
|
||||
Checklist:
|
||||
|
||||
1. Add the public constant in the normal source location, such as `enum.go`.
|
||||
2. Add any address mapping in `consts.go` when the enum is account-derived, such as platform or MEV agent detection.
|
||||
3. Append the new value to the matching versioned enum list in `tx_binary.go` under `txBinaryEnumTables`.
|
||||
4. Do not reorder or insert into existing `tx_binary.go` enum lists unless the binary version is intentionally changed; append to preserve existing numeric IDs.
|
||||
5. Add or update tx-binary round-trip coverage so encoding and decoding the new enum value is exercised.
|
||||
272
consts.go
272
consts.go
@@ -3,8 +3,17 @@ 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,
|
||||
solana.MustPublicKeyFromBase58("DXfkEGoo6WFsdL7x6gLZ7r6Hw2S6HrtrAQVPWYx2A1s9"): PlatformGMGN,
|
||||
solana.MustPublicKeyFromBase58("3t9EKmRiAUcQUYzTZpNojzeGP1KBAVEEbDNmy6wECQpK"): PlatformGMGN,
|
||||
solana.MustPublicKeyFromBase58("DymeoWc5WLNiQBaoLuxrxDnDRvLgGZ1QGsEoCAM7Jsrx"): PlatformGMGN,
|
||||
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,
|
||||
@@ -30,16 +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{
|
||||
@@ -72,6 +100,15 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
||||
solana.MustPublicKeyFromBase58("ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13"): MevAgent0slot,
|
||||
solana.MustPublicKeyFromBase58("6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK"): MevAgent0slot,
|
||||
solana.MustPublicKeyFromBase58("Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr"): MevAgent0slot,
|
||||
solana.MustPublicKeyFromBase58("axm2JQY1FKEktAwgXWqjGYkkWsWPfwKzgbnGVt5kiP4"): MevAgent0slot,
|
||||
solana.MustPublicKeyFromBase58("axm3PjbgwVrF6rnY2xLRMmWmLdDQGKfUYTEDtZ1haz7"): MevAgent0slot,
|
||||
solana.MustPublicKeyFromBase58("axmD4LFJopAcbRKCKsrrmovCZZzmKQCMEfs5qEXj8dG"): MevAgent0slot,
|
||||
solana.MustPublicKeyFromBase58("axmFmfqQwZGEUZeF3i3MqbRCDiGPfshtbdoBjk41k88"): MevAgent0slot,
|
||||
solana.MustPublicKeyFromBase58("axmMdWvgEnN3NFrxMfTqUURzj9NLhZL2DkHkWCdgiFV"): MevAgent0slot,
|
||||
solana.MustPublicKeyFromBase58("axmQTWU68qZ4fuG7zzkCXCBmxxeHVZrNrLkgxEFCbRv"): MevAgent0slot,
|
||||
solana.MustPublicKeyFromBase58("axmWxBPqgRmcBN2cV12quqaQzsk16SazVXq8397KFKu"): MevAgent0slot,
|
||||
solana.MustPublicKeyFromBase58("axmYVq9b1ABYqtyizMtyfJppPTPxZGXPLctB3hV6W5b"): MevAgent0slot,
|
||||
solana.MustPublicKeyFromBase58("axmhpocX3hU7nT7KtsLBzNBR1Ur3HtU22Q5P313FREY"): MevAgent0slot,
|
||||
solana.MustPublicKeyFromBase58("HWEoBxYs7ssKuudEjzjmpfJVX7Dvi7wescFsVx2L5yoY"): MevAgentBlocxRoute,
|
||||
solana.MustPublicKeyFromBase58("7ks326H4LbMVaUC8nW5FpC5EoAf5eK5pf4Dsx4HDQLpq"): MevAgentBlocxRoute,
|
||||
solana.MustPublicKeyFromBase58("95cfoy472fcQHaw4tPGBTKpn6ZQnfEPfBgDQx6gcRmRg"): MevAgentBlocxRoute,
|
||||
@@ -144,6 +181,224 @@ 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("soyasi59njacMUPvo3TM5paHjeK8pYSdovXgFi32gRt"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("soyasQYhJxv8uZgWDxhg72td6piAf7XTkoyWHtSATEz"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("soyastP66xyYC8XADXZjdMM5BAVGD2YRvz8dwtLsqb8"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("soyasvdgUJWYcUCzDxpmjUnNjH7KamXLXTzLwFvdVPE"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("soyasvxAunisNxaoRxkKGjNir7KmbwYnr37JmefkX9G"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("soyas5doVFUwH8s5zK8gEvCL5KR5ogDmf52LsrJEZ9h"): MevAgentSoyas,
|
||||
solana.MustPublicKeyFromBase58("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium,
|
||||
solana.MustPublicKeyFromBase58("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("astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk"): MevAgentAstralane,
|
||||
solana.MustPublicKeyFromBase58("astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK"): 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,
|
||||
solana.MustPublicKeyFromBase58("7HkiWXe5deJvzn4D6kgMUFCADwX9Z4DMrdjNSSxN6bPp"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zanrUknLZXzT9JPj968A7RfgCjp77Lx1W1xKRAtfshb"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zanHbk2UsiT3jKsKjD7UuEqS5Vgpmcd4pG9HycAAV8g"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zanNazKCXNRoKnPS9BBbFTELTpNwUDJxeKEb1JtZJer"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zan3gbFhXCjGLHhRe2vaXRDta5fCrYiYr3Dq4RLvpfU"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zan6WoE3DX5aK7FMQT1vSGsGrgZG1ngns3oCsFMnBHU"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zan8Nb9fB4zMDsuTRP9R65QZbc9v2Cjn5a4Hjwnj8D3"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zanJgoR7ALJAJ6ohoKs6aS9T71D9ZkNN9gYM5xUsi3u"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("zanAtYifQP7Bo6kStB97mJvzqSDW1toKNibWibwcKDd"): MevAgentZan,
|
||||
solana.MustPublicKeyFromBase58("GWT5UjDheZzoqinLavJkYvSRH5sakW8vDRdAgrUS5ZcS"): MevAgentTunneling,
|
||||
}
|
||||
|
||||
var entryContractAddresses = map[solana.PublicKey]string{
|
||||
@@ -177,4 +432,21 @@ var entryContractAddresses = map[solana.PublicKey]string{
|
||||
solana.MustPublicKeyFromBase58("NoVA1TmDUqksaj2hB1nayFkPysjJbFiU76dT4qPw2wm"): EntryContractNovaBotsProgram,
|
||||
solana.MustPublicKeyFromBase58("E6YoRP3adE5XYneSseLee15wJshDxCsmyD2WtLvAmfLi"): EntryContractTaggedSearcher,
|
||||
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")
|
||||
|
||||
68
enum.go
68
enum.go
@@ -1,16 +1,28 @@
|
||||
package pump_parser
|
||||
|
||||
const (
|
||||
MevAgentJito = "jito"
|
||||
MevAgent0slot = "0slot"
|
||||
MevAgentBlocxRoute = "blocxroute"
|
||||
MevAgentNozomi = "nozomi"
|
||||
MevAgentNextBlock = "nextblock"
|
||||
MevAgentHelius = "helius"
|
||||
MevAgentNode1 = "node1"
|
||||
MevAgentFlashBlock = "flashBlock"
|
||||
MevAgentUnknown = "unknown"
|
||||
MevAgentBlockRazor = "blockrazor"
|
||||
MevAgentJito = "jito"
|
||||
MevAgent0slot = "0slot"
|
||||
MevAgentBlocxRoute = "blocxroute"
|
||||
MevAgentNozomi = "nozomi"
|
||||
MevAgentNextBlock = "nextblock"
|
||||
MevAgentHelius = "helius"
|
||||
MevAgentNode1 = "node1"
|
||||
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"
|
||||
MevAgentZan = "zan"
|
||||
MevAgentTunneling = "tunneling"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -38,10 +50,19 @@ const (
|
||||
EntryContractNumeraire = "numeraire"
|
||||
EntryContractBloomRouter = "bloomRouter"
|
||||
EntryContractOKXAggregatorV2 = "oKXAggregatorV2"
|
||||
EntryContractOKXDexRouterV2 = "oKXDExRouterV2"
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -61,6 +82,8 @@ const (
|
||||
PlatformMoonshotMoney = "moonshot.money"
|
||||
PlatformMaestro = "maestro"
|
||||
PlatformBonkBot = "bonkbot"
|
||||
PlatformPadre = "padre"
|
||||
PlatformDexScreener = "dexscreener"
|
||||
|
||||
// used to flag transactions impersonating platform users
|
||||
PlatformFake = "fake"
|
||||
@@ -98,9 +121,24 @@ func GetConditionByProgram(program string) []string {
|
||||
}
|
||||
|
||||
const (
|
||||
TxEventAddLP = "add"
|
||||
TxEventRemoveLP = "remove"
|
||||
TxEventBuy = "buy"
|
||||
TxEventSell = "sell"
|
||||
TxEventBurn = "burn"
|
||||
TxEventAddLP = "add"
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package geyser
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
@@ -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
47
error_test.go
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
protoc:
|
||||
protoc \
|
||||
--go_out=./proto \
|
||||
--go_opt=paths=source_relative \
|
||||
--go-grpc_out=./proto \
|
||||
--go-grpc_opt=paths=source_relative \
|
||||
--proto_path ./proto/ ./proto/*.proto
|
||||
@@ -1,98 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
parser "github.com/thloyi/pump-parser"
|
||||
example "github.com/thloyi/pump-parser/example"
|
||||
"github.com/thloyi/pump-parser/example/geyser"
|
||||
)
|
||||
|
||||
func main() {
|
||||
//pool, err := ants.NewPool(100, ants.WithPreAlloc(true), ants.WithNonblocking(true))
|
||||
//if err != nil {
|
||||
// panic(err)
|
||||
//}
|
||||
//xt := tracker.NewTwitterTracker(nil) // Initialize Twitter tracker if needed
|
||||
// laserstream-mainnet-slc.helius-rpc.com:80
|
||||
|
||||
ch := make(chan geyser.SubscriptionMessage, 1)
|
||||
go geyser.RunLoopWithReConnect(context.Background(), "127.0.0.1:10001", parser.SolProgramPump, ch)
|
||||
// var tokenTxs = make(map[string]*types.Tx)
|
||||
// currentBlock := uint64(0)
|
||||
for msg := range ch {
|
||||
if msg.Tx == nil {
|
||||
block := msg.Block
|
||||
if block.Slot%100 == 0 {
|
||||
fmt.Printf("slot: %d, hash: %s, time: %s, height: %d, estimate delay second: %d\n",
|
||||
block.Slot, block.BlockHash, time.Unix(block.BlockTime, 0).Format("2006-01-02 15:04:05"), block.Height, msg.EstimateDelaySecond)
|
||||
}
|
||||
continue
|
||||
}
|
||||
ptx := msg.Tx
|
||||
//data, _ := json.Marshal(tx)
|
||||
//fmt.Println(string(data))
|
||||
//continue
|
||||
|
||||
//if tx.Token0Address != "HRHLDjqFBhNeyTXUuZQE9gTy5z2112qeQBS9U79NHyyp" {
|
||||
// continue
|
||||
//}
|
||||
//if tx.Program != parser.SolProgramPump {
|
||||
// continue
|
||||
//}
|
||||
//if currentBlock == ptx.Block {
|
||||
// continue
|
||||
//}
|
||||
|
||||
// 处理交易
|
||||
txErr, ok := ptx.Err.(*geyser.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
|
||||
}
|
||||
printed := false
|
||||
for _, tx := range txs {
|
||||
if tx.Program != parser.SolProgramPump {
|
||||
continue
|
||||
}
|
||||
if tx.Token1Amount.GreaterThanOrEqual(decimal.NewFromFloat(0.1)) || tx.Event != "buy" {
|
||||
continue
|
||||
}
|
||||
printed = true
|
||||
fmt.Printf("t: %s, block: %d, hash: %s, signer: %s, program: %s, event: %s, token1: %s, cuPrice: %s, mevAgent: %s, mevFee: %s, platform: %s, platformFee: %s, entryContract: %s, mayhem: %t\n",
|
||||
time.Now().Format(time.RFC3339Nano),
|
||||
tx.Block, tx.GetTxHash(), tx.Maker, tx.Program, tx.Event, tx.Token1Amount, tx.CUPrice, tx.MevAgent, tx.MevAgentFee, tx.Platform, tx.PlatformFee, tx.EntryContract, tx.Mayhem)
|
||||
//break
|
||||
}
|
||||
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
|
||||
//
|
||||
//if tx.Event == "create" {
|
||||
// if err := pool.Submit(func() {
|
||||
// now := time.Now()
|
||||
// xt.AddToken(tx.Token)
|
||||
// log.Printf("Add token %s, cost: %s %s %v %v", tx.Token.Address, time.Since(now), tx.Token.Twitter, xt.DuplicateCount(tx.Token.Address), xt.HasTwitter(tx.Token.Address))
|
||||
// }); err != nil {
|
||||
// fmt.Println(err)
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,272 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import public "solana-storage.proto";
|
||||
|
||||
option go_package = "github.com/rpcpool/yellowstone-grpc/examples/golang/proto";
|
||||
|
||||
package geyser;
|
||||
|
||||
service Geyser {
|
||||
rpc Subscribe(stream SubscribeRequest) returns (stream SubscribeUpdate) {}
|
||||
rpc Ping(PingRequest) returns (PongResponse) {}
|
||||
rpc GetLatestBlockhash(GetLatestBlockhashRequest) returns (GetLatestBlockhashResponse) {}
|
||||
rpc GetBlockHeight(GetBlockHeightRequest) returns (GetBlockHeightResponse) {}
|
||||
rpc GetSlot(GetSlotRequest) returns (GetSlotResponse) {}
|
||||
rpc IsBlockhashValid(IsBlockhashValidRequest) returns (IsBlockhashValidResponse) {}
|
||||
rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) {}
|
||||
}
|
||||
|
||||
enum CommitmentLevel {
|
||||
PROCESSED = 0;
|
||||
CONFIRMED = 1;
|
||||
FINALIZED = 2;
|
||||
}
|
||||
|
||||
enum SlotStatus {
|
||||
SLOT_PROCESSED = 0;
|
||||
SLOT_CONFIRMED = 1;
|
||||
SLOT_FINALIZED = 2;
|
||||
SLOT_FIRST_SHRED_RECEIVED = 3;
|
||||
SLOT_COMPLETED = 4;
|
||||
SLOT_CREATED_BANK = 5;
|
||||
SLOT_DEAD = 6;
|
||||
}
|
||||
|
||||
message SubscribeRequest {
|
||||
map<string, SubscribeRequestFilterAccounts> accounts = 1;
|
||||
map<string, SubscribeRequestFilterSlots> slots = 2;
|
||||
map<string, SubscribeRequestFilterTransactions> transactions = 3;
|
||||
map<string, SubscribeRequestFilterTransactions> transactions_status = 10;
|
||||
map<string, SubscribeRequestFilterBlocks> blocks = 4;
|
||||
map<string, SubscribeRequestFilterBlocksMeta> blocks_meta = 5;
|
||||
map<string, SubscribeRequestFilterEntry> entry = 8;
|
||||
optional CommitmentLevel commitment = 6;
|
||||
repeated SubscribeRequestAccountsDataSlice accounts_data_slice = 7;
|
||||
optional SubscribeRequestPing ping = 9;
|
||||
optional uint64 from_slot = 11;
|
||||
}
|
||||
|
||||
message SubscribeRequestFilterAccounts {
|
||||
repeated string account = 2;
|
||||
repeated string owner = 3;
|
||||
repeated SubscribeRequestFilterAccountsFilter filters = 4;
|
||||
optional bool nonempty_txn_signature = 5;
|
||||
}
|
||||
|
||||
message SubscribeRequestFilterAccountsFilter {
|
||||
oneof filter {
|
||||
SubscribeRequestFilterAccountsFilterMemcmp memcmp = 1;
|
||||
uint64 datasize = 2;
|
||||
bool token_account_state = 3;
|
||||
SubscribeRequestFilterAccountsFilterLamports lamports = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message SubscribeRequestFilterAccountsFilterMemcmp {
|
||||
uint64 offset = 1;
|
||||
oneof data {
|
||||
bytes bytes = 2;
|
||||
string base58 = 3;
|
||||
string base64 = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message SubscribeRequestFilterAccountsFilterLamports {
|
||||
oneof cmp {
|
||||
uint64 eq = 1;
|
||||
uint64 ne = 2;
|
||||
uint64 lt = 3;
|
||||
uint64 gt = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message SubscribeRequestFilterSlots {
|
||||
optional bool filter_by_commitment = 1;
|
||||
optional bool interslot_updates = 2;
|
||||
}
|
||||
|
||||
message SubscribeRequestFilterTransactions {
|
||||
optional bool vote = 1;
|
||||
optional bool failed = 2;
|
||||
optional string signature = 5;
|
||||
repeated string account_include = 3;
|
||||
repeated string account_exclude = 4;
|
||||
repeated string account_required = 6;
|
||||
}
|
||||
|
||||
message SubscribeRequestFilterBlocks {
|
||||
repeated string account_include = 1;
|
||||
optional bool include_transactions = 2;
|
||||
optional bool include_accounts = 3;
|
||||
optional bool include_entries = 4;
|
||||
}
|
||||
|
||||
message SubscribeRequestFilterBlocksMeta {}
|
||||
|
||||
message SubscribeRequestFilterEntry {}
|
||||
|
||||
message SubscribeRequestAccountsDataSlice {
|
||||
uint64 offset = 1;
|
||||
uint64 length = 2;
|
||||
}
|
||||
|
||||
message SubscribeRequestPing {
|
||||
int32 id = 1;
|
||||
}
|
||||
|
||||
message SubscribeUpdate {
|
||||
repeated string filters = 1;
|
||||
oneof update_oneof {
|
||||
SubscribeUpdateAccount account = 2;
|
||||
SubscribeUpdateSlot slot = 3;
|
||||
SubscribeUpdateTransaction transaction = 4;
|
||||
SubscribeUpdateTransactionStatus transaction_status = 10;
|
||||
SubscribeUpdateBlock block = 5;
|
||||
SubscribeUpdatePing ping = 6;
|
||||
SubscribeUpdatePong pong = 9;
|
||||
SubscribeUpdateBlockMeta block_meta = 7;
|
||||
SubscribeUpdateEntry entry = 8;
|
||||
}
|
||||
google.protobuf.Timestamp created_at = 11;
|
||||
}
|
||||
|
||||
message SubscribeUpdateAccount {
|
||||
SubscribeUpdateAccountInfo account = 1;
|
||||
uint64 slot = 2;
|
||||
bool is_startup = 3;
|
||||
}
|
||||
|
||||
message SubscribeUpdateAccountInfo {
|
||||
bytes pubkey = 1;
|
||||
uint64 lamports = 2;
|
||||
bytes owner = 3;
|
||||
bool executable = 4;
|
||||
uint64 rent_epoch = 5;
|
||||
bytes data = 6;
|
||||
uint64 write_version = 7;
|
||||
optional bytes txn_signature = 8;
|
||||
}
|
||||
|
||||
message SubscribeUpdateSlot {
|
||||
uint64 slot = 1;
|
||||
optional uint64 parent = 2;
|
||||
SlotStatus status = 3;
|
||||
optional string dead_error = 4;
|
||||
}
|
||||
|
||||
message SubscribeUpdateTransaction {
|
||||
SubscribeUpdateTransactionInfo transaction = 1;
|
||||
uint64 slot = 2;
|
||||
}
|
||||
|
||||
message SubscribeUpdateTransactionInfo {
|
||||
bytes signature = 1;
|
||||
bool is_vote = 2;
|
||||
solana.storage.ConfirmedBlock.Transaction transaction = 3;
|
||||
solana.storage.ConfirmedBlock.TransactionStatusMeta meta = 4;
|
||||
uint64 index = 5;
|
||||
}
|
||||
|
||||
message SubscribeUpdateTransactionStatus {
|
||||
uint64 slot = 1;
|
||||
bytes signature = 2;
|
||||
bool is_vote = 3;
|
||||
uint64 index = 4;
|
||||
solana.storage.ConfirmedBlock.TransactionError err = 5;
|
||||
}
|
||||
|
||||
message SubscribeUpdateBlock {
|
||||
uint64 slot = 1;
|
||||
string blockhash = 2;
|
||||
solana.storage.ConfirmedBlock.Rewards rewards = 3;
|
||||
solana.storage.ConfirmedBlock.UnixTimestamp block_time = 4;
|
||||
solana.storage.ConfirmedBlock.BlockHeight block_height = 5;
|
||||
uint64 parent_slot = 7;
|
||||
string parent_blockhash = 8;
|
||||
uint64 executed_transaction_count = 9;
|
||||
repeated SubscribeUpdateTransactionInfo transactions = 6;
|
||||
uint64 updated_account_count = 10;
|
||||
repeated SubscribeUpdateAccountInfo accounts = 11;
|
||||
uint64 entries_count = 12;
|
||||
repeated SubscribeUpdateEntry entries = 13;
|
||||
}
|
||||
|
||||
message SubscribeUpdateBlockMeta {
|
||||
uint64 slot = 1;
|
||||
string blockhash = 2;
|
||||
solana.storage.ConfirmedBlock.Rewards rewards = 3;
|
||||
solana.storage.ConfirmedBlock.UnixTimestamp block_time = 4;
|
||||
solana.storage.ConfirmedBlock.BlockHeight block_height = 5;
|
||||
uint64 parent_slot = 6;
|
||||
string parent_blockhash = 7;
|
||||
uint64 executed_transaction_count = 8;
|
||||
uint64 entries_count = 9;
|
||||
}
|
||||
|
||||
message SubscribeUpdateEntry {
|
||||
uint64 slot = 1;
|
||||
uint64 index = 2;
|
||||
uint64 num_hashes = 3;
|
||||
bytes hash = 4;
|
||||
uint64 executed_transaction_count = 5;
|
||||
uint64 starting_transaction_index = 6; // added in v1.18, for solana 1.17 value is always 0
|
||||
}
|
||||
|
||||
message SubscribeUpdatePing {}
|
||||
|
||||
message SubscribeUpdatePong {
|
||||
int32 id = 1;
|
||||
}
|
||||
|
||||
// non-streaming methods
|
||||
|
||||
message PingRequest {
|
||||
int32 count = 1;
|
||||
}
|
||||
|
||||
message PongResponse {
|
||||
int32 count = 1;
|
||||
}
|
||||
|
||||
message GetLatestBlockhashRequest {
|
||||
optional CommitmentLevel commitment = 1;
|
||||
}
|
||||
|
||||
message GetLatestBlockhashResponse {
|
||||
uint64 slot = 1;
|
||||
string blockhash = 2;
|
||||
uint64 last_valid_block_height = 3;
|
||||
}
|
||||
|
||||
message GetBlockHeightRequest {
|
||||
optional CommitmentLevel commitment = 1;
|
||||
}
|
||||
|
||||
message GetBlockHeightResponse {
|
||||
uint64 block_height = 1;
|
||||
}
|
||||
|
||||
message GetSlotRequest {
|
||||
optional CommitmentLevel commitment = 1;
|
||||
}
|
||||
|
||||
message GetSlotResponse {
|
||||
uint64 slot = 1;
|
||||
}
|
||||
|
||||
message GetVersionRequest {}
|
||||
|
||||
message GetVersionResponse {
|
||||
string version = 1;
|
||||
}
|
||||
|
||||
message IsBlockhashValidRequest {
|
||||
string blockhash = 1;
|
||||
optional CommitmentLevel commitment = 2;
|
||||
}
|
||||
|
||||
message IsBlockhashValidResponse {
|
||||
uint64 slot = 1;
|
||||
bool valid = 2;
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.3
|
||||
// source: geyser.proto
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
Geyser_Subscribe_FullMethodName = "/geyser.Geyser/Subscribe"
|
||||
Geyser_Ping_FullMethodName = "/geyser.Geyser/Ping"
|
||||
Geyser_GetLatestBlockhash_FullMethodName = "/geyser.Geyser/GetLatestBlockhash"
|
||||
Geyser_GetBlockHeight_FullMethodName = "/geyser.Geyser/GetBlockHeight"
|
||||
Geyser_GetSlot_FullMethodName = "/geyser.Geyser/GetSlot"
|
||||
Geyser_IsBlockhashValid_FullMethodName = "/geyser.Geyser/IsBlockhashValid"
|
||||
Geyser_GetVersion_FullMethodName = "/geyser.Geyser/GetVersion"
|
||||
)
|
||||
|
||||
// GeyserClient is the client API for Geyser service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type GeyserClient interface {
|
||||
Subscribe(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SubscribeRequest, SubscribeUpdate], error)
|
||||
Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PongResponse, error)
|
||||
GetLatestBlockhash(ctx context.Context, in *GetLatestBlockhashRequest, opts ...grpc.CallOption) (*GetLatestBlockhashResponse, error)
|
||||
GetBlockHeight(ctx context.Context, in *GetBlockHeightRequest, opts ...grpc.CallOption) (*GetBlockHeightResponse, error)
|
||||
GetSlot(ctx context.Context, in *GetSlotRequest, opts ...grpc.CallOption) (*GetSlotResponse, error)
|
||||
IsBlockhashValid(ctx context.Context, in *IsBlockhashValidRequest, opts ...grpc.CallOption) (*IsBlockhashValidResponse, error)
|
||||
GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error)
|
||||
}
|
||||
|
||||
type geyserClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewGeyserClient(cc grpc.ClientConnInterface) GeyserClient {
|
||||
return &geyserClient{cc}
|
||||
}
|
||||
|
||||
func (c *geyserClient) Subscribe(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SubscribeRequest, SubscribeUpdate], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &Geyser_ServiceDesc.Streams[0], Geyser_Subscribe_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[SubscribeRequest, SubscribeUpdate]{ClientStream: stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type Geyser_SubscribeClient = grpc.BidiStreamingClient[SubscribeRequest, SubscribeUpdate]
|
||||
|
||||
func (c *geyserClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PongResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(PongResponse)
|
||||
err := c.cc.Invoke(ctx, Geyser_Ping_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *geyserClient) GetLatestBlockhash(ctx context.Context, in *GetLatestBlockhashRequest, opts ...grpc.CallOption) (*GetLatestBlockhashResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetLatestBlockhashResponse)
|
||||
err := c.cc.Invoke(ctx, Geyser_GetLatestBlockhash_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *geyserClient) GetBlockHeight(ctx context.Context, in *GetBlockHeightRequest, opts ...grpc.CallOption) (*GetBlockHeightResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetBlockHeightResponse)
|
||||
err := c.cc.Invoke(ctx, Geyser_GetBlockHeight_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *geyserClient) GetSlot(ctx context.Context, in *GetSlotRequest, opts ...grpc.CallOption) (*GetSlotResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetSlotResponse)
|
||||
err := c.cc.Invoke(ctx, Geyser_GetSlot_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *geyserClient) IsBlockhashValid(ctx context.Context, in *IsBlockhashValidRequest, opts ...grpc.CallOption) (*IsBlockhashValidResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(IsBlockhashValidResponse)
|
||||
err := c.cc.Invoke(ctx, Geyser_IsBlockhashValid_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *geyserClient) GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetVersionResponse)
|
||||
err := c.cc.Invoke(ctx, Geyser_GetVersion_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GeyserServer is the server API for Geyser service.
|
||||
// All implementations must embed UnimplementedGeyserServer
|
||||
// for forward compatibility.
|
||||
type GeyserServer interface {
|
||||
Subscribe(grpc.BidiStreamingServer[SubscribeRequest, SubscribeUpdate]) error
|
||||
Ping(context.Context, *PingRequest) (*PongResponse, error)
|
||||
GetLatestBlockhash(context.Context, *GetLatestBlockhashRequest) (*GetLatestBlockhashResponse, error)
|
||||
GetBlockHeight(context.Context, *GetBlockHeightRequest) (*GetBlockHeightResponse, error)
|
||||
GetSlot(context.Context, *GetSlotRequest) (*GetSlotResponse, error)
|
||||
IsBlockhashValid(context.Context, *IsBlockhashValidRequest) (*IsBlockhashValidResponse, error)
|
||||
GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error)
|
||||
mustEmbedUnimplementedGeyserServer()
|
||||
}
|
||||
|
||||
// UnimplementedGeyserServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedGeyserServer struct{}
|
||||
|
||||
func (UnimplementedGeyserServer) Subscribe(grpc.BidiStreamingServer[SubscribeRequest, SubscribeUpdate]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method Subscribe not implemented")
|
||||
}
|
||||
func (UnimplementedGeyserServer) Ping(context.Context, *PingRequest) (*PongResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented")
|
||||
}
|
||||
func (UnimplementedGeyserServer) GetLatestBlockhash(context.Context, *GetLatestBlockhashRequest) (*GetLatestBlockhashResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetLatestBlockhash not implemented")
|
||||
}
|
||||
func (UnimplementedGeyserServer) GetBlockHeight(context.Context, *GetBlockHeightRequest) (*GetBlockHeightResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetBlockHeight not implemented")
|
||||
}
|
||||
func (UnimplementedGeyserServer) GetSlot(context.Context, *GetSlotRequest) (*GetSlotResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetSlot not implemented")
|
||||
}
|
||||
func (UnimplementedGeyserServer) IsBlockhashValid(context.Context, *IsBlockhashValidRequest) (*IsBlockhashValidResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method IsBlockhashValid not implemented")
|
||||
}
|
||||
func (UnimplementedGeyserServer) GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetVersion not implemented")
|
||||
}
|
||||
func (UnimplementedGeyserServer) mustEmbedUnimplementedGeyserServer() {}
|
||||
func (UnimplementedGeyserServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeGeyserServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to GeyserServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeGeyserServer interface {
|
||||
mustEmbedUnimplementedGeyserServer()
|
||||
}
|
||||
|
||||
func RegisterGeyserServer(s grpc.ServiceRegistrar, srv GeyserServer) {
|
||||
// If the following call pancis, it indicates UnimplementedGeyserServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&Geyser_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _Geyser_Subscribe_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(GeyserServer).Subscribe(&grpc.GenericServerStream[SubscribeRequest, SubscribeUpdate]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type Geyser_SubscribeServer = grpc.BidiStreamingServer[SubscribeRequest, SubscribeUpdate]
|
||||
|
||||
func _Geyser_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PingRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GeyserServer).Ping(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Geyser_Ping_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GeyserServer).Ping(ctx, req.(*PingRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Geyser_GetLatestBlockhash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetLatestBlockhashRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GeyserServer).GetLatestBlockhash(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Geyser_GetLatestBlockhash_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GeyserServer).GetLatestBlockhash(ctx, req.(*GetLatestBlockhashRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Geyser_GetBlockHeight_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetBlockHeightRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GeyserServer).GetBlockHeight(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Geyser_GetBlockHeight_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GeyserServer).GetBlockHeight(ctx, req.(*GetBlockHeightRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Geyser_GetSlot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetSlotRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GeyserServer).GetSlot(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Geyser_GetSlot_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GeyserServer).GetSlot(ctx, req.(*GetSlotRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Geyser_IsBlockhashValid_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(IsBlockhashValidRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GeyserServer).IsBlockhashValid(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Geyser_IsBlockhashValid_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GeyserServer).IsBlockhashValid(ctx, req.(*IsBlockhashValidRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Geyser_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetVersionRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GeyserServer).GetVersion(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Geyser_GetVersion_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GeyserServer).GetVersion(ctx, req.(*GetVersionRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Geyser_ServiceDesc is the grpc.ServiceDesc for Geyser service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var Geyser_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "geyser.Geyser",
|
||||
HandlerType: (*GeyserServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Ping",
|
||||
Handler: _Geyser_Ping_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetLatestBlockhash",
|
||||
Handler: _Geyser_GetLatestBlockhash_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetBlockHeight",
|
||||
Handler: _Geyser_GetBlockHeight_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetSlot",
|
||||
Handler: _Geyser_GetSlot_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "IsBlockhashValid",
|
||||
Handler: _Geyser_IsBlockhashValid_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetVersion",
|
||||
Handler: _Geyser_GetVersion_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "Subscribe",
|
||||
Handler: _Geyser_Subscribe_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "geyser.proto",
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,149 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package solana.storage.ConfirmedBlock;
|
||||
|
||||
option go_package = "github.com/rpcpool/yellowstone-grpc/examples/golang/proto";
|
||||
|
||||
message ConfirmedBlock {
|
||||
string previous_blockhash = 1;
|
||||
string blockhash = 2;
|
||||
uint64 parent_slot = 3;
|
||||
repeated ConfirmedTransaction transactions = 4;
|
||||
repeated Reward rewards = 5;
|
||||
UnixTimestamp block_time = 6;
|
||||
BlockHeight block_height = 7;
|
||||
NumPartitions num_partitions = 8;
|
||||
}
|
||||
|
||||
message ConfirmedTransaction {
|
||||
Transaction transaction = 1;
|
||||
TransactionStatusMeta meta = 2;
|
||||
}
|
||||
|
||||
message Transaction {
|
||||
repeated bytes signatures = 1;
|
||||
Message message = 2;
|
||||
}
|
||||
|
||||
message Message {
|
||||
MessageHeader header = 1;
|
||||
repeated bytes account_keys = 2;
|
||||
bytes recent_blockhash = 3;
|
||||
repeated CompiledInstruction instructions = 4;
|
||||
bool versioned = 5;
|
||||
repeated MessageAddressTableLookup address_table_lookups = 6;
|
||||
}
|
||||
|
||||
message MessageHeader {
|
||||
uint32 num_required_signatures = 1;
|
||||
uint32 num_readonly_signed_accounts = 2;
|
||||
uint32 num_readonly_unsigned_accounts = 3;
|
||||
}
|
||||
|
||||
message MessageAddressTableLookup {
|
||||
bytes account_key = 1;
|
||||
bytes writable_indexes = 2;
|
||||
bytes readonly_indexes = 3;
|
||||
}
|
||||
|
||||
message TransactionStatusMeta {
|
||||
TransactionError err = 1;
|
||||
uint64 fee = 2;
|
||||
repeated uint64 pre_balances = 3;
|
||||
repeated uint64 post_balances = 4;
|
||||
repeated InnerInstructions inner_instructions = 5;
|
||||
bool inner_instructions_none = 10;
|
||||
repeated string log_messages = 6;
|
||||
bool log_messages_none = 11;
|
||||
repeated TokenBalance pre_token_balances = 7;
|
||||
repeated TokenBalance post_token_balances = 8;
|
||||
repeated Reward rewards = 9;
|
||||
repeated bytes loaded_writable_addresses = 12;
|
||||
repeated bytes loaded_readonly_addresses = 13;
|
||||
ReturnData return_data = 14;
|
||||
bool return_data_none = 15;
|
||||
|
||||
// Sum of compute units consumed by all instructions.
|
||||
// Available since Solana v1.10.35 / v1.11.6.
|
||||
// Set to `None` for txs executed on earlier versions.
|
||||
optional uint64 compute_units_consumed = 16;
|
||||
}
|
||||
|
||||
message TransactionError {
|
||||
bytes err = 1;
|
||||
}
|
||||
|
||||
message InnerInstructions {
|
||||
uint32 index = 1;
|
||||
repeated InnerInstruction instructions = 2;
|
||||
}
|
||||
|
||||
message InnerInstruction {
|
||||
uint32 program_id_index = 1;
|
||||
bytes accounts = 2;
|
||||
bytes data = 3;
|
||||
|
||||
// Invocation stack height of an inner instruction.
|
||||
// Available since Solana v1.14.6
|
||||
// Set to `None` for txs executed on earlier versions.
|
||||
optional uint32 stack_height = 4;
|
||||
}
|
||||
|
||||
message CompiledInstruction {
|
||||
uint32 program_id_index = 1;
|
||||
bytes accounts = 2;
|
||||
bytes data = 3;
|
||||
}
|
||||
|
||||
message TokenBalance {
|
||||
uint32 account_index = 1;
|
||||
string mint = 2;
|
||||
UiTokenAmount ui_token_amount = 3;
|
||||
string owner = 4;
|
||||
string program_id = 5;
|
||||
}
|
||||
|
||||
message UiTokenAmount {
|
||||
double ui_amount = 1;
|
||||
uint32 decimals = 2;
|
||||
string amount = 3;
|
||||
string ui_amount_string = 4;
|
||||
}
|
||||
|
||||
message ReturnData {
|
||||
bytes program_id = 1;
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
enum RewardType {
|
||||
Unspecified = 0;
|
||||
Fee = 1;
|
||||
Rent = 2;
|
||||
Staking = 3;
|
||||
Voting = 4;
|
||||
}
|
||||
|
||||
message Reward {
|
||||
string pubkey = 1;
|
||||
int64 lamports = 2;
|
||||
uint64 post_balance = 3;
|
||||
RewardType reward_type = 4;
|
||||
string commission = 5;
|
||||
}
|
||||
|
||||
message Rewards {
|
||||
repeated Reward rewards = 1;
|
||||
NumPartitions num_partitions = 2;
|
||||
}
|
||||
|
||||
message UnixTimestamp {
|
||||
int64 timestamp = 1;
|
||||
}
|
||||
|
||||
message BlockHeight {
|
||||
uint64 block_height = 1;
|
||||
}
|
||||
|
||||
message NumPartitions {
|
||||
uint64 num_partitions = 1;
|
||||
}
|
||||
@@ -1,525 +0,0 @@
|
||||
package geyser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
solana2 "github.com/gagliardetto/solana-go"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
types "github.com/thloyi/pump-parser"
|
||||
pb "github.com/thloyi/pump-parser/example/geyser/proto"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
HandleMessage(rawTx *types.RawTx)
|
||||
}
|
||||
|
||||
var kacp = keepalive.ClientParameters{
|
||||
Time: 10 * time.Second, // send pings every 10 seconds if there is no activity
|
||||
Timeout: time.Second, // wait 1 second for ping ack before considering the connection dead
|
||||
PermitWithoutStream: true, // send pings even without active streams
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ch chan SubscriptionMessage
|
||||
endpoint string
|
||||
conn *grpc.ClientConn
|
||||
ctx context.Context
|
||||
lastReceiveTime time.Time
|
||||
backoffFactor float64
|
||||
|
||||
subscription *pb.SubscribeRequest
|
||||
subStatus bool
|
||||
|
||||
leastBlock BlockInfo
|
||||
|
||||
firstMessage bool
|
||||
|
||||
handler Handler
|
||||
}
|
||||
|
||||
func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client {
|
||||
var subscription pb.SubscribeRequest
|
||||
|
||||
var failed = false
|
||||
var vote = false
|
||||
subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions)
|
||||
subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{
|
||||
Failed: &failed,
|
||||
Vote: &vote,
|
||||
}
|
||||
|
||||
subscription.Transactions["transactions_sub"].AccountInclude = []string{
|
||||
"pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
|
||||
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
|
||||
}
|
||||
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
|
||||
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
|
||||
|
||||
c := &Client{
|
||||
backoffFactor: 1.5,
|
||||
ch: ch,
|
||||
endpoint: endpoint,
|
||||
lastReceiveTime: time.Now(),
|
||||
subStatus: false,
|
||||
subscription: &subscription,
|
||||
}
|
||||
c.handler = NewPumpHandler(func(tx *types.Tx) {
|
||||
c.sendTx(tx)
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Client {
|
||||
var subscription pb.SubscribeRequest
|
||||
|
||||
var failed = false
|
||||
var vote = false
|
||||
subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions)
|
||||
subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{
|
||||
Failed: &failed,
|
||||
Vote: &vote,
|
||||
}
|
||||
|
||||
subscription.Transactions["transactions_sub"].AccountInclude = []string{
|
||||
"LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", //LaunchLab
|
||||
"CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C", //CPMM
|
||||
//"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8", //V4
|
||||
}
|
||||
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
|
||||
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
|
||||
|
||||
c := &Client{
|
||||
backoffFactor: 1.5,
|
||||
ch: ch,
|
||||
endpoint: endpoint,
|
||||
lastReceiveTime: time.Now(),
|
||||
subStatus: false,
|
||||
subscription: &subscription,
|
||||
}
|
||||
c.handler = NewPumpHandler(func(tx *types.Tx) {
|
||||
c.sendTx(tx)
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
func RunLoopWithReConnect(ctx context.Context, endpoint, program string, ch chan SubscriptionMessage) {
|
||||
var client *Client
|
||||
if program == types.SolProgramRaydiumLaunchLab {
|
||||
client = NewClientWithLaunchLab(endpoint, ch)
|
||||
} else {
|
||||
client = NewClientWithPumpSwap(endpoint, ch)
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("Context done, exiting loop")
|
||||
return
|
||||
default:
|
||||
}
|
||||
err := client.Connect(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Failed to connect: %v", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
// should not reach here, because Connect will block
|
||||
panic("geyser already connected, waiting for messages...")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SetSubscribe(subscription *pb.SubscribeRequest) {
|
||||
c.subscription = subscription
|
||||
}
|
||||
|
||||
func (c *Client) Connect(ctx context.Context) error {
|
||||
|
||||
c.ctx = ctx
|
||||
if c.conn == nil {
|
||||
// 连接到 geyser
|
||||
conn, err := c.grpcConnect(c.endpoint, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.conn = conn
|
||||
}
|
||||
|
||||
if c.subStatus {
|
||||
return nil // 已经订阅了
|
||||
}
|
||||
// 订阅交易
|
||||
err := c.grpcSubscribe(ctx, c.conn)
|
||||
if err != nil {
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
}
|
||||
c.conn = nil
|
||||
c.subStatus = false
|
||||
log.Printf("Failed to subscribe: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) grpcConnect(address string, plaintext bool) (*grpc.ClientConn, error) {
|
||||
var opts []grpc.DialOption
|
||||
if plaintext {
|
||||
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
} else {
|
||||
pool, _ := x509.SystemCertPool()
|
||||
creds := credentials.NewClientTLSFromCert(pool, "")
|
||||
opts = append(opts, grpc.WithTransportCredentials(creds))
|
||||
}
|
||||
|
||||
opts = append(opts, grpc.WithKeepaliveParams(kacp))
|
||||
|
||||
log.Println("Starting grpc client, connecting to", address)
|
||||
conn, err := grpc.NewClient(address, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to dial: %v", err)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Client) grpcSubscribe(ctx context.Context, conn *grpc.ClientConn) error {
|
||||
var err error
|
||||
client := pb.NewGeyserClient(conn)
|
||||
|
||||
//subscription.Transactions["transactions_sub"].AccountExclude = transactionsAccountsExclude
|
||||
|
||||
subscriptionJson, err := json.Marshal(c.subscription)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal subscription request: %v", subscriptionJson)
|
||||
return err
|
||||
}
|
||||
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"})
|
||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||
|
||||
stream, err := client.Subscribe(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = stream.Send(c.subscription)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.subStatus = true
|
||||
c.firstMessage = true
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
|
||||
if err == io.EOF {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error occurred in receiving update: %s", err)
|
||||
}
|
||||
|
||||
txn := resp.GetTransaction()
|
||||
if txn == nil {
|
||||
blockMeta := resp.GetBlockMeta()
|
||||
if blockMeta != nil && c.ch != nil {
|
||||
c.sendBlock(blockMeta)
|
||||
}
|
||||
continue
|
||||
}
|
||||
rawTx, err := ConvertYellowstoneGrpcTransactionToSolanaTransaction(txn, resp.GetCreatedAt().Seconds)
|
||||
if err != nil {
|
||||
log.Printf("Failed to convert transaction: %v", err)
|
||||
continue
|
||||
}
|
||||
c.handler.HandleMessage(rawTx)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) computeDelay(slot uint64) int64 {
|
||||
if c.leastBlock.Slot == 0 {
|
||||
return 0
|
||||
}
|
||||
if slot < c.leastBlock.Slot {
|
||||
return 0
|
||||
}
|
||||
delay := time.Now().Unix() - c.leastBlock.BlockTime - (4 * int64(slot-c.leastBlock.Slot) / 10)
|
||||
return delay
|
||||
}
|
||||
|
||||
func (c *Client) sendTx(t *types.Tx) {
|
||||
c.ch <- SubscriptionMessage{
|
||||
Reconnect: c.firstMessage,
|
||||
EstimateDelaySecond: c.computeDelay(t.Block),
|
||||
Block: nil,
|
||||
Tx: t,
|
||||
}
|
||||
c.firstMessage = false
|
||||
}
|
||||
|
||||
func (c *Client) sendBlock(blockMeta *pb.SubscribeUpdateBlockMeta) {
|
||||
c.leastBlock.Slot = blockMeta.GetSlot()
|
||||
c.leastBlock.BlockTime = blockMeta.GetBlockTime().Timestamp
|
||||
c.leastBlock.BlockHash = blockMeta.Blockhash
|
||||
c.leastBlock.Height = blockMeta.BlockHeight.BlockHeight
|
||||
c.ch <- SubscriptionMessage{
|
||||
EstimateDelaySecond: time.Now().Unix() - blockMeta.GetBlockTime().Timestamp,
|
||||
Reconnect: c.firstMessage,
|
||||
Block: &BlockInfo{
|
||||
Slot: c.leastBlock.Slot,
|
||||
BlockTime: c.leastBlock.BlockTime,
|
||||
BlockHash: c.leastBlock.BlockHash,
|
||||
Height: c.leastBlock.Height,
|
||||
},
|
||||
Tx: nil,
|
||||
}
|
||||
c.firstMessage = false
|
||||
}
|
||||
|
||||
func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateTransaction, created int64) (*types.RawTx, error) {
|
||||
sTx := &types.RawTx{
|
||||
BlockTime: created,
|
||||
Slot: y.Slot,
|
||||
IndexWithinBlock: int64(y.Transaction.Index),
|
||||
Meta: types.Meta{
|
||||
Err: nil,
|
||||
Fee: 0,
|
||||
InnerInstructions: nil,
|
||||
LoadedAddresses: types.LoadedAddresses{},
|
||||
LogMessages: nil,
|
||||
PostBalances: nil,
|
||||
PostTokenBalances: nil,
|
||||
PreBalances: nil,
|
||||
PreTokenBalances: nil,
|
||||
Rewards: nil,
|
||||
},
|
||||
//Transaction: types.Transaction{
|
||||
// Message: types.Message{
|
||||
// AccountKeys: nil,
|
||||
// AddressTableLookups: nil,
|
||||
// Header: types.Header{},
|
||||
// Instructions: nil,
|
||||
// RecentBlockHash: "",
|
||||
// },
|
||||
// Signatures: nil,
|
||||
//},
|
||||
//Version: nil,
|
||||
}
|
||||
meta := y.Transaction.GetMeta()
|
||||
yTx := y.Transaction.Transaction
|
||||
|
||||
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 = meta.Err.GetErr()
|
||||
}
|
||||
sTx.Meta.Fee = meta.Fee
|
||||
//sTx.Meta.InnerInstructions = meta.InnerInstructions
|
||||
|
||||
for _, innerInstr := range meta.InnerInstructions {
|
||||
var instrs []types.Instruction
|
||||
for _, instr := range innerInstr.Instructions {
|
||||
instrs = append(instrs, types.Instruction{
|
||||
ProgramIDIndex: int(instr.ProgramIdIndex),
|
||||
Accounts: func() []int {
|
||||
var out []int
|
||||
for i := range instr.Accounts {
|
||||
out = append(out, int(instr.Accounts[i]))
|
||||
}
|
||||
return out
|
||||
}(),
|
||||
Data: instr.Data,
|
||||
})
|
||||
}
|
||||
sTx.Meta.InnerInstructions = append(sTx.Meta.InnerInstructions, types.InnerInstructions{
|
||||
Index: int(innerInstr.Index),
|
||||
Instructions: instrs,
|
||||
})
|
||||
}
|
||||
sTx.Meta.LogMessages = meta.LogMessages
|
||||
sTx.Meta.PostBalances = meta.PostBalances
|
||||
sTx.Meta.PostTokenBalances = grpcTokenBalance(meta.PostTokenBalances)
|
||||
sTx.Meta.PreBalances = meta.PreBalances
|
||||
sTx.Meta.PreTokenBalances = grpcTokenBalance(meta.PreTokenBalances)
|
||||
sTx.Meta.Rewards = nil
|
||||
sTx.Meta.LoadedAddresses.Readonly = byteSlicesToKeySlices(meta.LoadedReadonlyAddresses)
|
||||
sTx.Meta.LoadedAddresses.Writable = byteSlicesToKeySlices(meta.LoadedWritableAddresses)
|
||||
|
||||
// copy signatures
|
||||
for i := range yTx.Signatures {
|
||||
sTx.Transaction.Signatures = append(sTx.Transaction.Signatures, solana2.SignatureFromBytes(yTx.Signatures[i]))
|
||||
}
|
||||
// copy message
|
||||
sTx.Transaction.Message = types.Message{
|
||||
RecentBlockHash: solana2.HashFromBytes(yTx.Message.RecentBlockhash).String(),
|
||||
}
|
||||
// copy message.AccountKeys
|
||||
//stopAt := len(yTx.Message.AccountKeys) - sTx.Message.NumLookups()
|
||||
stopAt := len(yTx.Message.AccountKeys)
|
||||
for accIndex, acc := range yTx.Message.AccountKeys {
|
||||
sTx.Transaction.Message.AccountKeys = append(sTx.Transaction.Message.AccountKeys, solana2.PublicKeyFromBytes(acc))
|
||||
if accIndex == stopAt-1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// copy message.Header
|
||||
sTx.Transaction.Message.Header = types.Header{
|
||||
NumRequiredSignatures: int(yTx.Message.Header.NumRequiredSignatures),
|
||||
NumReadonlySignedAccounts: int(yTx.Message.Header.NumReadonlySignedAccounts),
|
||||
NumReadonlyUnsignedAccounts: int(yTx.Message.Header.NumReadonlyUnsignedAccounts),
|
||||
}
|
||||
|
||||
// copy message.versioned
|
||||
if yTx.Message.Versioned {
|
||||
sTx.Version = solana2.MessageVersionV0
|
||||
} else {
|
||||
sTx.Version = solana2.MessageVersionLegacy
|
||||
}
|
||||
|
||||
// copy address table lookups
|
||||
{
|
||||
tables := map[solana2.PublicKey]solana2.PublicKeySlice{}
|
||||
writable := byteSlicesToKeySlices(meta.LoadedWritableAddresses)
|
||||
readonly := byteSlicesToKeySlices(meta.LoadedReadonlyAddresses)
|
||||
for _, addr := range yTx.Message.AddressTableLookups {
|
||||
sTx.Transaction.Message.AddressTableLookups = append(sTx.Transaction.Message.AddressTableLookups, solana2.MessageAddressTableLookup{
|
||||
AccountKey: solana2.PublicKeyFromBytes(addr.AccountKey),
|
||||
WritableIndexes: addr.WritableIndexes,
|
||||
ReadonlyIndexes: addr.ReadonlyIndexes,
|
||||
})
|
||||
numTakeWritable := len(addr.WritableIndexes)
|
||||
numTakeReadonly := len(addr.ReadonlyIndexes)
|
||||
tableKey := solana2.PublicKeyFromBytes(addr.AccountKey)
|
||||
{
|
||||
// now need to rebuild the address table taking into account the indexes, and put the keys into the tables
|
||||
maxIndex := 0
|
||||
for _, indexB := range addr.WritableIndexes {
|
||||
index := int(indexB)
|
||||
if index > maxIndex {
|
||||
maxIndex = index
|
||||
}
|
||||
}
|
||||
for _, indexB := range addr.ReadonlyIndexes {
|
||||
index := int(indexB)
|
||||
if index > maxIndex {
|
||||
maxIndex = index
|
||||
}
|
||||
}
|
||||
tables[tableKey] = make([]solana2.PublicKey, maxIndex+1)
|
||||
}
|
||||
if numTakeWritable > 0 {
|
||||
writableForTable := writable[:numTakeWritable]
|
||||
for i, indexB := range addr.WritableIndexes {
|
||||
index := int(indexB)
|
||||
tables[tableKey][index] = writableForTable[i]
|
||||
}
|
||||
writable = writable[numTakeWritable:]
|
||||
}
|
||||
if numTakeReadonly > 0 {
|
||||
readableForTable := readonly[:numTakeReadonly]
|
||||
for i, indexB := range addr.ReadonlyIndexes {
|
||||
index := int(indexB)
|
||||
tables[tableKey][index] = readableForTable[i]
|
||||
}
|
||||
readonly = readonly[numTakeReadonly:]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// copy instructions
|
||||
for _, instr := range yTx.Message.Instructions {
|
||||
sTx.Transaction.Message.Instructions = append(sTx.Transaction.Message.Instructions, types.Instruction{
|
||||
ProgramIDIndex: int(instr.ProgramIdIndex),
|
||||
Accounts: func() []int {
|
||||
var out []int
|
||||
for i := range instr.Accounts {
|
||||
out = append(out, int(instr.Accounts[i]))
|
||||
}
|
||||
return out
|
||||
}(),
|
||||
Data: instr.Data,
|
||||
})
|
||||
}
|
||||
|
||||
// resolve the lookups
|
||||
//{
|
||||
// if sTx.Transaction.Message.IsVersioned() {
|
||||
// // only versioned transactions have address table lookups
|
||||
// err := sTx.Transaction.Message.ResolveLookups()
|
||||
// if err != nil {
|
||||
// return sTx, fmt.Errorf("failed to resolve lookups: %w", err)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
return sTx, nil
|
||||
}
|
||||
|
||||
func byteSlicesToKeySlices(keys [][]byte) []solana2.PublicKey {
|
||||
var out []solana2.PublicKey
|
||||
for _, key := range keys {
|
||||
var k solana2.PublicKey
|
||||
copy(k[:], key)
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func grpcTokenBalance(src []*pb.TokenBalance) []types.TokenBalance {
|
||||
out := make([]types.TokenBalance, len(src))
|
||||
for i, tb := range src {
|
||||
var (
|
||||
mintAccount solana2.PublicKey
|
||||
ownerAccount solana2.PublicKey
|
||||
programIDAccount solana2.PublicKey
|
||||
)
|
||||
|
||||
if tb.Mint != "" {
|
||||
mintAccount, _ = solana2.PublicKeyFromBase58(tb.Mint)
|
||||
}
|
||||
if tb.Owner != "" {
|
||||
ownerAccount, _ = solana2.PublicKeyFromBase58(tb.Owner)
|
||||
}
|
||||
if tb.ProgramId != "" {
|
||||
programIDAccount, _ = solana2.PublicKeyFromBase58(tb.ProgramId)
|
||||
}
|
||||
|
||||
out[i] = types.TokenBalance{
|
||||
AccountIndex: int(tb.AccountIndex),
|
||||
MintAccount: mintAccount,
|
||||
OwnerAccount: &ownerAccount,
|
||||
ProgramIDAccount: programIDAccount,
|
||||
Mint: tb.Mint,
|
||||
Owner: tb.Owner,
|
||||
ProgramID: tb.ProgramId,
|
||||
UITokenAmount: types.UITokenAmount{
|
||||
Amount: tb.UiTokenAmount.Amount,
|
||||
Decimals: uint64(tb.UiTokenAmount.Decimals),
|
||||
UIAmount: tb.UiTokenAmount.UiAmount,
|
||||
UIAmountString: tb.UiTokenAmount.UiAmountString,
|
||||
},
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
31
globals.go
31
globals.go
@@ -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) {
|
||||
|
||||
48
go.mod
48
go.mod
@@ -8,21 +8,29 @@ require (
|
||||
github.com/jackc/pgtype v1.14.4
|
||||
github.com/mr-tron/base58 v1.2.0
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
google.golang.org/grpc v1.77.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
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.0.0 // indirect
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // 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
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
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.13.6 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -30,17 +38,19 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect
|
||||
github.com/streamingfast/logging v0.0.0-20250918142248-ac5a1e292845 // indirect
|
||||
go.mongodb.org/mongo-driver v1.12.2 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/ratelimit v0.2.0 // indirect
|
||||
go.uber.org/zap v1.21.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/term v0.36.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
github.com/streamingfast/logging v0.0.0-20251216203033-fdad0a00f1ca // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.6 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
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
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
112
go.sum
112
go.sum
@@ -1,13 +1,12 @@
|
||||
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=
|
||||
github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE=
|
||||
github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
@@ -17,8 +16,8 @@ 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=
|
||||
github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c=
|
||||
github.com/gagliardetto/solana-go v1.14.0 h1:3WfAi70jOOjAJ0deFMjdhFYlLXATF4tOQXsDNWJtOLw=
|
||||
@@ -35,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=
|
||||
@@ -74,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=
|
||||
@@ -89,16 +87,24 @@ 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=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -114,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=
|
||||
@@ -132,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=
|
||||
@@ -140,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=
|
||||
@@ -157,8 +157,8 @@ 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=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
@@ -168,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=
|
||||
@@ -176,15 +175,13 @@ 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=
|
||||
go.onsig.ai/onsig/yellowstone-proto v1.0.0/go.mod h1:e5dlYkNpgNHtiXFwPmPDZRf4PrCsgNaSoA8iG4rfiKA=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
@@ -201,23 +198,27 @@ 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/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
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/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/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA=
|
||||
go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=
|
||||
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.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=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@@ -229,11 +230,10 @@ 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=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
@@ -251,12 +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=
|
||||
@@ -278,30 +280,29 @@ 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=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
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/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/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
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=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
@@ -318,17 +319,16 @@ 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/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
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/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.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.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=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
@@ -341,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=
|
||||
|
||||
85
internal/example/cmd/main.go
Normal file
85
internal/example/cmd/main.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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 {
|
||||
// panic(err)
|
||||
//}
|
||||
//xt := tracker.NewTwitterTracker(nil) // Initialize Twitter tracker if needed
|
||||
// laserstream-mainnet-slc.helius-rpc.com:80
|
||||
|
||||
ch := make(chan example.SubscriptionMessage, 1)
|
||||
go example.RunLoopWithReConnect(context.Background(), "ams.rpc.orbitflare.com:10000", "ORBIT-EPUZGQ-177605-508881", parser.SolProgramPump, ch)
|
||||
// var tokenTxs = make(map[string]*types.Tx)
|
||||
// currentBlock := uint64(0)
|
||||
for msg := range ch {
|
||||
if msg.Tx == nil {
|
||||
block := msg.Block
|
||||
if block.Slot%100 == 0 {
|
||||
fmt.Printf("slot: %d, hash: %s, time: %s, height: %d, estimate delay second: %d\n",
|
||||
block.Slot, block.BlockHash, time.Unix(block.BlockTime, 0).Format("2006-01-02 15:04:05"), block.Height, msg.EstimateDelaySecond)
|
||||
}
|
||||
continue
|
||||
}
|
||||
ptx := msg.Tx
|
||||
// fmt.Println("consume", ptx.ComputeUnitsConsumed, "limit", ptx.CuLimit, "hash", ptx.GetTxHash())
|
||||
//data, _ := json.Marshal(tx)
|
||||
//fmt.Println(string(data))
|
||||
//continue
|
||||
|
||||
//if tx.Token0Address != "HRHLDjqFBhNeyTXUuZQE9gTy5z2112qeQBS9U79NHyyp" {
|
||||
// continue
|
||||
//}
|
||||
//if currentBlock == ptx.Block {
|
||||
// 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)))
|
||||
}
|
||||
if swap.SlippageBps.Equal(decimal.Zero) && (swap.Event == "buy" || swap.Event == "sell") {
|
||||
fmt.Printf("zero success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s, fix: %s, limit: %s, \n", time.Now().Format("2006-01-02 15:04:05"), swap.Program, swap.Event, ptx.Block, ptx.GetTxHash(),
|
||||
swap.BaseAmount.Div(decimal.NewFromInt(1e6)), swap.QuoteAmount.Div(decimal.NewFromInt(1e9)), swap.FixedAmount.String(), swap.LimitAmount.String())
|
||||
}
|
||||
}
|
||||
if len(ptx.Swaps) > 0 {
|
||||
_, err := parser.EncodeTxBinary(ptx)
|
||||
if err != nil {
|
||||
fmt.Printf("success tx : %s, , block: %d, tx: %s, err: %s \n", time.Now().Format("2006-01-02 15:04:05"), ptx.Block, ptx.GetTxHash(), err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// currentBlock = ptx.Block
|
||||
//
|
||||
//if tx.Event == "create" {
|
||||
// if err := pool.Submit(func() {
|
||||
// now := time.Now()
|
||||
// xt.AddToken(tx.Token)
|
||||
// log.Printf("Add token %s, cost: %s %s %v %v", tx.Token.Address, time.Since(now), tx.Token.Twitter, xt.DuplicateCount(tx.Token.Address), xt.HasTwitter(tx.Token.Address))
|
||||
// }); err != nil {
|
||||
// fmt.Println(err)
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package geyser
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/thloyi/pump-parser"
|
||||
@@ -1,9 +1,8 @@
|
||||
package geyser
|
||||
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
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ type Tx struct {
|
||||
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"`
|
||||
|
||||
Mayhem bool
|
||||
Mayhem bool
|
||||
Cashback bool `json:"is_cashback_coin"`
|
||||
}
|
||||
|
||||
func (tx *Tx) GetTxHash() string {
|
||||
@@ -70,9 +71,9 @@ func FromTx(tx *parser.Tx) []*Tx {
|
||||
for i, s := range tx.Swaps {
|
||||
var newTx *Tx
|
||||
platform, platformFee := tx.CheckPlatform(s)
|
||||
token0Program := s.BaseTokenProgram
|
||||
token0Address := s.BaseMint
|
||||
token0Decimals := s.BaseMintDecimals
|
||||
//token0Program := s.BaseTokenProgram
|
||||
//token0Address := s.BaseMint
|
||||
//token0Decimals := s.BaseMintDecimals
|
||||
if s.Program == "Pump" {
|
||||
quoteMint := s.QuoteMint
|
||||
// 有些数据里 quote 会给 SystemProgram,统一转成 WSOL
|
||||
@@ -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) {
|
||||
@@ -130,9 +132,9 @@ func FromTx(tx *parser.Tx) []*Tx {
|
||||
} else if s.Event == "sell" {
|
||||
eventName = "buy"
|
||||
}
|
||||
token0Program = s.QuoteTokenProgram
|
||||
token0Address = s.QuoteMint
|
||||
token0Decimals = s.QuoteMintDecimals
|
||||
//token0Program = s.QuoteTokenProgram
|
||||
//token0Address = s.QuoteMint
|
||||
//token0Decimals = s.QuoteMintDecimals
|
||||
newTx = &Tx{
|
||||
Err: nil,
|
||||
//BondingCurve: s.Pool.String(),
|
||||
@@ -175,6 +177,7 @@ func FromTx(tx *parser.Tx) []*Tx {
|
||||
|
||||
EntryContract: s.CheckEntryContract(),
|
||||
Mayhem: s.Mayhem,
|
||||
Cashback: s.Cashback,
|
||||
}
|
||||
} else {
|
||||
newTx = &Tx{
|
||||
@@ -219,16 +222,18 @@ func FromTx(tx *parser.Tx) []*Tx {
|
||||
|
||||
EntryContract: s.CheckEntryContract(),
|
||||
Mayhem: s.Mayhem,
|
||||
Cashback: s.Cashback,
|
||||
}
|
||||
}
|
||||
}
|
||||
if newTx == nil {
|
||||
continue
|
||||
}
|
||||
if newTx.Maker == "HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K" && newTx.EntryContract == "oKXAggregatorV2" {
|
||||
newTx.Maker = tx.Signer.String()
|
||||
newTx.AfterSignerToken0Balance = tx.GetSignerTokenBalanceAfterTx(token0Program, token0Address).Div(decimal.New(1, int32(token0Decimals)))
|
||||
}
|
||||
|
||||
//if (newTx.Maker == "HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K" && newTx.EntryContract == "oKXAggregatorV2") || (newTx.Maker == "ARu4n5mFdZogZAravu7CcizaojWnS6oqka37gdLT5SZn" && newTx.EntryContract == "oKXDExRouterV2") {
|
||||
// newTx.Maker = tx.Signer.String()
|
||||
// newTx.AfterSignerToken0Balance = tx.GetSignerTokenBalanceAfterTx(token0Program, token0Address).Div(decimal.New(1, int32(token0Decimals)))
|
||||
//}
|
||||
|
||||
txs = append(txs, newTx)
|
||||
}
|
||||
295
internal/example/yellowstone.go
Normal file
295
internal/example/yellowstone.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
types "github.com/thloyi/pump-parser"
|
||||
pb "go.onsig.ai/onsig/yellowstone-proto"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
HandleMessage(rawTx *types.RawTx)
|
||||
}
|
||||
|
||||
var kacp = keepalive.ClientParameters{
|
||||
Time: 10 * time.Second, // send pings every 10 seconds if there is no activity
|
||||
Timeout: time.Second, // wait 1 second for ping ack before considering the connection dead
|
||||
PermitWithoutStream: true, // send pings even without active streams
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ch chan SubscriptionMessage
|
||||
endpoint string
|
||||
conn *grpc.ClientConn
|
||||
ctx context.Context
|
||||
lastReceiveTime time.Time
|
||||
backoffFactor float64
|
||||
|
||||
subscription *pb.SubscribeRequest
|
||||
subStatus bool
|
||||
|
||||
leastBlock BlockInfo
|
||||
|
||||
firstMessage bool
|
||||
|
||||
handler Handler
|
||||
|
||||
xToken string
|
||||
}
|
||||
|
||||
func NewClientWithPumpSwap(endpoint string, xtoken string, ch chan SubscriptionMessage) *Client {
|
||||
var subscription pb.SubscribeRequest
|
||||
|
||||
//var failed = true
|
||||
var vote = false
|
||||
subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions)
|
||||
subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{
|
||||
//Failed: &failed,
|
||||
Vote: &vote,
|
||||
}
|
||||
|
||||
//subscription.Transactions["transactions_sub"].AccountInclude = []string{
|
||||
// "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
|
||||
// "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
|
||||
//}
|
||||
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
|
||||
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
|
||||
|
||||
c := &Client{
|
||||
backoffFactor: 1.5,
|
||||
ch: ch,
|
||||
endpoint: endpoint,
|
||||
lastReceiveTime: time.Now(),
|
||||
subStatus: false,
|
||||
subscription: &subscription,
|
||||
xToken: xtoken,
|
||||
}
|
||||
c.handler = NewPumpHandler(func(tx *types.Tx) {
|
||||
c.sendTx(tx)
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Client {
|
||||
var subscription pb.SubscribeRequest
|
||||
|
||||
//var failed = false
|
||||
var vote = false
|
||||
subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions)
|
||||
subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{
|
||||
//Failed: &failed,
|
||||
Vote: &vote,
|
||||
}
|
||||
|
||||
subscription.Transactions["transactions_sub"].AccountInclude = []string{
|
||||
"LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", //LaunchLab
|
||||
"CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C", //CPMM
|
||||
//"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8", //V4
|
||||
}
|
||||
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
|
||||
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
|
||||
|
||||
c := &Client{
|
||||
backoffFactor: 1.5,
|
||||
ch: ch,
|
||||
endpoint: endpoint,
|
||||
lastReceiveTime: time.Now(),
|
||||
subStatus: false,
|
||||
subscription: &subscription,
|
||||
}
|
||||
c.handler = NewPumpHandler(func(tx *types.Tx) {
|
||||
c.sendTx(tx)
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
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, token, ch)
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("Context done, exiting loop")
|
||||
return
|
||||
default:
|
||||
}
|
||||
err := client.Connect(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Failed to connect: %v", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
// should not reach here, because Connect will block
|
||||
panic("geyser already connected, waiting for messages...")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SetSubscribe(subscription *pb.SubscribeRequest) {
|
||||
c.subscription = subscription
|
||||
}
|
||||
|
||||
func (c *Client) Connect(ctx context.Context) error {
|
||||
|
||||
c.ctx = ctx
|
||||
if c.conn == nil {
|
||||
// 连接到 geyser
|
||||
conn, err := c.grpcConnect(c.endpoint, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.conn = conn
|
||||
}
|
||||
|
||||
if c.subStatus {
|
||||
return nil // 已经订阅了
|
||||
}
|
||||
// 订阅交易
|
||||
err := c.grpcSubscribe(ctx, c.conn)
|
||||
if err != nil {
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
}
|
||||
c.conn = nil
|
||||
c.subStatus = false
|
||||
log.Printf("Failed to subscribe: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) grpcConnect(address string, plaintext bool) (*grpc.ClientConn, error) {
|
||||
var opts []grpc.DialOption
|
||||
if plaintext {
|
||||
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
} else {
|
||||
pool, _ := x509.SystemCertPool()
|
||||
creds := credentials.NewClientTLSFromCert(pool, "")
|
||||
opts = append(opts, grpc.WithTransportCredentials(creds))
|
||||
}
|
||||
|
||||
opts = append(opts, grpc.WithKeepaliveParams(kacp))
|
||||
|
||||
log.Println("Starting grpc client, connecting to", address)
|
||||
conn, err := grpc.NewClient(address, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to dial: %v", err)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Client) grpcSubscribe(ctx context.Context, conn *grpc.ClientConn) error {
|
||||
var err error
|
||||
client := pb.NewGeyserClient(conn)
|
||||
|
||||
//subscription.Transactions["transactions_sub"].AccountExclude = transactionsAccountsExclude
|
||||
|
||||
subscriptionJson, err := json.Marshal(c.subscription)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal subscription request: %v", subscriptionJson)
|
||||
return err
|
||||
}
|
||||
log.Printf("Subscription request: %s", string(subscriptionJson))
|
||||
|
||||
// Set up the subscription request
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
err = stream.Send(c.subscription)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.subStatus = true
|
||||
c.firstMessage = true
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
|
||||
if err == io.EOF {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error occurred in receiving update: %s", err)
|
||||
}
|
||||
|
||||
txn := resp.GetTransaction()
|
||||
if txn == nil {
|
||||
blockMeta := resp.GetBlockMeta()
|
||||
if blockMeta != nil && c.ch != nil {
|
||||
c.sendBlock(blockMeta)
|
||||
}
|
||||
continue
|
||||
}
|
||||
rawTx, err := types.ConvertYellowstoneGrpcTransactionToSolanaTransaction(txn, resp.GetCreatedAt().Seconds)
|
||||
if err != nil {
|
||||
log.Printf("Failed to convert transaction: %v", err)
|
||||
continue
|
||||
}
|
||||
c.handler.HandleMessage(rawTx)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) computeDelay(slot uint64) int64 {
|
||||
if c.leastBlock.Slot == 0 {
|
||||
return 0
|
||||
}
|
||||
if slot < c.leastBlock.Slot {
|
||||
return 0
|
||||
}
|
||||
delay := time.Now().Unix() - c.leastBlock.BlockTime - (4 * int64(slot-c.leastBlock.Slot) / 10)
|
||||
return delay
|
||||
}
|
||||
|
||||
func (c *Client) sendTx(t *types.Tx) {
|
||||
c.ch <- SubscriptionMessage{
|
||||
Reconnect: c.firstMessage,
|
||||
EstimateDelaySecond: c.computeDelay(t.Block),
|
||||
Block: nil,
|
||||
Tx: t,
|
||||
}
|
||||
c.firstMessage = false
|
||||
}
|
||||
|
||||
func (c *Client) sendBlock(blockMeta *pb.SubscribeUpdateBlockMeta) {
|
||||
c.leastBlock.Slot = blockMeta.GetSlot()
|
||||
c.leastBlock.BlockTime = blockMeta.GetBlockTime().Timestamp
|
||||
c.leastBlock.BlockHash = blockMeta.Blockhash
|
||||
c.leastBlock.Height = blockMeta.BlockHeight.BlockHeight
|
||||
c.ch <- SubscriptionMessage{
|
||||
EstimateDelaySecond: time.Now().Unix() - blockMeta.GetBlockTime().Timestamp,
|
||||
Reconnect: c.firstMessage,
|
||||
Block: &BlockInfo{
|
||||
Slot: c.leastBlock.Slot,
|
||||
BlockTime: c.leastBlock.BlockTime,
|
||||
BlockHash: c.leastBlock.BlockHash,
|
||||
Height: c.leastBlock.Height,
|
||||
},
|
||||
Tx: nil,
|
||||
}
|
||||
c.firstMessage = false
|
||||
}
|
||||
866
internal/test/test.go
Normal file
866
internal/test/test.go
Normal 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
60
internal/test2/test.go
Normal 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
821
internal/test3/test.go
Normal 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,
|
||||
)
|
||||
}
|
||||
208
meta.go
208
meta.go
@@ -20,12 +20,16 @@ var mayhemFeeAccounts = []solana.PublicKey{
|
||||
|
||||
var pumpGetFeesDiscriminator = calculateDiscriminator("global:get_fees")
|
||||
var pumpBuyDiscriminator = calculateDiscriminator("global:buy")
|
||||
var pumpBuyV2Discriminator = calculateDiscriminator("global:buy_exact_sol_in")
|
||||
var pumpBuyExactSolInDiscriminator = calculateDiscriminator("global:buy_exact_sol_in")
|
||||
var pumpBuyV2Discriminator = calculateDiscriminator("global:buy_v2")
|
||||
var pumpBuyExactQuoteInV2Discriminator = calculateDiscriminator("global:buy_exact_quote_in_v2")
|
||||
var pumpSellDiscriminator = calculateDiscriminator("global:sell")
|
||||
var pumpSellV2Discriminator = calculateDiscriminator("global:sell_v2")
|
||||
var pumpCreateDiscriminator = calculateDiscriminator("global:create")
|
||||
var pumpCreateV2Discriminator = calculateDiscriminator("global:create_v2")
|
||||
var pumpAdminSetCreatorDiscriminator = calculateDiscriminator("global:admin_set_creator")
|
||||
var pumpMigrateDiscriminator = calculateDiscriminator("global:migrate")
|
||||
var pumpMigrateV2Discriminator = calculateDiscriminator("global:migrate_v2")
|
||||
|
||||
var pumpEventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29}
|
||||
var pumpTradeEventDiscriminator = [16]byte{228, 69, 165, 46, 81, 203, 154, 29, 189, 219, 127, 211, 78, 230, 97, 238}
|
||||
@@ -35,12 +39,18 @@ var pumpMigrateEventDiscriminator = calculateDiscriminator("event:CompletePumpAm
|
||||
var pumpBuyEventDiscriminator = [8]byte{189, 219, 127, 211, 78, 230, 97, 238}
|
||||
|
||||
var (
|
||||
pumpAmmProgram = solana.MustPublicKeyFromBase58("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
||||
wSolMint = solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112")
|
||||
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 (
|
||||
pumpAmmBuyDiscriminator = calculateDiscriminator("global:buy")
|
||||
pumpAmmBuyDiscriminator = calculateDiscriminator("global:buy")
|
||||
pumpAmmBuyV2Discriminator = calculateDiscriminator("global:buy_exact_quote_in")
|
||||
|
||||
pumpAmmSellDiscriminator = calculateDiscriminator("global:sell")
|
||||
pumpAmmCreateDiscriminator = calculateDiscriminator("global:create_pool")
|
||||
pumpAmmWithdrawDiscriminator = calculateDiscriminator("global:withdraw")
|
||||
@@ -61,6 +71,191 @@ 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")
|
||||
metaoraPoolSwapEventDiscriminator = calculateDiscriminator("event:Swap")
|
||||
)
|
||||
|
||||
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")
|
||||
@@ -71,5 +266,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
2543
metaoradlmm.go
Normal file
File diff suppressed because it is too large
Load Diff
468
metaoradlmm_test.go
Normal file
468
metaoradlmm_test.go
Normal file
@@ -0,0 +1,468 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
agbinary "github.com/gagliardetto/binary"
|
||||
"github.com/gagliardetto/solana-go"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func testPublicKey(seed byte) solana.PublicKey {
|
||||
buf := make([]byte, solana.PublicKeyLength)
|
||||
for i := range buf {
|
||||
buf[i] = seed
|
||||
}
|
||||
return solana.PublicKeyFromBytes(buf)
|
||||
}
|
||||
|
||||
func seqInts(n int) []int {
|
||||
out := make([]int, n)
|
||||
for i := range out {
|
||||
out[i] = i
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mustBorshEncode(t *testing.T, value any) []byte {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := agbinary.NewBorshEncoder(&buf).Encode(value); err != nil {
|
||||
t.Fatalf("borsh encode failed: %v", err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func TestMeteoraDlmmInitializeParserCompatibility(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
discriminator [8]byte
|
||||
accountCount int
|
||||
wantPoolPos int
|
||||
wantBaseMintPos int
|
||||
wantQuoteMintPos int
|
||||
wantUserPos int
|
||||
wantBaseProgramPos int
|
||||
wantQuoteProgramPos int
|
||||
}{
|
||||
{
|
||||
name: "initialize_lb_pair",
|
||||
discriminator: meteoraInitializeLbPairDiscriminator,
|
||||
accountCount: 14,
|
||||
wantPoolPos: 0,
|
||||
wantBaseMintPos: 2,
|
||||
wantQuoteMintPos: 3,
|
||||
wantUserPos: 8,
|
||||
wantBaseProgramPos: 9,
|
||||
wantQuoteProgramPos: 9,
|
||||
},
|
||||
{
|
||||
name: "initialize_lb_pair2",
|
||||
discriminator: meteoraInitializeLbPair2Discriminator,
|
||||
accountCount: 16,
|
||||
wantPoolPos: 0,
|
||||
wantBaseMintPos: 2,
|
||||
wantQuoteMintPos: 3,
|
||||
wantUserPos: 8,
|
||||
wantBaseProgramPos: 11,
|
||||
wantQuoteProgramPos: 12,
|
||||
},
|
||||
{
|
||||
name: "initialize_customizable_permissionless_lb_pair",
|
||||
discriminator: meteoraInitializeCustomizablePermissionlessLbPairDiscriminator,
|
||||
accountCount: 14,
|
||||
wantPoolPos: 0,
|
||||
wantBaseMintPos: 2,
|
||||
wantQuoteMintPos: 3,
|
||||
wantUserPos: 8,
|
||||
wantBaseProgramPos: 9,
|
||||
wantQuoteProgramPos: 9,
|
||||
},
|
||||
{
|
||||
name: "initialize_customizable_permissionless_lb_pair2",
|
||||
discriminator: meteoraInitializeCustomizablePermissionlessLbPair2Discriminator,
|
||||
accountCount: 17,
|
||||
wantPoolPos: 0,
|
||||
wantBaseMintPos: 2,
|
||||
wantQuoteMintPos: 3,
|
||||
wantUserPos: 8,
|
||||
wantBaseProgramPos: 11,
|
||||
wantQuoteProgramPos: 12,
|
||||
},
|
||||
{
|
||||
name: "initialize_permission_lb_pair",
|
||||
discriminator: meteoraInitializePermissionLbPairDiscriminator,
|
||||
accountCount: 17,
|
||||
wantPoolPos: 1,
|
||||
wantBaseMintPos: 3,
|
||||
wantQuoteMintPos: 4,
|
||||
wantUserPos: 8,
|
||||
wantBaseProgramPos: 11,
|
||||
wantQuoteProgramPos: 12,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
accountList := make([]solana.PublicKey, 32)
|
||||
for i := range accountList {
|
||||
accountList[i] = testPublicKey(byte(i + 1))
|
||||
}
|
||||
programIndex := 30
|
||||
accountList[programIndex] = meteoraDlmmProgram
|
||||
|
||||
instruction := Instruction{
|
||||
Accounts: seqInts(tc.accountCount),
|
||||
Data: solana.Base58(tc.discriminator[:]),
|
||||
ProgramIDIndex: programIndex,
|
||||
}
|
||||
|
||||
rawTx := &RawTx{
|
||||
accountList: accountList,
|
||||
Meta: Meta{
|
||||
PostTokenBalances: []TokenBalance{
|
||||
{
|
||||
MintAccount: accountList[tc.wantBaseMintPos],
|
||||
UITokenAmount: UITokenAmount{
|
||||
Decimals: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
MintAccount: accountList[tc.wantQuoteMintPos],
|
||||
UITokenAmount: UITokenAmount{
|
||||
Decimals: 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Transaction: Transaction{
|
||||
Message: Message{
|
||||
Instructions: []Instruction{instruction},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tx := &Tx{rawTx: rawTx}
|
||||
|
||||
swaps, _, err := metaoradlmmParser(tx, instruction, InnerInstructions{}, [2]uint{0, 0})
|
||||
if err != nil {
|
||||
t.Fatalf("metaoradlmmParser() error = %v", err)
|
||||
}
|
||||
if len(swaps) != 1 {
|
||||
t.Fatalf("metaoradlmmParser() swaps len = %d, want 1", len(swaps))
|
||||
}
|
||||
|
||||
swap := swaps[0]
|
||||
if !swap.Pool.Equals(accountList[tc.wantPoolPos]) {
|
||||
t.Fatalf("swap.Pool = %s, want %s", swap.Pool, accountList[tc.wantPoolPos])
|
||||
}
|
||||
if !swap.BaseMint.Equals(accountList[tc.wantBaseMintPos]) {
|
||||
t.Fatalf("swap.BaseMint = %s, want %s", swap.BaseMint, accountList[tc.wantBaseMintPos])
|
||||
}
|
||||
if !swap.QuoteMint.Equals(accountList[tc.wantQuoteMintPos]) {
|
||||
t.Fatalf("swap.QuoteMint = %s, want %s", swap.QuoteMint, accountList[tc.wantQuoteMintPos])
|
||||
}
|
||||
if !swap.User.Equals(accountList[tc.wantUserPos]) {
|
||||
t.Fatalf("swap.User = %s, want %s", swap.User, accountList[tc.wantUserPos])
|
||||
}
|
||||
if !swap.BaseTokenProgram.Equals(accountList[tc.wantBaseProgramPos]) {
|
||||
t.Fatalf("swap.BaseTokenProgram = %s, want %s", swap.BaseTokenProgram, accountList[tc.wantBaseProgramPos])
|
||||
}
|
||||
if !swap.QuoteTokenProgram.Equals(accountList[tc.wantQuoteProgramPos]) {
|
||||
t.Fatalf("swap.QuoteTokenProgram = %s, want %s", swap.QuoteTokenProgram, accountList[tc.wantQuoteProgramPos])
|
||||
}
|
||||
if swap.BaseMintDecimals != 6 {
|
||||
t.Fatalf("swap.BaseMintDecimals = %d, want 6", swap.BaseMintDecimals)
|
||||
}
|
||||
if swap.QuoteMintDecimals != 9 {
|
||||
t.Fatalf("swap.QuoteMintDecimals = %d, want 9", swap.QuoteMintDecimals)
|
||||
}
|
||||
if !swap.EntryContract.Equals(meteoraDlmmProgram) {
|
||||
t.Fatalf("swap.EntryContract = %s, want %s", swap.EntryContract, meteoraDlmmProgram)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDlmmDecodeLbPairCreateEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
event := dlmmLbPairCreateEvent{
|
||||
LbPair: testPublicKey(90),
|
||||
BinStep: 42,
|
||||
TokenX: testPublicKey(91),
|
||||
TokenY: testPublicKey(92),
|
||||
}
|
||||
|
||||
body := mustBorshEncode(t, event)
|
||||
|
||||
barePayload := append(append([]byte{}, meteoraInitializeLbPairEventDiscriminator[:]...), body...)
|
||||
decodedBare, ok := dlmmDecodeLbPairCreateEvent(barePayload)
|
||||
if !ok {
|
||||
t.Fatalf("dlmmDecodeLbPairCreateEvent() failed for bare payload")
|
||||
}
|
||||
if decodedBare != event {
|
||||
t.Fatalf("decoded bare event = %+v, want %+v", decodedBare, event)
|
||||
}
|
||||
|
||||
anchorPayload := append(append(append([]byte{}, eventDiscriminator[:]...), meteoraInitializeLbPairEventDiscriminator[:]...), body...)
|
||||
decodedAnchor, ok := dlmmDecodeLbPairCreateEvent(anchorPayload)
|
||||
if !ok {
|
||||
t.Fatalf("dlmmDecodeLbPairCreateEvent() failed for anchor payload")
|
||||
}
|
||||
if decodedAnchor != event {
|
||||
t.Fatalf("decoded anchor event = %+v, want %+v", decodedAnchor, event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDlmmSwapAccountsAllowsRemainingAccountsAfterEventAuthority(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
accountList := make([]solana.PublicKey, 40)
|
||||
for i := range accountList {
|
||||
accountList[i] = testPublicKey(byte(i + 1))
|
||||
}
|
||||
accountList[0] = testPublicKey(200)
|
||||
accountList[26] = meteoraDlmmProgram
|
||||
accountList[27] = solana.MemoProgramID
|
||||
accountList[29] = solana.TokenProgramID
|
||||
accountList[33] = meteoraDlmmEventAuthority
|
||||
|
||||
rawTx := &RawTx{
|
||||
accountList: accountList,
|
||||
Transaction: Transaction{
|
||||
Message: Message{
|
||||
AccountKeys: accountList[:11],
|
||||
Header: Header{
|
||||
NumRequiredSignatures: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
accounts := []int{13, 26, 16, 14, 11, 4, 35, 28, 15, 26, 0, 29, 29, 27, 33, 29, 3, 7, 2}
|
||||
|
||||
resolved, err := resolveDlmmSwapAccounts(rawTx, accounts)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveDlmmSwapAccounts() error = %v", err)
|
||||
}
|
||||
if resolved.poolIdx != 13 {
|
||||
t.Fatalf("poolIdx = %d, want 13", resolved.poolIdx)
|
||||
}
|
||||
if resolved.reserveXIdx != 16 || resolved.reserveYIdx != 14 {
|
||||
t.Fatalf("reserve indexes = %d/%d, want 16/14", resolved.reserveXIdx, resolved.reserveYIdx)
|
||||
}
|
||||
if resolved.userIdx != 0 {
|
||||
t.Fatalf("userIdx = %d, want 0", resolved.userIdx)
|
||||
}
|
||||
if resolved.tokenXProgramIdx != 29 || resolved.tokenYProgramIdx != 29 {
|
||||
t.Fatalf("token program indexes = %d/%d, want 29/29", resolved.tokenXProgramIdx, resolved.tokenYProgramIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeteoraDlmmInitializeParserUsesLbPairCreateEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
accountList := make([]solana.PublicKey, 32)
|
||||
for i := range accountList {
|
||||
accountList[i] = testPublicKey(byte(i + 1))
|
||||
}
|
||||
programIndex := 30
|
||||
accountList[programIndex] = meteoraDlmmProgram
|
||||
|
||||
instruction := Instruction{
|
||||
Accounts: seqInts(16),
|
||||
Data: solana.Base58(meteoraInitializeLbPair2Discriminator[:]),
|
||||
ProgramIDIndex: programIndex,
|
||||
}
|
||||
|
||||
event := dlmmLbPairCreateEvent{
|
||||
LbPair: testPublicKey(111),
|
||||
BinStep: 25,
|
||||
TokenX: testPublicKey(112),
|
||||
TokenY: testPublicKey(113),
|
||||
}
|
||||
innerEventData := append(
|
||||
append(append([]byte{}, eventDiscriminator[:]...), meteoraInitializeLbPairEventDiscriminator[:]...),
|
||||
mustBorshEncode(t, event)...,
|
||||
)
|
||||
|
||||
rawTx := &RawTx{
|
||||
accountList: accountList,
|
||||
Meta: Meta{
|
||||
PostTokenBalances: []TokenBalance{
|
||||
{
|
||||
MintAccount: accountList[2],
|
||||
UITokenAmount: UITokenAmount{
|
||||
Decimals: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
MintAccount: accountList[3],
|
||||
UITokenAmount: UITokenAmount{
|
||||
Decimals: 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
InnerInstructions: []InnerInstructions{
|
||||
{
|
||||
Index: 0,
|
||||
Instructions: []Instruction{
|
||||
{
|
||||
ProgramIDIndex: programIndex,
|
||||
Data: solana.Base58(innerEventData),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Transaction: Transaction{
|
||||
Message: Message{
|
||||
Instructions: []Instruction{instruction},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tx := &Tx{rawTx: rawTx}
|
||||
|
||||
swaps, nextOffset, err := metaoradlmmParser(tx, instruction, rawTx.Meta.InnerInstructions[0], [2]uint{0, 0})
|
||||
if err != nil {
|
||||
t.Fatalf("metaoradlmmParser() error = %v", err)
|
||||
}
|
||||
if len(swaps) != 1 {
|
||||
t.Fatalf("metaoradlmmParser() swaps len = %d, want 1", len(swaps))
|
||||
}
|
||||
|
||||
swap := swaps[0]
|
||||
if !swap.Pool.Equals(event.LbPair) {
|
||||
t.Fatalf("swap.Pool = %s, want event %s", swap.Pool, event.LbPair)
|
||||
}
|
||||
if !swap.BaseMint.Equals(event.TokenX) {
|
||||
t.Fatalf("swap.BaseMint = %s, want event %s", swap.BaseMint, event.TokenX)
|
||||
}
|
||||
if !swap.QuoteMint.Equals(event.TokenY) {
|
||||
t.Fatalf("swap.QuoteMint = %s, want event %s", swap.QuoteMint, event.TokenY)
|
||||
}
|
||||
if nextOffset != ([2]uint{1, 0}) {
|
||||
t.Fatalf("nextOffset = %#v, want [2]uint{1, 0}", nextOffset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDlmmSwapFeeInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
baseMint := testPublicKey(1)
|
||||
quoteMint := testPublicKey(2)
|
||||
baseProgram := testPublicKey(3)
|
||||
quoteProgram := testPublicKey(4)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
baseIsX bool
|
||||
swapForY bool
|
||||
wantFeeSide string
|
||||
wantFeeMint solana.PublicKey
|
||||
wantFeeProg solana.PublicKey
|
||||
wantDecimals uint8
|
||||
}{
|
||||
{
|
||||
name: "x is base and input is x",
|
||||
baseIsX: true,
|
||||
swapForY: true,
|
||||
wantFeeSide: "base",
|
||||
wantFeeMint: baseMint,
|
||||
wantFeeProg: baseProgram,
|
||||
wantDecimals: 6,
|
||||
},
|
||||
{
|
||||
name: "x is base and input is y",
|
||||
baseIsX: true,
|
||||
swapForY: false,
|
||||
wantFeeSide: "quote",
|
||||
wantFeeMint: quoteMint,
|
||||
wantFeeProg: quoteProgram,
|
||||
wantDecimals: 9,
|
||||
},
|
||||
{
|
||||
name: "y is base and input is x",
|
||||
baseIsX: false,
|
||||
swapForY: true,
|
||||
wantFeeSide: "quote",
|
||||
wantFeeMint: quoteMint,
|
||||
wantFeeProg: quoteProgram,
|
||||
wantDecimals: 9,
|
||||
},
|
||||
{
|
||||
name: "y is base and input is y",
|
||||
baseIsX: false,
|
||||
swapForY: false,
|
||||
wantFeeSide: "base",
|
||||
wantFeeMint: baseMint,
|
||||
wantFeeProg: baseProgram,
|
||||
wantDecimals: 6,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
feeAmount, feeSide, feeMint, feeProgram, feeDecimals := dlmmSwapFeeInfo(
|
||||
tc.baseIsX,
|
||||
tc.swapForY,
|
||||
123,
|
||||
baseMint,
|
||||
quoteMint,
|
||||
baseProgram,
|
||||
quoteProgram,
|
||||
6,
|
||||
9,
|
||||
)
|
||||
if !feeAmount.Equal(decimal.NewFromInt(123)) {
|
||||
t.Fatalf("feeAmount = %s, want 123", feeAmount)
|
||||
}
|
||||
if feeSide != tc.wantFeeSide {
|
||||
t.Fatalf("feeSide = %s, want %s", feeSide, tc.wantFeeSide)
|
||||
}
|
||||
if !feeMint.Equals(tc.wantFeeMint) {
|
||||
t.Fatalf("feeMint = %s, want %s", feeMint, tc.wantFeeMint)
|
||||
}
|
||||
if !feeProgram.Equals(tc.wantFeeProg) {
|
||||
t.Fatalf("feeProgram = %s, want %s", feeProgram, tc.wantFeeProg)
|
||||
}
|
||||
if feeDecimals != tc.wantDecimals {
|
||||
t.Fatalf("feeDecimals = %d, want %d", feeDecimals, tc.wantDecimals)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDlmmSwapLpFeeAmount(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lpFee := dlmmSwapLpFeeAmount(100, 15, 5)
|
||||
if !lpFee.Equal(decimal.NewFromInt(80)) {
|
||||
t.Fatalf("lpFee = %s, want 80", lpFee)
|
||||
}
|
||||
|
||||
lpFee = dlmmSwapLpFeeAmount(10, 8, 5)
|
||||
if !lpFee.IsZero() {
|
||||
t.Fatalf("lpFee should floor at zero, got %s", lpFee)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDlmmSwapFeeBpsString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
feeBps := agbinary.Uint128{Lo: 12345}
|
||||
if got := dlmmSwapFeeBpsString(feeBps); got != "12345" {
|
||||
t.Fatalf("dlmmSwapFeeBpsString() = %s, want 12345", got)
|
||||
}
|
||||
}
|
||||
1091
metaorapool.go
Normal file
1091
metaorapool.go
Normal file
File diff suppressed because it is too large
Load Diff
152
metaorapool_test.go
Normal file
152
metaorapool_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
)
|
||||
|
||||
func TestMetaoraPoolSwapEventFromLogsUsesMatchingInvocation(t *testing.T) {
|
||||
firstEvent := metaoraPoolSwapEventLogForTest(10, 9, 1, 0, 0)
|
||||
secondEvent := metaoraPoolSwapEventLogForTest(4013522650, 135, 8043041, 2010760, 0)
|
||||
|
||||
logs := []string{
|
||||
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
|
||||
"Program log: Instruction: Swap",
|
||||
"Program data: " + firstEvent,
|
||||
"Program " + metaoraPoolProgramID.String() + " success",
|
||||
"Program " + solana.TokenProgramID.String() + " invoke [1]",
|
||||
"Program data: " + secondEvent,
|
||||
"Program " + solana.TokenProgramID.String() + " success",
|
||||
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
|
||||
"Program log: Instruction: Swap",
|
||||
"Program data: " + secondEvent,
|
||||
"Program " + metaoraPoolProgramID.String() + " success",
|
||||
}
|
||||
|
||||
event, ok := metaoraPoolSwapEventFromLogs(logs, 2)
|
||||
if !ok {
|
||||
t.Fatal("expected second swap event")
|
||||
}
|
||||
if event.InAmount != 4013522650 {
|
||||
t.Fatalf("in amount = %d, want 4013522650", event.InAmount)
|
||||
}
|
||||
if event.OutAmount != 135 {
|
||||
t.Fatalf("out amount = %d, want 135", event.OutAmount)
|
||||
}
|
||||
if event.TradeFee != 8043041 {
|
||||
t.Fatalf("trade fee = %d, want 8043041", event.TradeFee)
|
||||
}
|
||||
if event.ProtocolFee != 2010760 {
|
||||
t.Fatalf("protocol fee = %d, want 2010760", event.ProtocolFee)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaoraPoolSwapInstructionOccurrenceIncludesInnerInstructions(t *testing.T) {
|
||||
rawTx := &RawTx{
|
||||
Transaction: Transaction{
|
||||
Message: Message{
|
||||
AccountKeys: solana.PublicKeySlice{
|
||||
metaoraPoolProgramID,
|
||||
solana.MustPublicKeyFromBase58("BASDaPs2cdVTsvgPRfESDLZgek8tKRTfqbR2ksdgptsn"),
|
||||
},
|
||||
Instructions: []Instruction{
|
||||
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
|
||||
{ProgramIDIndex: 1, Data: []byte{1}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Meta: Meta{
|
||||
InnerInstructions: []InnerInstructions{
|
||||
{
|
||||
Index: 1,
|
||||
Instructions: []Instruction{
|
||||
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if got := metaoraPoolSwapInstructionOccurrence(rawTx, [2]uint{1, 1}); got != 2 {
|
||||
t.Fatalf("occurrence = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachLogEventsToInstructions(t *testing.T) {
|
||||
swapEvent := metaoraPoolSwapEventLogForTest(4013522650, 135, 8043041, 2010760, 0)
|
||||
rawTx := &RawTx{
|
||||
Transaction: Transaction{
|
||||
Message: Message{
|
||||
AccountKeys: solana.PublicKeySlice{
|
||||
metaoraPoolProgramID,
|
||||
solana.TokenProgramID,
|
||||
},
|
||||
Instructions: []Instruction{
|
||||
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
|
||||
},
|
||||
},
|
||||
},
|
||||
Meta: Meta{
|
||||
InnerInstructions: []InnerInstructions{
|
||||
{
|
||||
Index: 0,
|
||||
Instructions: []Instruction{
|
||||
{ProgramIDIndex: 1, Data: []byte{3}, StackHeight: intPtrForTest(2)},
|
||||
},
|
||||
},
|
||||
},
|
||||
LogMessages: []string{
|
||||
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
|
||||
"Program log: Instruction: Swap",
|
||||
"Program " + solana.TokenProgramID.String() + " invoke [2]",
|
||||
"Program " + solana.TokenProgramID.String() + " success",
|
||||
"Program data: " + swapEvent,
|
||||
"Program " + metaoraPoolProgramID.String() + " success",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
applyRawTxConvertLogOptions(rawTx, RawTxConvertOptions{ParseLogEvents: true, IgnoreLogMessages: true})
|
||||
|
||||
if len(rawTx.Meta.LogMessages) != 0 {
|
||||
t.Fatalf("log messages length = %d, want 0", len(rawTx.Meta.LogMessages))
|
||||
}
|
||||
if len(rawTx.Transaction.Message.Instructions[0].LogEvents) != 1 {
|
||||
t.Fatalf("outer log events length = %d, want 1", len(rawTx.Transaction.Message.Instructions[0].LogEvents))
|
||||
}
|
||||
if len(rawTx.Meta.InnerInstructions[0].Instructions[0].LogEvents) != 0 {
|
||||
t.Fatalf("inner log events length = %d, want 0", len(rawTx.Meta.InnerInstructions[0].Instructions[0].LogEvents))
|
||||
}
|
||||
|
||||
event, ok := metaoraPoolSwapEventFromInstruction(rawTx.Transaction.Message.Instructions[0])
|
||||
if !ok {
|
||||
t.Fatal("expected swap event from outer instruction")
|
||||
}
|
||||
if event.OutAmount != 135 {
|
||||
t.Fatalf("out amount = %d, want 135", event.OutAmount)
|
||||
}
|
||||
}
|
||||
|
||||
func metaoraPoolSwapInstructionDataForTest() []byte {
|
||||
data := make([]byte, 8+16)
|
||||
copy(data, metaoraPoolSwapDiscriminator[:])
|
||||
return data
|
||||
}
|
||||
|
||||
func metaoraPoolSwapEventLogForTest(inAmount, outAmount, tradeFee, protocolFee, hostFee uint64) string {
|
||||
data := make([]byte, 8+40)
|
||||
copy(data, metaoraPoolSwapEventDiscriminator[:])
|
||||
binary.LittleEndian.PutUint64(data[8:16], inAmount)
|
||||
binary.LittleEndian.PutUint64(data[16:24], outAmount)
|
||||
binary.LittleEndian.PutUint64(data[24:32], tradeFee)
|
||||
binary.LittleEndian.PutUint64(data[32:40], protocolFee)
|
||||
binary.LittleEndian.PutUint64(data[40:48], hostFee)
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
func intPtrForTest(value int) *int {
|
||||
return &value
|
||||
}
|
||||
396
meteora_bonding_curve.go
Normal file
396
meteora_bonding_curve.go
Normal 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
518
meteoradamm.go
Normal 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
1365
orcawhirpool.go
Normal file
File diff suppressed because it is too large
Load Diff
23
orcawhirpool_test.go
Normal file
23
orcawhirpool_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package pump_parser
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestOrcaWhirlpoolRemoveLiquidityPreservesLargeUint64TransferAmounts(t *testing.T) {
|
||||
EnableAllParsers()
|
||||
|
||||
tx := mustParseRPCFixtureTx(t, "4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri")
|
||||
if len(tx.Swaps) == 0 {
|
||||
t.Fatal("expected parsed swaps")
|
||||
}
|
||||
|
||||
swap := tx.Swaps[0]
|
||||
if swap.Program != SolProgramOrcaWhirPool {
|
||||
t.Fatalf("program = %s, want %s", swap.Program, SolProgramOrcaWhirPool)
|
||||
}
|
||||
if swap.Event != TxEventRemoveLiquidity {
|
||||
t.Fatalf("event = %s, want %s", swap.Event, TxEventRemoveLiquidity)
|
||||
}
|
||||
|
||||
assertDecimalString(t, "base_amount", swap.BaseAmount, "101086439062")
|
||||
assertDecimalString(t, "quote_amount", swap.QuoteAmount, "9863327902766042414")
|
||||
}
|
||||
213
parser.go
213
parser.go
@@ -2,19 +2,67 @@ package pump_parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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,
|
||||
systemProgram: systemParser,
|
||||
budgGetProgram: budgetParser,
|
||||
chainLinkProgram: chainLinkParser,
|
||||
}
|
||||
|
||||
func ParseRawTx(rawTx *RawTx) (*Tx, error) {
|
||||
@@ -33,23 +81,80 @@ 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()
|
||||
tx.Block = tx.rawTx.Slot
|
||||
tx.BlockIndex = uint64(tx.rawTx.IndexWithinBlock)
|
||||
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)})
|
||||
@@ -59,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 {
|
||||
@@ -73,6 +192,10 @@ func (tx *Tx) Parser() error {
|
||||
// unknown program, parser inner instructions
|
||||
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)
|
||||
break
|
||||
}
|
||||
innerInstr := innersMap[i].Instructions[j-1]
|
||||
innerProgramAccount := accountList[innerInstr.ProgramIDIndex]
|
||||
|
||||
@@ -85,9 +208,33 @@ func (tx *Tx) Parser() error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
tx.Swaps = append(tx.Swaps, swaps...)
|
||||
j = int(offset[1])
|
||||
ii = int(offset[0])
|
||||
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 {
|
||||
@@ -108,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
|
||||
}
|
||||
|
||||
263
pump_test.go
263
pump_test.go
@@ -1,14 +1,42 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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 +44,245 @@ 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")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPumpExactQuoteInKeepsFeeArgBeforeMatchedTrade(t *testing.T) {
|
||||
EnableAllParsers()
|
||||
|
||||
tx := mustParseRPCFixtureTx(t, "3jugr2KthX3cUHzPrMpaFKM7RtxXM6Gcxi8eFjDL7aZGLXpc6f1RaVdnAoB4ye5bRVYsP2fFs3aLaP19Utz91ewv")
|
||||
if len(tx.Swaps) != 4 {
|
||||
t.Fatalf("swaps len = %d, want 4", len(tx.Swaps))
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
swap := tx.Swaps[i]
|
||||
if swap.Program != SolProgramPump || swap.Event != "buy" {
|
||||
t.Fatalf("swap[%d] = %s/%s, want Pump/buy", i, swap.Program, swap.Event)
|
||||
}
|
||||
assertDecimalString(t, fmt.Sprintf("swap[%d].quote_amount", i), swap.QuoteAmount, "329217")
|
||||
assertDecimalString(t, fmt.Sprintf("swap[%d].fixed_amount", i), swap.FixedAmount, "333333")
|
||||
}
|
||||
|
||||
sell := tx.Swaps[3]
|
||||
if sell.Program != SolProgramPump || sell.Event != "sell" {
|
||||
t.Fatalf("swap[3] = %s/%s, want Pump/sell", sell.Program, sell.Event)
|
||||
}
|
||||
assertDecimalString(t, "swap[3].base_amount", sell.BaseAmount, "12282189230")
|
||||
assertDecimalString(t, "swap[3].quote_amount", sell.QuoteAmount, "987647")
|
||||
}
|
||||
|
||||
func TestPumpV2Discriminators(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
got [8]byte
|
||||
want [8]byte
|
||||
}{
|
||||
{name: "buy_exact_sol_in", got: pumpBuyExactSolInDiscriminator, want: [8]byte{56, 252, 116, 8, 158, 223, 205, 95}},
|
||||
{name: "buy_v2", got: pumpBuyV2Discriminator, want: [8]byte{184, 23, 238, 97, 103, 197, 211, 61}},
|
||||
{name: "buy_exact_quote_in_v2", got: pumpBuyExactQuoteInV2Discriminator, want: [8]byte{194, 171, 28, 70, 104, 77, 91, 47}},
|
||||
{name: "sell_v2", got: pumpSellV2Discriminator, want: [8]byte{93, 246, 130, 60, 231, 233, 64, 178}},
|
||||
{name: "create_v2", got: pumpCreateV2Discriminator, want: [8]byte{214, 144, 76, 236, 95, 139, 49, 180}},
|
||||
{name: "migrate_v2", got: pumpMigrateV2Discriminator, want: [8]byte{187, 203, 18, 31, 206, 237, 254, 41}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if tt.got != tt.want {
|
||||
t.Fatalf("%s discriminator = %v, want %v", tt.name, tt.got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPumpMigrateLayoutV2(t *testing.T) {
|
||||
accounts := make([]int, 27)
|
||||
for i := range accounts {
|
||||
accounts[i] = i
|
||||
}
|
||||
layout, ok := pumpMigrateLayout(Instruction{
|
||||
Data: pumpMigrateV2Discriminator[:],
|
||||
Accounts: accounts,
|
||||
})
|
||||
if !ok {
|
||||
t.Fatal("migrate_v2 layout not recognized")
|
||||
}
|
||||
if !layout.IsV2 ||
|
||||
layout.BaseMint != 2 ||
|
||||
layout.QuoteMint != 3 ||
|
||||
layout.Pool != 4 ||
|
||||
layout.BasePoolToken != 5 ||
|
||||
layout.QuotePoolToken != 6 ||
|
||||
layout.User != 7 ||
|
||||
layout.BaseTokenProgram != 19 ||
|
||||
layout.QuoteTokenProgram != 20 {
|
||||
t.Fatalf("migrate_v2 layout = %+v", layout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPumpTradeAmountInfoV2(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
disc [8]byte
|
||||
wantMode SwapMode
|
||||
}{
|
||||
{name: "legacy exact quote in", disc: pumpBuyExactSolInDiscriminator, wantMode: SwapModeExactIn},
|
||||
{name: "v2 exact quote in", disc: pumpBuyExactQuoteInV2Discriminator, wantMode: SwapModeExactIn},
|
||||
{name: "v2 buy exact out", disc: pumpBuyV2Discriminator, wantMode: SwapModeExactOut},
|
||||
{name: "v2 sell exact in", disc: pumpSellV2Discriminator, wantMode: SwapModeExactIn},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
mode, fixed, limit, ok := pumpTradeAmountInfoFromArgs(PumpTradeArgs{
|
||||
Discriminator: tt.disc,
|
||||
Amount1: 11,
|
||||
Amount2: 22,
|
||||
})
|
||||
if !ok {
|
||||
t.Fatalf("%s not recognized", tt.name)
|
||||
}
|
||||
if mode != tt.wantMode {
|
||||
t.Fatalf("%s mode = %s, want %s", tt.name, mode.String(), tt.wantMode.String())
|
||||
}
|
||||
if fixed.String() != "11" || limit.String() != "22" {
|
||||
t.Fatalf("%s fixed/limit = %s/%s, want 11/22", tt.name, fixed, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPumpCreateQuoteAccountsOptional(t *testing.T) {
|
||||
createAccounts := make([]int, 17)
|
||||
for i := range createAccounts {
|
||||
createAccounts[i] = i
|
||||
}
|
||||
createV2Accounts := make([]int, 19)
|
||||
for i := range createV2Accounts {
|
||||
createV2Accounts[i] = i
|
||||
}
|
||||
createV2Accounts[16] = 14
|
||||
createV2Accounts[18] = 16
|
||||
accountList := make([]solana.PublicKey, 19)
|
||||
accountList[14] = usdcMint
|
||||
accountList[16] = solana.TokenProgramID
|
||||
accountList[18] = solana.TokenProgramID
|
||||
result := &RawTx{accountList: accountList}
|
||||
|
||||
quoteMint, quoteTokenProgram, quoteDecimals := pumpCreateQuoteAccounts(result, Instruction{
|
||||
Data: pumpCreateDiscriminator[:],
|
||||
Accounts: createAccounts,
|
||||
}, PumpCreateEvent{})
|
||||
if !quoteMint.IsZero() || !quoteTokenProgram.IsZero() || quoteDecimals != 9 {
|
||||
t.Fatalf("create quote accounts = %s/%s/%d, want zero/zero/9", quoteMint, quoteTokenProgram, quoteDecimals)
|
||||
}
|
||||
|
||||
quoteMint, quoteTokenProgram, quoteDecimals = pumpCreateQuoteAccounts(result, Instruction{
|
||||
Data: pumpCreateV2Discriminator[:],
|
||||
Accounts: createV2Accounts,
|
||||
}, PumpCreateEvent{})
|
||||
if !quoteMint.Equals(usdcMint) || !quoteTokenProgram.Equals(solana.TokenProgramID) || quoteDecimals != 6 {
|
||||
t.Fatalf("create_v2 quote accounts = %s/%s/%d, want USDC/token/6", quoteMint, quoteTokenProgram, quoteDecimals)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePumpTradeEventV2QuoteFields(t *testing.T) {
|
||||
user := solana.MustPublicKeyFromBase58("DS95KxqUCCjwQaXhD7fhKatXbivwWDNrJdNV5ZcubGdz")
|
||||
mint := solana.MustPublicKeyFromBase58("8GNGkNnfBuoTP3QRnmdNzSYuuE15M8tvcNvxNsV4pump")
|
||||
want := PumpTradeEvent{
|
||||
Mint: mint,
|
||||
SolAmount: 1,
|
||||
TokenAmount: 2,
|
||||
IsBuy: true,
|
||||
User: user,
|
||||
VirtualTokenReserves: 3,
|
||||
RealTokenReserves: 4,
|
||||
IxName: "buy_v2",
|
||||
Shareholders: []PumpShareholder{{Address: user, ShareBps: 250}},
|
||||
QuoteMint: usdcMint,
|
||||
QuoteAmount: 5,
|
||||
VirtualQuoteReserves: 6,
|
||||
RealQuoteReserves: 7,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := agbinary.NewBorshEncoder(&buf).Encode(want); err != nil {
|
||||
t.Fatalf("encode v2 trade event: %v", err)
|
||||
}
|
||||
got, err := decodePumpTradeEvent(buf.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("decodePumpTradeEvent() error = %v", err)
|
||||
}
|
||||
if !got.QuoteMint.Equals(usdcMint) || got.QuoteAmount != 5 || got.VirtualQuoteReserves != 6 || got.RealQuoteReserves != 7 {
|
||||
t.Fatalf("decoded quote fields = %s/%d/%d/%d", got.QuoteMint, got.QuoteAmount, got.VirtualQuoteReserves, got.RealQuoteReserves)
|
||||
}
|
||||
if len(got.Shareholders) != 1 || got.Shareholders[0].ShareBps != 250 {
|
||||
t.Fatalf("decoded shareholders = %+v", got.Shareholders)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePumpTradeEventLegacyFallback(t *testing.T) {
|
||||
hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e"
|
||||
data, err := hex.DecodeString(hexData)
|
||||
if err != nil {
|
||||
t.Fatalf("decode hex: %v", err)
|
||||
}
|
||||
got, err := decodePumpTradeEvent(data[16:])
|
||||
if err != nil {
|
||||
t.Fatalf("decodePumpTradeEvent() legacy error = %v", err)
|
||||
}
|
||||
if got.IxName != "buy_exact_sol_in" || got.SolAmount != 11725956 || !got.IsBuy {
|
||||
t.Fatalf("legacy event = %+v", got)
|
||||
}
|
||||
if !got.QuoteMint.IsZero() || got.QuoteAmount != 0 || got.RealQuoteReserves != 0 {
|
||||
t.Fatalf("legacy quote fields = %s/%d/%d, want zero", got.QuoteMint, got.QuoteAmount, got.RealQuoteReserves)
|
||||
}
|
||||
}
|
||||
|
||||
479
pumpamm.go
479
pumpamm.go
@@ -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:
|
||||
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,6 +529,17 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var event ammBuyEvent
|
||||
for innerIndex, innerInstr := range inners {
|
||||
if innerInstr.ProgramIDIndex == instruction.ProgramIDIndex &&
|
||||
@@ -254,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)
|
||||
@@ -298,29 +593,69 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
||||
TokenProgram: quoteTokenProgram,
|
||||
}
|
||||
}
|
||||
return []Swap{
|
||||
{
|
||||
Program: SolProgramPumpAMM,
|
||||
Event: "buy",
|
||||
Pool: event.Pool,
|
||||
BaseMint: baseMint,
|
||||
QuoteMint: quoteMint,
|
||||
BaseTokenProgram: baseTokenProgram,
|
||||
QuoteTokenProgram: quoteTokenProgram,
|
||||
Creator: event.CoinCreator,
|
||||
BaseMintDecimals: baseMintDecimals,
|
||||
QuoteMintDecimals: quoteMintDecimals,
|
||||
User: event.User,
|
||||
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
|
||||
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountIn),
|
||||
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
|
||||
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
|
||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
||||
UserBaseBalance: decimal.NewFromUint64(event.UserBaseTokenReserve + event.BaseAmountOut),
|
||||
UserQuoteBalance: decimal.NewFromUint64(event.UserQuoteTokenReserve - event.UserQuoteAmountIn),
|
||||
EntryContract: entryContract,
|
||||
},
|
||||
}, offset, nil
|
||||
|
||||
var eventUser = event.User
|
||||
|
||||
baseMintAtaUserIdx := instruction.Accounts[5]
|
||||
userIndex := instruction.Accounts[1]
|
||||
if !event.User.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))
|
||||
}
|
||||
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,
|
||||
BaseMint: baseMint,
|
||||
QuoteMint: quoteMint,
|
||||
BaseTokenProgram: baseTokenProgram,
|
||||
QuoteTokenProgram: quoteTokenProgram,
|
||||
Creator: event.CoinCreator,
|
||||
BaseMintDecimals: baseMintDecimals,
|
||||
QuoteMintDecimals: quoteMintDecimals,
|
||||
User: eventUser,
|
||||
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
|
||||
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,
|
||||
}
|
||||
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) {
|
||||
@@ -332,6 +667,18 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var event ammSellEvent
|
||||
for innerIndex, innerInstr := range inners {
|
||||
if innerInstr.ProgramIDIndex == instruction.ProgramIDIndex &&
|
||||
@@ -341,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)
|
||||
@@ -385,29 +732,57 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
||||
TokenProgram: quoteTokenProgram,
|
||||
}
|
||||
}
|
||||
return []Swap{
|
||||
{
|
||||
Program: SolProgramPumpAMM,
|
||||
Event: "sell",
|
||||
Pool: event.Pool,
|
||||
BaseMint: baseMint,
|
||||
QuoteMint: quoteMint,
|
||||
BaseTokenProgram: baseTokenProgram,
|
||||
QuoteTokenProgram: quoteTokenProgram,
|
||||
Creator: event.CoinCreator,
|
||||
BaseMintDecimals: baseMintDecimals,
|
||||
QuoteMintDecimals: quoteMintDecimals,
|
||||
User: event.User,
|
||||
BaseAmount: decimal.NewFromUint64(event.BaseAmountIn),
|
||||
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountOut),
|
||||
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn),
|
||||
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut),
|
||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
||||
UserBaseBalance: decimal.NewFromUint64(event.UserBaseTokenReserves - event.BaseAmountIn),
|
||||
UserQuoteBalance: decimal.NewFromUint64(event.UserQuoteTokenReserves + event.UserQuoteAmountOut),
|
||||
EntryContract: entryContract,
|
||||
},
|
||||
}, offset, nil
|
||||
|
||||
var eventUser = event.User
|
||||
|
||||
baseMintAtaUserIdx := instruction.Accounts[5]
|
||||
userIndex := instruction.Accounts[1]
|
||||
if !event.User.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))
|
||||
}
|
||||
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
|
||||
swap := Swap{
|
||||
Program: SolProgramPumpAMM,
|
||||
Event: "sell",
|
||||
Pool: event.Pool,
|
||||
BaseMint: baseMint,
|
||||
QuoteMint: quoteMint,
|
||||
BaseTokenProgram: baseTokenProgram,
|
||||
QuoteTokenProgram: quoteTokenProgram,
|
||||
Creator: event.CoinCreator,
|
||||
BaseMintDecimals: baseMintDecimals,
|
||||
QuoteMintDecimals: quoteMintDecimals,
|
||||
User: eventUser,
|
||||
BaseAmount: decimal.NewFromUint64(event.BaseAmountIn),
|
||||
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountOut),
|
||||
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,
|
||||
}
|
||||
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) {
|
||||
@@ -430,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)
|
||||
@@ -528,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)
|
||||
|
||||
844
rawtx.go
844
rawtx.go
@@ -1,8 +1,12 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
bin "github.com/gagliardetto/binary"
|
||||
@@ -10,6 +14,7 @@ import (
|
||||
"github.com/gagliardetto/solana-go/rpc"
|
||||
"github.com/jackc/pgtype"
|
||||
"github.com/shopspring/decimal"
|
||||
pb "go.onsig.ai/onsig/yellowstone-proto"
|
||||
)
|
||||
|
||||
func (tx *RawTx) getAccountList() []solana.PublicKey {
|
||||
@@ -72,6 +77,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()
|
||||
@@ -100,10 +109,11 @@ func (tx *RawTx) GetBlockTime() *pgtype.Timestamptz {
|
||||
}
|
||||
|
||||
type Instruction struct {
|
||||
Accounts []int `json:"accounts"`
|
||||
Data solana.Base58 `json:"data"`
|
||||
ProgramIDIndex int `json:"programIdIndex"`
|
||||
StackHeight *int `json:"stackHeight"`
|
||||
Accounts []int `json:"accounts"`
|
||||
Data solana.Base58 `json:"data"`
|
||||
ProgramIDIndex int `json:"programIdIndex"`
|
||||
StackHeight *int `json:"stackHeight"`
|
||||
LogEvents []solana.Base64 `json:"logEvents,omitempty"`
|
||||
}
|
||||
type InnerInstructions struct {
|
||||
Index int `json:"index"`
|
||||
@@ -145,16 +155,17 @@ func (tb *TokenBalance) ParseAccount() {
|
||||
}
|
||||
|
||||
type Meta struct {
|
||||
Err interface{} `json:"err"`
|
||||
Fee uint64 `json:"fee"`
|
||||
InnerInstructions []InnerInstructions `json:"innerInstructions"`
|
||||
LoadedAddresses LoadedAddresses `json:"loadedAddresses"`
|
||||
LogMessages []string `json:"logMessages"`
|
||||
PostBalances []uint64 `json:"postBalances"`
|
||||
PostTokenBalances []TokenBalance `json:"postTokenBalances"`
|
||||
PreBalances []uint64 `json:"preBalances"`
|
||||
PreTokenBalances []TokenBalance `json:"preTokenBalances"`
|
||||
Rewards []interface{} `json:"rewards"`
|
||||
Err *TransactionParsedError `json:"err"`
|
||||
Fee uint64 `json:"fee"`
|
||||
InnerInstructions []InnerInstructions `json:"innerInstructions"`
|
||||
LoadedAddresses LoadedAddresses `json:"loadedAddresses"`
|
||||
LogMessages []string `json:"logMessages"`
|
||||
PostBalances []uint64 `json:"postBalances"`
|
||||
PostTokenBalances []TokenBalance `json:"postTokenBalances"`
|
||||
PreBalances []uint64 `json:"preBalances"`
|
||||
PreTokenBalances []TokenBalance `json:"preTokenBalances"`
|
||||
Rewards []interface{} `json:"rewards"`
|
||||
ComputeUnitsConsumed uint64 `json:"computeUnitsConsumed"`
|
||||
}
|
||||
type Header struct {
|
||||
NumReadonlySignedAccounts int `json:"numReadonlySignedAccounts"`
|
||||
@@ -174,6 +185,11 @@ type Transaction struct {
|
||||
Signatures []solana.Signature `json:"signatures"`
|
||||
}
|
||||
|
||||
type RawTxConvertOptions struct {
|
||||
IgnoreLogMessages bool
|
||||
ParseLogEvents bool
|
||||
}
|
||||
|
||||
func (tx *Transaction) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 || (len(data) == 4 && string(data) == "null") {
|
||||
// TODO: is this an error?
|
||||
@@ -292,6 +308,267 @@ 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, options ...RawTxConvertOptions) (*RawTx, error) {
|
||||
option := rawTxConvertOption(options)
|
||||
created := int64(0)
|
||||
if blockTime != nil {
|
||||
created = int64(*blockTime)
|
||||
}
|
||||
sTx := &RawTx{
|
||||
BlockTime: created,
|
||||
Slot: slot,
|
||||
IndexWithinBlock: index,
|
||||
Meta: Meta{
|
||||
Err: nil,
|
||||
Fee: 0,
|
||||
InnerInstructions: nil,
|
||||
LoadedAddresses: LoadedAddresses{},
|
||||
LogMessages: nil,
|
||||
PostBalances: nil,
|
||||
PostTokenBalances: nil,
|
||||
PreBalances: nil,
|
||||
PreTokenBalances: nil,
|
||||
Rewards: nil,
|
||||
},
|
||||
}
|
||||
|
||||
meta := tx.Meta
|
||||
yTx, _ := tx.GetTransaction()
|
||||
|
||||
if meta.Err != nil {
|
||||
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 {
|
||||
instrs = append(instrs, Instruction{
|
||||
ProgramIDIndex: int(instr.ProgramIDIndex),
|
||||
Accounts: func() []int {
|
||||
var out []int
|
||||
for i := range instr.Accounts {
|
||||
out = append(out, int(instr.Accounts[i]))
|
||||
}
|
||||
return out
|
||||
}(),
|
||||
Data: instr.Data,
|
||||
StackHeight: newInt16(instr.StackHeight),
|
||||
})
|
||||
}
|
||||
sTx.Meta.InnerInstructions = append(sTx.Meta.InnerInstructions, InnerInstructions{
|
||||
Index: int(innerInstr.Index),
|
||||
Instructions: instrs,
|
||||
})
|
||||
}
|
||||
sTx.Meta.LogMessages = meta.LogMessages
|
||||
sTx.Meta.PostBalances = meta.PostBalances
|
||||
sTx.Meta.PreBalances = meta.PreBalances
|
||||
sTx.Meta.PostTokenBalances = convertTokenBalanceFromRpc(meta.PostTokenBalances)
|
||||
sTx.Meta.PreTokenBalances = convertTokenBalanceFromRpc(meta.PreTokenBalances)
|
||||
sTx.Meta.Rewards = nil
|
||||
sTx.Meta.LoadedAddresses.Readonly = meta.LoadedAddresses.ReadOnly
|
||||
sTx.Meta.LoadedAddresses.Writable = meta.LoadedAddresses.Writable
|
||||
|
||||
// copy signatures
|
||||
for i := range yTx.Signatures {
|
||||
sTx.Transaction.Signatures = append(sTx.Transaction.Signatures, yTx.Signatures[i])
|
||||
}
|
||||
// copy message
|
||||
sTx.Transaction.Message = Message{
|
||||
RecentBlockHash: yTx.Message.RecentBlockhash.String(),
|
||||
}
|
||||
// copy message.AccountKeys
|
||||
//stopAt := len(yTx.Message.AccountKeys) - sTx.Message.NumLookups()
|
||||
stopAt := len(yTx.Message.AccountKeys)
|
||||
for accIndex, acc := range yTx.Message.AccountKeys {
|
||||
sTx.Transaction.Message.AccountKeys = append(sTx.Transaction.Message.AccountKeys, acc)
|
||||
if accIndex == stopAt-1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// copy message.Header
|
||||
sTx.Transaction.Message.Header = Header{
|
||||
NumRequiredSignatures: int(yTx.Message.Header.NumRequiredSignatures),
|
||||
NumReadonlySignedAccounts: int(yTx.Message.Header.NumReadonlySignedAccounts),
|
||||
NumReadonlyUnsignedAccounts: int(yTx.Message.Header.NumReadonlyUnsignedAccounts),
|
||||
}
|
||||
|
||||
// copy message.versioned
|
||||
if yTx.Message.IsVersioned() {
|
||||
sTx.Version = solana.MessageVersionV0
|
||||
} else {
|
||||
sTx.Version = solana.MessageVersionLegacy
|
||||
}
|
||||
|
||||
// copy address table lookups
|
||||
{
|
||||
tables := map[solana.PublicKey]solana.PublicKeySlice{}
|
||||
writable := meta.LoadedAddresses.Writable
|
||||
readonly := meta.LoadedAddresses.ReadOnly
|
||||
for _, addr := range yTx.Message.AddressTableLookups {
|
||||
sTx.Transaction.Message.AddressTableLookups = append(sTx.Transaction.Message.AddressTableLookups, solana.MessageAddressTableLookup{
|
||||
AccountKey: addr.AccountKey,
|
||||
WritableIndexes: addr.WritableIndexes,
|
||||
ReadonlyIndexes: addr.ReadonlyIndexes,
|
||||
})
|
||||
numTakeWritable := len(addr.WritableIndexes)
|
||||
numTakeReadonly := len(addr.ReadonlyIndexes)
|
||||
tableKey := addr.AccountKey
|
||||
{
|
||||
// now need to rebuild the address table taking into account the indexes, and put the keys into the tables
|
||||
maxIndex := 0
|
||||
for _, indexB := range addr.WritableIndexes {
|
||||
index := int(indexB)
|
||||
if index > maxIndex {
|
||||
maxIndex = index
|
||||
}
|
||||
}
|
||||
for _, indexB := range addr.ReadonlyIndexes {
|
||||
index := int(indexB)
|
||||
if index > maxIndex {
|
||||
maxIndex = index
|
||||
}
|
||||
}
|
||||
tables[tableKey] = make([]solana.PublicKey, maxIndex+1)
|
||||
}
|
||||
if numTakeWritable > 0 {
|
||||
writableForTable := writable[:numTakeWritable]
|
||||
for i, indexB := range addr.WritableIndexes {
|
||||
index := int(indexB)
|
||||
tables[tableKey][index] = writableForTable[i]
|
||||
}
|
||||
writable = writable[numTakeWritable:]
|
||||
}
|
||||
if numTakeReadonly > 0 {
|
||||
readableForTable := readonly[:numTakeReadonly]
|
||||
for i, indexB := range addr.ReadonlyIndexes {
|
||||
index := int(indexB)
|
||||
tables[tableKey][index] = readableForTable[i]
|
||||
}
|
||||
readonly = readonly[numTakeReadonly:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// copy instructions
|
||||
for _, instr := range yTx.Message.Instructions {
|
||||
sTx.Transaction.Message.Instructions = append(sTx.Transaction.Message.Instructions, Instruction{
|
||||
ProgramIDIndex: int(instr.ProgramIDIndex),
|
||||
Accounts: func() []int {
|
||||
var out []int
|
||||
for i := range instr.Accounts {
|
||||
out = append(out, int(instr.Accounts[i]))
|
||||
}
|
||||
return out
|
||||
}(),
|
||||
Data: instr.Data,
|
||||
})
|
||||
}
|
||||
|
||||
applyRawTxConvertLogOptions(sTx, option)
|
||||
|
||||
return sTx, nil
|
||||
}
|
||||
|
||||
func convertTokenBalanceFromRpc(tb []rpc.TokenBalance) []TokenBalance {
|
||||
var tokenBalances []TokenBalance = make([]TokenBalance, len(tb))
|
||||
for i, balance := range tb {
|
||||
var uiAmount = float64(0)
|
||||
if balance.UiTokenAmount.UiAmount != nil {
|
||||
uiAmount = *balance.UiTokenAmount.UiAmount
|
||||
}
|
||||
tokenBalances[i] = TokenBalance{
|
||||
AccountIndex: int(balance.AccountIndex),
|
||||
MintAccount: balance.Mint,
|
||||
OwnerAccount: balance.Owner,
|
||||
ProgramIDAccount: func() solana.PublicKey {
|
||||
if balance.ProgramId != nil {
|
||||
return *balance.ProgramId
|
||||
}
|
||||
return solana.PublicKey{}
|
||||
}(),
|
||||
UITokenAmount: UITokenAmount{
|
||||
Amount: balance.UiTokenAmount.Amount,
|
||||
Decimals: uint64(balance.UiTokenAmount.Decimals),
|
||||
UIAmount: uiAmount,
|
||||
UIAmountString: balance.UiTokenAmount.UiAmountString,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return tokenBalances
|
||||
|
||||
}
|
||||
|
||||
func InnerInstructionsFromRpc(instructions []rpc.InnerInstruction) []InnerInstructions {
|
||||
var innerInstructions []InnerInstructions = make([]InnerInstructions, len(instructions))
|
||||
for i, instruction := range instructions {
|
||||
@@ -321,6 +598,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 {
|
||||
@@ -374,6 +724,53 @@ func getAccountBalanceAfterTx(result *RawTx, accountIndex int) decimal.Decimal {
|
||||
return amount
|
||||
}
|
||||
|
||||
func tokenBalanceChange(result *RawTx, accountIndex int, tokenProgram, mint solana.PublicKey) (change decimal.Decimal, ataIndex int) {
|
||||
ataAccount, _, _ := solana.FindProgramAddress([][]byte{
|
||||
result.accountList[accountIndex][:],
|
||||
tokenProgram[:],
|
||||
mint[:],
|
||||
},
|
||||
solana.SPLAssociatedTokenAccountProgramID)
|
||||
|
||||
for i, account := range result.accountList {
|
||||
if account.Equals(ataAccount) {
|
||||
ataIndex = i
|
||||
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
|
||||
|
||||
for _, pre := range result.Meta.PreTokenBalances {
|
||||
if pre.AccountIndex == ataIndex {
|
||||
amount, err := decimal.NewFromString(pre.UITokenAmount.Amount)
|
||||
if err != nil {
|
||||
return decimal.Zero, ataIndex
|
||||
}
|
||||
before = amount
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, post := range result.Meta.PostTokenBalances {
|
||||
if post.AccountIndex == ataIndex {
|
||||
amount, err := decimal.NewFromString(post.UITokenAmount.Amount)
|
||||
if err != nil {
|
||||
return decimal.Zero, ataIndex
|
||||
}
|
||||
after = amount
|
||||
break
|
||||
}
|
||||
}
|
||||
return after.Sub(before).Abs(), ataIndex
|
||||
}
|
||||
|
||||
func GetTokenBalanceAfterTx(result *RawTx, accountIndex int, tokenProgram, mint solana.PublicKey) decimal.Decimal {
|
||||
ataAccount, _, _ := solana.FindProgramAddress([][]byte{
|
||||
result.accountList[accountIndex][:],
|
||||
@@ -389,10 +786,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
|
||||
}
|
||||
@@ -445,3 +845,415 @@ func isAccountOwner(account, owner, mint solana.PublicKey) (bool, error) {
|
||||
}
|
||||
return account == ata, nil
|
||||
}
|
||||
|
||||
func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateTransaction, created int64, options ...RawTxConvertOptions) (*RawTx, error) {
|
||||
option := rawTxConvertOption(options)
|
||||
sTx := &RawTx{
|
||||
BlockTime: created,
|
||||
Slot: y.Slot,
|
||||
IndexWithinBlock: int64(y.Transaction.Index),
|
||||
Meta: Meta{
|
||||
Err: nil,
|
||||
Fee: 0,
|
||||
InnerInstructions: nil,
|
||||
LoadedAddresses: LoadedAddresses{},
|
||||
LogMessages: nil,
|
||||
PostBalances: nil,
|
||||
PostTokenBalances: nil,
|
||||
PreBalances: nil,
|
||||
PreTokenBalances: nil,
|
||||
Rewards: nil,
|
||||
},
|
||||
//Transaction: types.Transaction{
|
||||
// Message: types.Message{
|
||||
// AccountKeys: nil,
|
||||
// AddressTableLookups: nil,
|
||||
// Header: types.Header{},
|
||||
// Instructions: nil,
|
||||
// RecentBlockHash: "",
|
||||
// },
|
||||
// Signatures: nil,
|
||||
//},
|
||||
//Version: nil,
|
||||
}
|
||||
meta := y.Transaction.GetMeta()
|
||||
if meta == nil {
|
||||
return nil, errors.New("meta can not parser")
|
||||
}
|
||||
yTx := y.Transaction.Transaction
|
||||
|
||||
if meta.Err != nil && len(meta.Err.GetErr()) > 0 {
|
||||
// If the transaction has an error, we set the error in the Meta
|
||||
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 {
|
||||
instrs = append(instrs, Instruction{
|
||||
ProgramIDIndex: int(instr.ProgramIdIndex),
|
||||
Accounts: func() []int {
|
||||
var out []int
|
||||
for i := range instr.Accounts {
|
||||
out = append(out, int(instr.Accounts[i]))
|
||||
}
|
||||
return out
|
||||
}(),
|
||||
Data: instr.Data,
|
||||
StackHeight: newInt(instr.StackHeight),
|
||||
})
|
||||
}
|
||||
sTx.Meta.InnerInstructions = append(sTx.Meta.InnerInstructions, InnerInstructions{
|
||||
Index: int(innerInstr.Index),
|
||||
Instructions: instrs,
|
||||
})
|
||||
}
|
||||
sTx.Meta.LogMessages = meta.LogMessages
|
||||
sTx.Meta.PostBalances = meta.PostBalances
|
||||
sTx.Meta.PostTokenBalances = grpcTokenBalance(meta.PostTokenBalances)
|
||||
sTx.Meta.PreBalances = meta.PreBalances
|
||||
sTx.Meta.PreTokenBalances = grpcTokenBalance(meta.PreTokenBalances)
|
||||
sTx.Meta.Rewards = nil
|
||||
sTx.Meta.LoadedAddresses.Readonly = byteSlicesToKeySlices(meta.LoadedReadonlyAddresses)
|
||||
sTx.Meta.LoadedAddresses.Writable = byteSlicesToKeySlices(meta.LoadedWritableAddresses)
|
||||
|
||||
// copy signatures
|
||||
for i := range yTx.Signatures {
|
||||
sTx.Transaction.Signatures = append(sTx.Transaction.Signatures, solana.SignatureFromBytes(yTx.Signatures[i]))
|
||||
}
|
||||
// copy message
|
||||
sTx.Transaction.Message = Message{
|
||||
RecentBlockHash: solana.HashFromBytes(yTx.Message.RecentBlockhash).String(),
|
||||
}
|
||||
// copy message.AccountKeys
|
||||
//stopAt := len(yTx.Message.AccountKeys) - sTx.Message.NumLookups()
|
||||
stopAt := len(yTx.Message.AccountKeys)
|
||||
for accIndex, acc := range yTx.Message.AccountKeys {
|
||||
sTx.Transaction.Message.AccountKeys = append(sTx.Transaction.Message.AccountKeys, solana.PublicKeyFromBytes(acc))
|
||||
if accIndex == stopAt-1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// copy message.Header
|
||||
sTx.Transaction.Message.Header = Header{
|
||||
NumRequiredSignatures: int(yTx.Message.Header.NumRequiredSignatures),
|
||||
NumReadonlySignedAccounts: int(yTx.Message.Header.NumReadonlySignedAccounts),
|
||||
NumReadonlyUnsignedAccounts: int(yTx.Message.Header.NumReadonlyUnsignedAccounts),
|
||||
}
|
||||
|
||||
// copy message.versioned
|
||||
if yTx.Message.Versioned {
|
||||
sTx.Version = solana.MessageVersionV0
|
||||
} else {
|
||||
sTx.Version = solana.MessageVersionLegacy
|
||||
}
|
||||
|
||||
// copy address table lookups
|
||||
{
|
||||
tables := map[solana.PublicKey]solana.PublicKeySlice{}
|
||||
writable := byteSlicesToKeySlices(meta.LoadedWritableAddresses)
|
||||
readonly := byteSlicesToKeySlices(meta.LoadedReadonlyAddresses)
|
||||
for _, addr := range yTx.Message.AddressTableLookups {
|
||||
sTx.Transaction.Message.AddressTableLookups = append(sTx.Transaction.Message.AddressTableLookups, solana.MessageAddressTableLookup{
|
||||
AccountKey: solana.PublicKeyFromBytes(addr.AccountKey),
|
||||
WritableIndexes: addr.WritableIndexes,
|
||||
ReadonlyIndexes: addr.ReadonlyIndexes,
|
||||
})
|
||||
numTakeWritable := len(addr.WritableIndexes)
|
||||
numTakeReadonly := len(addr.ReadonlyIndexes)
|
||||
tableKey := solana.PublicKeyFromBytes(addr.AccountKey)
|
||||
{
|
||||
// now need to rebuild the address table taking into account the indexes, and put the keys into the tables
|
||||
maxIndex := 0
|
||||
for _, indexB := range addr.WritableIndexes {
|
||||
index := int(indexB)
|
||||
if index > maxIndex {
|
||||
maxIndex = index
|
||||
}
|
||||
}
|
||||
for _, indexB := range addr.ReadonlyIndexes {
|
||||
index := int(indexB)
|
||||
if index > maxIndex {
|
||||
maxIndex = index
|
||||
}
|
||||
}
|
||||
tables[tableKey] = make([]solana.PublicKey, maxIndex+1)
|
||||
}
|
||||
if numTakeWritable > 0 {
|
||||
writableForTable := writable[:numTakeWritable]
|
||||
for i, indexB := range addr.WritableIndexes {
|
||||
index := int(indexB)
|
||||
tables[tableKey][index] = writableForTable[i]
|
||||
}
|
||||
writable = writable[numTakeWritable:]
|
||||
}
|
||||
if numTakeReadonly > 0 {
|
||||
readableForTable := readonly[:numTakeReadonly]
|
||||
for i, indexB := range addr.ReadonlyIndexes {
|
||||
index := int(indexB)
|
||||
tables[tableKey][index] = readableForTable[i]
|
||||
}
|
||||
readonly = readonly[numTakeReadonly:]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// copy instructions
|
||||
for _, instr := range yTx.Message.Instructions {
|
||||
sTx.Transaction.Message.Instructions = append(sTx.Transaction.Message.Instructions, Instruction{
|
||||
ProgramIDIndex: int(instr.ProgramIdIndex),
|
||||
Accounts: func() []int {
|
||||
var out []int
|
||||
for i := range instr.Accounts {
|
||||
out = append(out, int(instr.Accounts[i]))
|
||||
}
|
||||
return out
|
||||
}(),
|
||||
Data: instr.Data,
|
||||
})
|
||||
}
|
||||
|
||||
applyRawTxConvertLogOptions(sTx, option)
|
||||
|
||||
// resolve the lookups
|
||||
//{
|
||||
// if sTx.Transaction.Message.IsVersioned() {
|
||||
// // only versioned transactions have address table lookups
|
||||
// err := sTx.Transaction.Message.ResolveLookups()
|
||||
// if err != nil {
|
||||
// return sTx, fmt.Errorf("failed to resolve lookups: %w", err)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
return sTx, nil
|
||||
}
|
||||
|
||||
func newInt16(x uint16) *int {
|
||||
y := int(x)
|
||||
return &y
|
||||
}
|
||||
|
||||
func rawTxConvertOption(options []RawTxConvertOptions) RawTxConvertOptions {
|
||||
out := RawTxConvertOptions{ParseLogEvents: true}
|
||||
if len(options) > 0 {
|
||||
out = options[0]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func applyRawTxConvertLogOptions(tx *RawTx, option RawTxConvertOptions) {
|
||||
if tx == nil {
|
||||
return
|
||||
}
|
||||
if option.ParseLogEvents {
|
||||
attachLogEventsToInstructions(tx, tx.Meta.LogMessages)
|
||||
}
|
||||
if option.IgnoreLogMessages {
|
||||
tx.Meta.LogMessages = nil
|
||||
}
|
||||
}
|
||||
|
||||
type instructionLogFrame struct {
|
||||
program string
|
||||
instr *Instruction
|
||||
}
|
||||
|
||||
type instructionLogTarget struct {
|
||||
program string
|
||||
stackHeight int
|
||||
instr *Instruction
|
||||
}
|
||||
|
||||
func attachLogEventsToInstructions(tx *RawTx, logMessages []string) {
|
||||
if tx == nil || len(logMessages) == 0 {
|
||||
return
|
||||
}
|
||||
targets := rawTxInstructionLogTargets(tx)
|
||||
if len(targets) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
nextTarget := 0
|
||||
var stack []instructionLogFrame
|
||||
for _, logMessage := range logMessages {
|
||||
if program, stackHeight, ok := parseProgramInvokeLog(logMessage); ok {
|
||||
var instr *Instruction
|
||||
for nextTarget < len(targets) {
|
||||
target := targets[nextTarget]
|
||||
nextTarget++
|
||||
if target.program != program {
|
||||
continue
|
||||
}
|
||||
if target.stackHeight != 0 && target.stackHeight != stackHeight {
|
||||
continue
|
||||
}
|
||||
instr = target.instr
|
||||
break
|
||||
}
|
||||
stack = append(stack, instructionLogFrame{program: program, instr: instr})
|
||||
continue
|
||||
}
|
||||
|
||||
if data, ok := parseProgramDataLog(logMessage); ok {
|
||||
if len(stack) == 0 {
|
||||
continue
|
||||
}
|
||||
top := stack[len(stack)-1]
|
||||
if top.instr != nil {
|
||||
top.instr.LogEvents = append(top.instr.LogEvents, data)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if program, ok := parseProgramFinishedLog(logMessage); ok {
|
||||
for i := len(stack) - 1; i >= 0; i-- {
|
||||
if stack[i].program == program {
|
||||
stack = stack[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rawTxInstructionLogTargets(tx *RawTx) []instructionLogTarget {
|
||||
accountList := tx.getAccountList()
|
||||
innerByOuter := make(map[int]*InnerInstructions, len(tx.Meta.InnerInstructions))
|
||||
for i := range tx.Meta.InnerInstructions {
|
||||
inner := &tx.Meta.InnerInstructions[i]
|
||||
innerByOuter[inner.Index] = inner
|
||||
}
|
||||
|
||||
out := make([]instructionLogTarget, 0, len(tx.Transaction.Message.Instructions))
|
||||
for i := range tx.Transaction.Message.Instructions {
|
||||
instr := &tx.Transaction.Message.Instructions[i]
|
||||
if instr.ProgramIDIndex >= 0 && instr.ProgramIDIndex < len(accountList) {
|
||||
out = append(out, instructionLogTarget{
|
||||
program: accountList[instr.ProgramIDIndex].String(),
|
||||
stackHeight: 1,
|
||||
instr: instr,
|
||||
})
|
||||
}
|
||||
if inner := innerByOuter[i]; inner != nil {
|
||||
for j := range inner.Instructions {
|
||||
innerInstr := &inner.Instructions[j]
|
||||
if innerInstr.ProgramIDIndex < 0 || innerInstr.ProgramIDIndex >= len(accountList) {
|
||||
continue
|
||||
}
|
||||
stackHeight := 0
|
||||
if innerInstr.StackHeight != nil {
|
||||
stackHeight = *innerInstr.StackHeight
|
||||
}
|
||||
out = append(out, instructionLogTarget{
|
||||
program: accountList[innerInstr.ProgramIDIndex].String(),
|
||||
stackHeight: stackHeight,
|
||||
instr: innerInstr,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseProgramInvokeLog(logMessage string) (string, int, bool) {
|
||||
if !strings.HasPrefix(logMessage, "Program ") {
|
||||
return "", 0, false
|
||||
}
|
||||
rest := strings.TrimPrefix(logMessage, "Program ")
|
||||
program, suffix, ok := strings.Cut(rest, " invoke [")
|
||||
if !ok {
|
||||
return "", 0, false
|
||||
}
|
||||
suffix = strings.TrimSuffix(suffix, "]")
|
||||
stackHeight, err := strconv.Atoi(suffix)
|
||||
if err != nil {
|
||||
return "", 0, false
|
||||
}
|
||||
return program, stackHeight, true
|
||||
}
|
||||
|
||||
func parseProgramDataLog(logMessage string) (solana.Base64, bool) {
|
||||
if !strings.HasPrefix(logMessage, "Program data: ") {
|
||||
return nil, false
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(strings.TrimSpace(strings.TrimPrefix(logMessage, "Program data: ")))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return solana.Base64(data), true
|
||||
}
|
||||
|
||||
func parseProgramFinishedLog(logMessage string) (string, bool) {
|
||||
if !strings.HasPrefix(logMessage, "Program ") {
|
||||
return "", false
|
||||
}
|
||||
if !strings.HasSuffix(logMessage, " success") && !strings.Contains(logMessage, " failed:") {
|
||||
return "", false
|
||||
}
|
||||
rest := strings.TrimPrefix(logMessage, "Program ")
|
||||
program, _, ok := strings.Cut(rest, " ")
|
||||
return program, ok
|
||||
}
|
||||
|
||||
func newInt(x *uint32) *int {
|
||||
if x == nil {
|
||||
return nil
|
||||
}
|
||||
y := int(*x)
|
||||
return &y
|
||||
}
|
||||
|
||||
func byteSlicesToKeySlices(keys [][]byte) []solana.PublicKey {
|
||||
var out []solana.PublicKey
|
||||
for _, key := range keys {
|
||||
var k solana.PublicKey
|
||||
copy(k[:], key)
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func grpcTokenBalance(src []*pb.TokenBalance) []TokenBalance {
|
||||
out := make([]TokenBalance, len(src))
|
||||
for i, tb := range src {
|
||||
var (
|
||||
mintAccount solana.PublicKey
|
||||
ownerAccount solana.PublicKey
|
||||
programIDAccount solana.PublicKey
|
||||
)
|
||||
|
||||
if tb.Mint != "" {
|
||||
mintAccount, _ = solana.PublicKeyFromBase58(tb.Mint)
|
||||
}
|
||||
if tb.Owner != "" {
|
||||
ownerAccount, _ = solana.PublicKeyFromBase58(tb.Owner)
|
||||
}
|
||||
if tb.ProgramId != "" {
|
||||
programIDAccount, _ = solana.PublicKeyFromBase58(tb.ProgramId)
|
||||
}
|
||||
|
||||
out[i] = TokenBalance{
|
||||
AccountIndex: int(tb.AccountIndex),
|
||||
MintAccount: mintAccount,
|
||||
OwnerAccount: &ownerAccount,
|
||||
ProgramIDAccount: programIDAccount,
|
||||
Mint: tb.Mint,
|
||||
Owner: tb.Owner,
|
||||
ProgramID: tb.ProgramId,
|
||||
UITokenAmount: UITokenAmount{
|
||||
Amount: tb.UiTokenAmount.Amount,
|
||||
Decimals: uint64(tb.UiTokenAmount.Decimals),
|
||||
UIAmount: tb.UiTokenAmount.UiAmount,
|
||||
UIAmountString: tb.UiTokenAmount.UiAmountString,
|
||||
},
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
1812
rawtx_binary.go
Normal file
1812
rawtx_binary.go
Normal file
File diff suppressed because it is too large
Load Diff
434
rawtx_binary_test.go
Normal file
434
rawtx_binary_test.go
Normal file
@@ -0,0 +1,434 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
"github.com/gagliardetto/solana-go/rpc"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func TestRawTxBinaryRoundTripRealFixture(t *testing.T) {
|
||||
original := mustLoadRawTxFixture(t, "testdata/rpc/4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri.json")
|
||||
|
||||
encoded, err := EncodeRawTxBinary(original)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeRawTxBinary() error = %v", err)
|
||||
}
|
||||
|
||||
decoded, err := DecodeRawTxBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxBinary() error = %v", err)
|
||||
}
|
||||
|
||||
assertRawTxAccountAccess(t, decoded)
|
||||
if decoded.TxHash() != original.TxHash() {
|
||||
t.Fatalf("TxHash = %s, want %s", decoded.TxHash(), original.TxHash())
|
||||
}
|
||||
if decoded.Slot != original.Slot {
|
||||
t.Fatalf("Slot = %d, want %d", decoded.Slot, original.Slot)
|
||||
}
|
||||
if decoded.IndexWithinBlock != original.IndexWithinBlock {
|
||||
t.Fatalf("IndexWithinBlock = %d, want %d", decoded.IndexWithinBlock, original.IndexWithinBlock)
|
||||
}
|
||||
if len(decoded.Meta.PostTokenBalances) != len(original.Meta.PostTokenBalances) {
|
||||
t.Fatalf("PostTokenBalances len = %d, want %d", len(decoded.Meta.PostTokenBalances), len(original.Meta.PostTokenBalances))
|
||||
}
|
||||
if len(decoded.Meta.PostTokenBalances) > 0 {
|
||||
got := decoded.Meta.PostTokenBalances[0]
|
||||
want := original.Meta.PostTokenBalances[0]
|
||||
if got.AccountIndex != want.AccountIndex {
|
||||
t.Fatalf("token balance account index = %d, want %d", got.AccountIndex, want.AccountIndex)
|
||||
}
|
||||
wantMint := want.MintAccount
|
||||
if wantMint.IsZero() && want.Mint != "" {
|
||||
var err error
|
||||
wantMint, err = solana.PublicKeyFromBase58(want.Mint)
|
||||
if err != nil {
|
||||
t.Fatalf("parse want mint: %v", err)
|
||||
}
|
||||
}
|
||||
if got.MintAccount != wantMint {
|
||||
t.Fatalf("token balance mint = %s, want %s", got.MintAccount, wantMint)
|
||||
}
|
||||
if got.UITokenAmount.Decimals != want.UITokenAmount.Decimals {
|
||||
t.Fatalf("token balance decimals = %d, want %d", got.UITokenAmount.Decimals, want.UITokenAmount.Decimals)
|
||||
}
|
||||
if got.UITokenAmount.Amount != want.UITokenAmount.Amount {
|
||||
t.Fatalf("token balance amount = %s, want %s", got.UITokenAmount.Amount, want.UITokenAmount.Amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawTxsBinaryBatchAndStreamRoundTrip(t *testing.T) {
|
||||
tx1 := mustLoadRawTxFixture(t, "testdata/rpc/4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri.json")
|
||||
tx2 := mustLoadRawTxFixture(t, "testdata/rpc/43EouSYkeVmLBZSKW1KptiQcAVvB6KX49wjztjgzkQ9iU38A6fF68k77bNy9Wn6fwjykqYsPorUKj8m6SFY7naf1.json")
|
||||
tx2.BlockTime = tx1.BlockTime
|
||||
original := []RawTx{*tx1, *tx2}
|
||||
|
||||
encoded, err := EncodeRawTxsBinary(original)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeRawTxsBinary() error = %v", err)
|
||||
}
|
||||
|
||||
decoded, err := DecodeRawTxsBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxsBinary() error = %v", err)
|
||||
}
|
||||
if len(decoded) != len(original) {
|
||||
t.Fatalf("DecodeRawTxsBinary len = %d, want %d", len(decoded), len(original))
|
||||
}
|
||||
for i := range decoded {
|
||||
assertRawTxAccountAccess(t, decoded[i])
|
||||
if decoded[i].TxHash() != original[i].TxHash() {
|
||||
t.Fatalf("decoded[%d].TxHash = %s, want %s", i, decoded[i].TxHash(), original[i].TxHash())
|
||||
}
|
||||
if decoded[i].BlockTime != original[i].BlockTime {
|
||||
t.Fatalf("decoded[%d].BlockTime = %d, want %d", i, decoded[i].BlockTime, original[i].BlockTime)
|
||||
}
|
||||
}
|
||||
|
||||
var streamed int
|
||||
for decodedTx, err := range DecodeRawTxsBinaryReader(bytes.NewReader(encoded)) {
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxsBinaryReader() error = %v", err)
|
||||
}
|
||||
assertRawTxAccountAccess(t, decodedTx)
|
||||
streamed++
|
||||
}
|
||||
if streamed != len(original) {
|
||||
t.Fatalf("streamed tx count = %d, want %d", streamed, len(original))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawTxBlocksBinaryRoundTrip(t *testing.T) {
|
||||
tx1 := mustLoadRawTxFixture(t, "testdata/rpc/4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri.json")
|
||||
tx2 := mustLoadRawTxFixture(t, "testdata/rpc/43EouSYkeVmLBZSKW1KptiQcAVvB6KX49wjztjgzkQ9iU38A6fF68k77bNy9Wn6fwjykqYsPorUKj8m6SFY7naf1.json")
|
||||
blocks := [][]RawTx{{*tx1}, {*tx2}}
|
||||
|
||||
encoded, err := EncodeRawTxBlocksBinary(blocks)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeRawTxBlocksBinary() error = %v", err)
|
||||
}
|
||||
|
||||
decoded, err := DecodeRawTxBlocksBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxBlocksBinary() error = %v", err)
|
||||
}
|
||||
if len(decoded) != len(blocks) {
|
||||
t.Fatalf("block count = %d, want %d", len(decoded), len(blocks))
|
||||
}
|
||||
for blockIndex := range decoded {
|
||||
if len(decoded[blockIndex]) != len(blocks[blockIndex]) {
|
||||
t.Fatalf("block[%d] tx count = %d, want %d", blockIndex, len(decoded[blockIndex]), len(blocks[blockIndex]))
|
||||
}
|
||||
for txIndex := range decoded[blockIndex] {
|
||||
assertRawTxAccountAccess(t, decoded[blockIndex][txIndex])
|
||||
if decoded[blockIndex][txIndex].TxHash() != blocks[blockIndex][txIndex].TxHash() {
|
||||
t.Fatalf("block[%d].tx[%d] hash mismatch", blockIndex, txIndex)
|
||||
}
|
||||
if decoded[blockIndex][txIndex].BlockTime != blocks[blockIndex][txIndex].BlockTime {
|
||||
t.Fatalf("block[%d].tx[%d] block time = %d, want %d", blockIndex, txIndex, decoded[blockIndex][txIndex].BlockTime, blocks[blockIndex][txIndex].BlockTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawTxBinaryPreservesAccountListHelperBehavior(t *testing.T) {
|
||||
owner := mustPubKey("SysvarRent111111111111111111111111111111111")
|
||||
mint := solana.WrappedSol
|
||||
tokenProgram := solana.TokenProgramID
|
||||
ata, _, err := solana.FindProgramAddress([][]byte{
|
||||
owner[:],
|
||||
tokenProgram[:],
|
||||
mint[:],
|
||||
}, solana.SPLAssociatedTokenAccountProgramID)
|
||||
if err != nil {
|
||||
t.Fatalf("find ata: %v", err)
|
||||
}
|
||||
|
||||
original := &RawTx{
|
||||
accountList: []solana.PublicKey{owner, ata, mint, tokenProgram},
|
||||
BlockTime: 1710000000,
|
||||
Slot: 123,
|
||||
IndexWithinBlock: 7,
|
||||
Transaction: Transaction{Signatures: []solana.Signature{{1, 2, 3}}},
|
||||
Meta: Meta{
|
||||
PreBalances: []uint64{2_000_000_000, 0, 0, 0},
|
||||
PostBalances: []uint64{1_500_000_000, 0, 0, 0},
|
||||
PreTokenBalances: []TokenBalance{{
|
||||
AccountIndex: 1,
|
||||
MintAccount: mint,
|
||||
OwnerAccount: &owner,
|
||||
ProgramIDAccount: tokenProgram,
|
||||
UITokenAmount: UITokenAmount{
|
||||
Amount: "100",
|
||||
Decimals: 9,
|
||||
UIAmount: 0.0000001,
|
||||
UIAmountString: "0.0000001",
|
||||
},
|
||||
}},
|
||||
PostTokenBalances: []TokenBalance{{
|
||||
AccountIndex: 1,
|
||||
MintAccount: mint,
|
||||
OwnerAccount: &owner,
|
||||
ProgramIDAccount: tokenProgram,
|
||||
UITokenAmount: UITokenAmount{
|
||||
Amount: "250",
|
||||
Decimals: 9,
|
||||
UIAmount: 0.00000025,
|
||||
UIAmountString: "0.00000025",
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
binaryForm, err := NewRawTxBinary(original)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRawTxBinary() error = %v", err)
|
||||
}
|
||||
if len(binaryForm.Meta.TokenBalances) != 1 {
|
||||
t.Fatalf("binary token balance count = %d, want 1", len(binaryForm.Meta.TokenBalances))
|
||||
}
|
||||
if got := binaryForm.Meta.TokenBalances[0]; !got.HasPreAmount || !got.HasPostAmount || got.PreAmount != "100" || got.PostAmount != "250" {
|
||||
t.Fatalf("merged binary token balance = %+v", got)
|
||||
}
|
||||
|
||||
encoded, err := EncodeRawTxBinary(original)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeRawTxBinary() error = %v", err)
|
||||
}
|
||||
decoded, err := DecodeRawTxBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxBinary() error = %v", err)
|
||||
}
|
||||
|
||||
for _, tx := range []*RawTx{original, decoded} {
|
||||
if got, err := GetSolAfterTx(tx, 0); err != nil || got != 1_500_000_000 {
|
||||
t.Fatalf("GetSolAfterTx() = %d, %v; want 1500000000, nil", got, err)
|
||||
}
|
||||
balance, err := getTokenBalanceAfterTx(tx, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("getTokenBalanceAfterTx() error = %v", err)
|
||||
}
|
||||
if balance.UITokenAmount.Amount != "250" {
|
||||
t.Fatalf("getTokenBalanceAfterTx amount = %s, want 250", balance.UITokenAmount.Amount)
|
||||
}
|
||||
if got := getAccountBalanceAfterTx(tx, 1); !got.Equal(decimal.NewFromInt(250)) {
|
||||
t.Fatalf("getAccountBalanceAfterTx() = %s, want 250", got)
|
||||
}
|
||||
if got := GetTokenBalanceAfterTx(tx, 0, tokenProgram, mint); !got.Equal(decimal.NewFromInt(250)) {
|
||||
t.Fatalf("GetTokenBalanceAfterTx() = %s, want 250", got)
|
||||
}
|
||||
ataBalance, err := getAtaByOwner(tx, owner, mint)
|
||||
if err != nil {
|
||||
t.Fatalf("getAtaByOwner() error = %v", err)
|
||||
}
|
||||
if ataBalance.AccountIndex != 1 {
|
||||
t.Fatalf("getAtaByOwner account index = %d, want 1", ataBalance.AccountIndex)
|
||||
}
|
||||
ataIndex, err := getAtaIdxByOwner(tx, owner, mint)
|
||||
if err != nil {
|
||||
t.Fatalf("getAtaIdxByOwner() error = %v", err)
|
||||
}
|
||||
if ataIndex != 1 {
|
||||
t.Fatalf("getAtaIdxByOwner() = %d, want 1", ataIndex)
|
||||
}
|
||||
change, changeAtaIndex := tokenBalanceChange(tx, 0, tokenProgram, mint)
|
||||
if !change.Equal(decimal.NewFromInt(150)) || changeAtaIndex != 1 {
|
||||
t.Fatalf("tokenBalanceChange() = (%s, %d), want (150, 1)", change, changeAtaIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawTxBinaryTokenBalanceAmountSupportsUint256(t *testing.T) {
|
||||
owner := mustPubKey("SysvarRent111111111111111111111111111111111")
|
||||
mint := solana.WrappedSol
|
||||
tokenProgram := solana.TokenProgramID
|
||||
amount := "340282366920938463463374607431768211455"
|
||||
original := &RawTx{
|
||||
accountList: []solana.PublicKey{owner, mint, tokenProgram},
|
||||
Transaction: Transaction{
|
||||
Signatures: []solana.Signature{{1, 2, 3}},
|
||||
},
|
||||
Meta: Meta{
|
||||
PreBalances: []uint64{1},
|
||||
PostBalances: []uint64{1},
|
||||
PostTokenBalances: []TokenBalance{{
|
||||
AccountIndex: 0,
|
||||
MintAccount: mint,
|
||||
OwnerAccount: &owner,
|
||||
ProgramIDAccount: tokenProgram,
|
||||
UITokenAmount: UITokenAmount{
|
||||
Amount: amount,
|
||||
Decimals: 9,
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
encoded, err := EncodeRawTxBinary(original)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeRawTxBinary() error = %v", err)
|
||||
}
|
||||
decoded, err := DecodeRawTxBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxBinary() error = %v", err)
|
||||
}
|
||||
got := decoded.Meta.PostTokenBalances[0].UITokenAmount
|
||||
if got.Amount != amount {
|
||||
t.Fatalf("Amount = %s, want %s", got.Amount, amount)
|
||||
}
|
||||
if got.UIAmountString != "340282366920938463463374607431.768211455" {
|
||||
t.Fatalf("UIAmountString = %s", got.UIAmountString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawTxBlocksBinarySaveSlots414696178To414696182(t *testing.T) {
|
||||
rpcURL := os.Getenv("RAWTX_BINARY_RPC_URL")
|
||||
if rpcURL == "" {
|
||||
t.Skip("set RAWTX_BINARY_RPC_URL to run RPC-backed rawtx-binary block save test")
|
||||
}
|
||||
|
||||
const startSlot uint64 = 414696178
|
||||
const endSlot uint64 = 414696182
|
||||
|
||||
client := rpc.New(rpcURL)
|
||||
rewards := false
|
||||
version := uint64(0)
|
||||
blocks := make([][]RawTx, 0, endSlot-startSlot+1)
|
||||
totalTx := 0
|
||||
filteredVote := 0
|
||||
for slot := startSlot; slot <= endSlot; slot++ {
|
||||
block, err := client.GetBlockWithOpts(context.Background(), slot, &rpc.GetBlockOpts{
|
||||
TransactionDetails: rpc.TransactionDetailsFull,
|
||||
Rewards: &rewards,
|
||||
Commitment: rpc.CommitmentFinalized,
|
||||
Encoding: solana.EncodingBase64,
|
||||
MaxSupportedTransactionVersion: &version,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get block %d: %v", slot, err)
|
||||
}
|
||||
var blockTime uint64
|
||||
if block.BlockTime != nil {
|
||||
blockTime = uint64(*block.BlockTime)
|
||||
}
|
||||
|
||||
rawTxs := make([]RawTx, 0, len(block.Transactions))
|
||||
for i, tx := range block.Transactions {
|
||||
totalTx++
|
||||
rawTx, err := FromRpcTransactionWithMeta(tx, &blockTime, slot, int64(i))
|
||||
if err != nil {
|
||||
t.Fatalf("slot %d tx[%d] convert: %v", slot, i, err)
|
||||
}
|
||||
if rawTxBinaryIsVoteTx(rawTx) {
|
||||
filteredVote++
|
||||
continue
|
||||
}
|
||||
rawTxs = append(rawTxs, *rawTx)
|
||||
}
|
||||
blocks = append(blocks, rawTxs)
|
||||
}
|
||||
|
||||
encoded, err := EncodeRawTxBlocksBinary(blocks)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeRawTxBlocksBinary() error = %v", err)
|
||||
}
|
||||
outputPath := os.Getenv("RAWTX_BINARY_OUT")
|
||||
if outputPath == "" {
|
||||
outputPath = filepath.Join("testdata", "rawtx-binary", "rawtx-blocks-414696178-414696182.prbs")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
||||
t.Fatalf("create rawtx binary output dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(outputPath, encoded, 0o644); err != nil {
|
||||
t.Fatalf("write rawtx binary: %v", err)
|
||||
}
|
||||
|
||||
decoded, err := DecodeRawTxBlocksBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxBlocksBinary() error = %v", err)
|
||||
}
|
||||
if len(decoded) != len(blocks) {
|
||||
t.Fatalf("decoded block count = %d, want %d", len(decoded), len(blocks))
|
||||
}
|
||||
savedTx := 0
|
||||
for blockIndex := range decoded {
|
||||
if len(decoded[blockIndex]) != len(blocks[blockIndex]) {
|
||||
t.Fatalf("decoded block[%d] tx count = %d, want %d", blockIndex, len(decoded[blockIndex]), len(blocks[blockIndex]))
|
||||
}
|
||||
for txIndex := range decoded[blockIndex] {
|
||||
assertRawTxAccountAccess(t, decoded[blockIndex][txIndex])
|
||||
if rawTxBinaryIsVoteTx(decoded[blockIndex][txIndex]) {
|
||||
t.Fatalf("decoded block[%d].tx[%d] is vote tx", blockIndex, txIndex)
|
||||
}
|
||||
savedTx++
|
||||
}
|
||||
}
|
||||
if savedTx == 0 {
|
||||
t.Fatal("saved tx count is zero")
|
||||
}
|
||||
t.Logf("saved rawtx binary: path=%s bytes=%d total_tx=%d saved_tx=%d filtered_vote=%d", outputPath, len(encoded), totalTx, savedTx, filteredVote)
|
||||
}
|
||||
|
||||
func mustLoadRawTxFixture(t *testing.T, path string) *RawTx {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read fixture: %v", err)
|
||||
}
|
||||
var response RPCResponse
|
||||
if err := json.Unmarshal(raw, &response); err != nil {
|
||||
t.Fatalf("unmarshal fixture: %v", err)
|
||||
}
|
||||
return &response.Result
|
||||
}
|
||||
|
||||
func assertRawTxAccountAccess(t *testing.T, tx *RawTx) {
|
||||
t.Helper()
|
||||
accounts := tx.GetAccountList()
|
||||
if len(accounts) == 0 {
|
||||
t.Fatal("decoded account list is empty")
|
||||
}
|
||||
for _, instr := range tx.Transaction.Message.Instructions {
|
||||
if instr.ProgramIDIndex < 0 || instr.ProgramIDIndex >= len(accounts) {
|
||||
t.Fatalf("instruction program id index %d out of range %d", instr.ProgramIDIndex, len(accounts))
|
||||
}
|
||||
for _, accountIndex := range instr.Accounts {
|
||||
if accountIndex < 0 || accountIndex >= len(accounts) {
|
||||
t.Fatalf("instruction account index %d out of range %d", accountIndex, len(accounts))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, inner := range tx.Meta.InnerInstructions {
|
||||
for _, instr := range inner.Instructions {
|
||||
if instr.ProgramIDIndex < 0 || instr.ProgramIDIndex >= len(accounts) {
|
||||
t.Fatalf("inner instruction program id index %d out of range %d", instr.ProgramIDIndex, len(accounts))
|
||||
}
|
||||
for _, accountIndex := range instr.Accounts {
|
||||
if accountIndex < 0 || accountIndex >= len(accounts) {
|
||||
t.Fatalf("inner instruction account index %d out of range %d", accountIndex, len(accounts))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rawTxBinaryIsVoteTx(tx *RawTx) bool {
|
||||
if tx == nil {
|
||||
return false
|
||||
}
|
||||
accountList := tx.GetAccountList()
|
||||
for _, instr := range tx.Transaction.Message.Instructions {
|
||||
if instr.ProgramIDIndex >= 0 && instr.ProgramIDIndex < len(accountList) && accountList[instr.ProgramIDIndex] == solana.VoteProgramID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
394
raydiumclmm.go
Normal file
394
raydiumclmm.go
Normal 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
444
raydiumcpmm.go
Normal 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
498
raydiumlaunchlab.go
Normal 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
523
raydiumv4.go
Normal 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
144
raydiumv4_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func transferInstructionData(amount uint64) solana.Base58 {
|
||||
data := make([]byte, 9)
|
||||
data[0] = 3
|
||||
binary.LittleEndian.PutUint64(data[1:], amount)
|
||||
return solana.Base58(data)
|
||||
}
|
||||
|
||||
func raydiumV4SwapInstructionData(discriminator byte, amountSpecified, otherAmountThreshold uint64) solana.Base58 {
|
||||
data := make([]byte, 17)
|
||||
data[0] = discriminator
|
||||
binary.LittleEndian.PutUint64(data[1:9], amountSpecified)
|
||||
binary.LittleEndian.PutUint64(data[9:17], otherAmountThreshold)
|
||||
return solana.Base58(data)
|
||||
}
|
||||
|
||||
func TestRaydiumV4SwapV2ParserAllowsTrailingReadonlyAccounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
accountList := make([]solana.PublicKey, 32)
|
||||
for i := range accountList {
|
||||
accountList[i] = testPublicKey(byte(i + 1))
|
||||
}
|
||||
|
||||
accountList[0] = solana.TokenProgramID
|
||||
accountList[8] = raydiumV4Program
|
||||
accountList[20] = testPublicKey(200)
|
||||
accountList[21] = testPublicKey(201)
|
||||
accountList[22] = testPublicKey(202)
|
||||
|
||||
outerInstruction := Instruction{ProgramIDIndex: 20}
|
||||
swapInstruction := Instruction{
|
||||
Accounts: []int{0, 1, 2, 3, 4, 5, 6, 7, 8},
|
||||
ProgramIDIndex: 8,
|
||||
Data: raydiumV4SwapInstructionData(raydiumV4SwapBaseInV2Discriminator, 55, 42),
|
||||
}
|
||||
innerInstructions := InnerInstructions{
|
||||
Index: 0,
|
||||
Instructions: []Instruction{
|
||||
swapInstruction,
|
||||
{
|
||||
Accounts: []int{5, 4, 7},
|
||||
ProgramIDIndex: 0,
|
||||
Data: transferInstructionData(55),
|
||||
},
|
||||
{
|
||||
Accounts: []int{3, 6, 2},
|
||||
ProgramIDIndex: 0,
|
||||
Data: transferInstructionData(42),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rawTx := &RawTx{
|
||||
accountList: accountList,
|
||||
Meta: Meta{
|
||||
PostTokenBalances: []TokenBalance{
|
||||
{
|
||||
AccountIndex: 3,
|
||||
MintAccount: accountList[21],
|
||||
ProgramIDAccount: solana.TokenProgramID,
|
||||
UITokenAmount: UITokenAmount{
|
||||
Amount: "1000",
|
||||
Decimals: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
AccountIndex: 4,
|
||||
MintAccount: accountList[22],
|
||||
ProgramIDAccount: solana.TokenProgramID,
|
||||
UITokenAmount: UITokenAmount{
|
||||
Amount: "2000",
|
||||
Decimals: 9,
|
||||
},
|
||||
},
|
||||
{
|
||||
AccountIndex: 5,
|
||||
MintAccount: accountList[22],
|
||||
ProgramIDAccount: solana.TokenProgramID,
|
||||
UITokenAmount: UITokenAmount{
|
||||
Amount: "300",
|
||||
Decimals: 9,
|
||||
},
|
||||
},
|
||||
{
|
||||
AccountIndex: 6,
|
||||
MintAccount: accountList[21],
|
||||
ProgramIDAccount: solana.TokenProgramID,
|
||||
UITokenAmount: UITokenAmount{
|
||||
Amount: "400",
|
||||
Decimals: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Transaction: Transaction{
|
||||
Message: Message{
|
||||
Instructions: []Instruction{outerInstruction},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tx := &Tx{rawTx: rawTx}
|
||||
|
||||
swaps, nextOffset, err := raydiumv4SwapV2Parser(tx, swapInstruction, innerInstructions, [2]uint{0, 1})
|
||||
if err != nil {
|
||||
t.Fatalf("raydiumv4SwapV2Parser() error = %v", err)
|
||||
}
|
||||
if len(swaps) != 1 {
|
||||
t.Fatalf("raydiumv4SwapV2Parser() swaps len = %d, want 1", len(swaps))
|
||||
}
|
||||
if nextOffset != [2]uint{0, 4} {
|
||||
t.Fatalf("raydiumv4SwapV2Parser() nextOffset = %v, want [0 4]", nextOffset)
|
||||
}
|
||||
|
||||
swap := swaps[0]
|
||||
if swap.Event != "buy" {
|
||||
t.Fatalf("swap.Event = %q, want %q", swap.Event, "buy")
|
||||
}
|
||||
if !swap.Pool.Equals(accountList[1]) {
|
||||
t.Fatalf("swap.Pool = %s, want %s", swap.Pool, accountList[1])
|
||||
}
|
||||
if !swap.User.Equals(accountList[7]) {
|
||||
t.Fatalf("swap.User = %s, want %s", swap.User, accountList[7])
|
||||
}
|
||||
if !swap.EntryContract.Equals(accountList[20]) {
|
||||
t.Fatalf("swap.EntryContract = %s, want %s", swap.EntryContract, accountList[20])
|
||||
}
|
||||
if !swap.BaseAmount.Equal(decimal.NewFromInt(42)) {
|
||||
t.Fatalf("swap.BaseAmount = %s, want 42", swap.BaseAmount)
|
||||
}
|
||||
if !swap.QuoteAmount.Equal(decimal.NewFromInt(55)) {
|
||||
t.Fatalf("swap.QuoteAmount = %s, want 55", swap.QuoteAmount)
|
||||
}
|
||||
}
|
||||
230
swap_amounts.go
Normal file
230
swap_amounts.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
var maxSlippageBps = decimal.NewFromInt(10000)
|
||||
|
||||
func normalizeSlippageBps(value decimal.Decimal) decimal.Decimal {
|
||||
//if value.IsNegative() {
|
||||
// return decimal.Zero
|
||||
//}
|
||||
//if value.GreaterThan(maxSlippageBps) {
|
||||
// return maxSlippageBps
|
||||
//}
|
||||
return value
|
||||
}
|
||||
|
||||
type SwapMode uint8
|
||||
type SwapAmountSide uint8
|
||||
type SwapLimitType uint8
|
||||
|
||||
const (
|
||||
SwapModeUnknown SwapMode = iota
|
||||
SwapModeExactIn
|
||||
SwapModeExactOut
|
||||
)
|
||||
|
||||
const (
|
||||
SwapAmountSideUnknown SwapAmountSide = iota
|
||||
SwapAmountSideBase
|
||||
SwapAmountSideQuote
|
||||
)
|
||||
|
||||
const (
|
||||
SwapLimitTypeUnknown SwapLimitType = iota
|
||||
SwapLimitTypeMinOut
|
||||
SwapLimitTypeMaxIn
|
||||
)
|
||||
|
||||
func (m SwapMode) String() string {
|
||||
switch m {
|
||||
case SwapModeExactIn:
|
||||
return "exact_in"
|
||||
case SwapModeExactOut:
|
||||
return "exact_out"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (m SwapMode) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(m.String())
|
||||
}
|
||||
|
||||
func (s SwapAmountSide) String() string {
|
||||
switch s {
|
||||
case SwapAmountSideBase:
|
||||
return "base"
|
||||
case SwapAmountSideQuote:
|
||||
return "quote"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s SwapAmountSide) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
func (t SwapLimitType) String() string {
|
||||
switch t {
|
||||
case SwapLimitTypeMinOut:
|
||||
return "min_out"
|
||||
case SwapLimitTypeMaxIn:
|
||||
return "max_in"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (t SwapLimitType) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.String())
|
||||
}
|
||||
|
||||
func swapAmountForSide(baseAmount, quoteAmount decimal.Decimal, side SwapAmountSide) decimal.Decimal {
|
||||
switch side {
|
||||
case SwapAmountSideBase:
|
||||
return baseAmount
|
||||
case SwapAmountSideQuote:
|
||||
return quoteAmount
|
||||
default:
|
||||
return decimal.Zero
|
||||
}
|
||||
}
|
||||
|
||||
func swapMintForSide(baseMint, quoteMint solana.PublicKey, side SwapAmountSide) solana.PublicKey {
|
||||
switch side {
|
||||
case SwapAmountSideBase:
|
||||
return baseMint
|
||||
case SwapAmountSideQuote:
|
||||
return quoteMint
|
||||
default:
|
||||
return solana.PublicKey{}
|
||||
}
|
||||
}
|
||||
|
||||
func oppositeSwapAmountSide(side SwapAmountSide) SwapAmountSide {
|
||||
switch side {
|
||||
case SwapAmountSideBase:
|
||||
return SwapAmountSideQuote
|
||||
case SwapAmountSideQuote:
|
||||
return SwapAmountSideBase
|
||||
default:
|
||||
return SwapAmountSideUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func fixedSwapAmountSide(event string, swapMode SwapMode) SwapAmountSide {
|
||||
switch swapMode {
|
||||
case SwapModeExactIn:
|
||||
switch event {
|
||||
case TxEventBuy:
|
||||
return SwapAmountSideQuote
|
||||
case TxEventSell:
|
||||
return SwapAmountSideBase
|
||||
}
|
||||
case SwapModeExactOut:
|
||||
switch event {
|
||||
case TxEventBuy:
|
||||
return SwapAmountSideBase
|
||||
case TxEventSell:
|
||||
return SwapAmountSideQuote
|
||||
}
|
||||
}
|
||||
return SwapAmountSideUnknown
|
||||
}
|
||||
|
||||
func limitSwapAmountType(swapMode SwapMode) SwapLimitType {
|
||||
switch swapMode {
|
||||
case SwapModeExactIn:
|
||||
return SwapLimitTypeMinOut
|
||||
case SwapModeExactOut:
|
||||
return SwapLimitTypeMaxIn
|
||||
default:
|
||||
return SwapLimitTypeUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func calculateLimitSlippageBps(limitType SwapLimitType, limitAmount, actualAmount decimal.Decimal) decimal.Decimal {
|
||||
var value decimal.Decimal
|
||||
switch limitType {
|
||||
case SwapLimitTypeMinOut:
|
||||
if !actualAmount.IsPositive() {
|
||||
if !limitAmount.IsPositive() {
|
||||
value = maxSlippageBps
|
||||
break
|
||||
}
|
||||
value = maxSlippageBps.Neg()
|
||||
break
|
||||
}
|
||||
if !limitAmount.IsPositive() {
|
||||
value = maxSlippageBps
|
||||
break
|
||||
}
|
||||
value = actualAmount.Sub(limitAmount).Mul(maxSlippageBps).Div(actualAmount)
|
||||
case SwapLimitTypeMaxIn:
|
||||
if !limitAmount.IsPositive() {
|
||||
if !actualAmount.IsPositive() {
|
||||
value = maxSlippageBps
|
||||
break
|
||||
}
|
||||
value = maxSlippageBps.Neg()
|
||||
break
|
||||
}
|
||||
value = limitAmount.Sub(actualAmount).Mul(maxSlippageBps).Div(limitAmount)
|
||||
default:
|
||||
value = decimal.Zero
|
||||
}
|
||||
return normalizeSlippageBps(value)
|
||||
}
|
||||
|
||||
func (s *Swap) SetSwapAmountInfoDetailed(
|
||||
swapMode SwapMode,
|
||||
fixedAmount decimal.Decimal,
|
||||
fixedSide SwapAmountSide,
|
||||
fixedMint solana.PublicKey,
|
||||
limitType SwapLimitType,
|
||||
limitAmount decimal.Decimal,
|
||||
limitSide SwapAmountSide,
|
||||
limitMint solana.PublicKey,
|
||||
actualLimitAmount decimal.Decimal,
|
||||
) {
|
||||
s.SwapMode = swapMode
|
||||
s.FixedAmount = fixedAmount
|
||||
s.FixedAmountSide = fixedSide
|
||||
s.FixedMint = fixedMint
|
||||
s.LimitAmountType = limitType
|
||||
s.LimitAmount = limitAmount
|
||||
s.LimitAmountSide = limitSide
|
||||
s.LimitMint = limitMint
|
||||
s.ActualLimitAmount = actualLimitAmount
|
||||
s.ActualLimitAmountSide = limitSide
|
||||
s.SlippageBps = calculateLimitSlippageBps(limitType, limitAmount, actualLimitAmount)
|
||||
}
|
||||
|
||||
func (s *Swap) SetSwapAmountInfo(swapMode SwapMode, fixedAmount, limitAmount decimal.Decimal) {
|
||||
fixedSide := fixedSwapAmountSide(s.Event, swapMode)
|
||||
if fixedSide == SwapAmountSideUnknown {
|
||||
return
|
||||
}
|
||||
|
||||
limitType := limitSwapAmountType(swapMode)
|
||||
limitSide := oppositeSwapAmountSide(fixedSide)
|
||||
actualLimitAmount := swapAmountForSide(s.BaseAmount, s.QuoteAmount, limitSide)
|
||||
s.SetSwapAmountInfoDetailed(
|
||||
swapMode,
|
||||
fixedAmount,
|
||||
fixedSide,
|
||||
swapMintForSide(s.BaseMint, s.QuoteMint, fixedSide),
|
||||
limitType,
|
||||
limitAmount,
|
||||
limitSide,
|
||||
swapMintForSide(s.BaseMint, s.QuoteMint, limitSide),
|
||||
actualLimitAmount,
|
||||
)
|
||||
}
|
||||
387
swap_amounts_oracle_test.go
Normal file
387
swap_amounts_oracle_test.go
Normal file
@@ -0,0 +1,387 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
"github.com/gagliardetto/solana-go/rpc"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type swapOracleCase struct {
|
||||
name string
|
||||
txHash string
|
||||
index int
|
||||
|
||||
program string
|
||||
event string
|
||||
|
||||
swapMode SwapMode
|
||||
fixedAmount string
|
||||
fixedAmountSide SwapAmountSide
|
||||
fixedMint string
|
||||
limitAmountType SwapLimitType
|
||||
limitAmount string
|
||||
limitAmountSide SwapAmountSide
|
||||
limitMint string
|
||||
actualLimitAmount string
|
||||
actualLimitAmountSide SwapAmountSide
|
||||
slippageBps string
|
||||
}
|
||||
|
||||
func TestSwapAmountOracleSamples(t *testing.T) {
|
||||
EnableAllParsers()
|
||||
|
||||
cases := []swapOracleCase{
|
||||
{
|
||||
name: "pump buy exact out",
|
||||
txHash: "5ybEYcXYhFNfCNAu1o7ovM1Rw5285PBzAwsj4ezwmPRLkYtXX91GhcvAgTVZvdCVV6upsGH8DwYeseNswPhEfVbg",
|
||||
index: 0,
|
||||
program: "Pump",
|
||||
event: TxEventBuy,
|
||||
swapMode: SwapModeExactOut,
|
||||
fixedAmount: "1459556161603",
|
||||
fixedAmountSide: SwapAmountSideBase,
|
||||
fixedMint: "CEwaxx5j1K61JMYXavcxihVQW4NxC6c4NQ27veFpYUYA",
|
||||
limitAmountType: SwapLimitTypeMaxIn,
|
||||
limitAmount: "100000001",
|
||||
limitAmountSide: SwapAmountSideQuote,
|
||||
limitMint: "So11111111111111111111111111111111111111112",
|
||||
actualLimitAmount: "98765431",
|
||||
actualLimitAmountSide: SwapAmountSideQuote,
|
||||
slippageBps: "123.4569987654300123",
|
||||
},
|
||||
{
|
||||
name: "raydium v4 exact out",
|
||||
txHash: "3Q1BhUvqm889oJfCn96AZAJqyHi1uxdeoKQknCc9xQcajhZS5APfwzkHuNTkPJEhyGknj7VyLpkNoMmNPK3No6hC",
|
||||
index: 0,
|
||||
program: "RaydiumV4",
|
||||
event: TxEventSell,
|
||||
swapMode: SwapModeExactOut,
|
||||
fixedAmount: "432588",
|
||||
fixedAmountSide: SwapAmountSideQuote,
|
||||
fixedMint: "2qEHjDLDLbuBgRYvsxhc5D6uDWAivNFZGan56P1tpump",
|
||||
limitAmountType: SwapLimitTypeMaxIn,
|
||||
limitAmount: "18446744073709551615",
|
||||
limitAmountSide: SwapAmountSideBase,
|
||||
limitMint: "So11111111111111111111111111111111111111112",
|
||||
actualLimitAmount: "279099",
|
||||
actualLimitAmountSide: SwapAmountSideBase,
|
||||
slippageBps: "9999.9999999998487001",
|
||||
},
|
||||
{
|
||||
name: "pump amm exact in",
|
||||
txHash: "3Q1BhUvqm889oJfCn96AZAJqyHi1uxdeoKQknCc9xQcajhZS5APfwzkHuNTkPJEhyGknj7VyLpkNoMmNPK3No6hC",
|
||||
index: 1,
|
||||
program: "PumpAMM",
|
||||
event: TxEventSell,
|
||||
swapMode: SwapModeExactIn,
|
||||
fixedAmount: "432588",
|
||||
fixedAmountSide: SwapAmountSideBase,
|
||||
fixedMint: "2qEHjDLDLbuBgRYvsxhc5D6uDWAivNFZGan56P1tpump",
|
||||
limitAmountType: SwapLimitTypeMinOut,
|
||||
limitAmount: "0",
|
||||
limitAmountSide: SwapAmountSideQuote,
|
||||
limitMint: "So11111111111111111111111111111111111111112",
|
||||
actualLimitAmount: "284317",
|
||||
actualLimitAmountSide: SwapAmountSideQuote,
|
||||
slippageBps: "10000",
|
||||
},
|
||||
{
|
||||
name: "meteora dlmm exact in",
|
||||
txHash: "5uw9Uwe9KDLCzUNf1sRr7EjKqE2Xs8qUHaoC1CRFhkE3TMpo5TimwApW65pd6pmB7Cp92XXMaZ9jQav6aXRZGtoS",
|
||||
index: 0,
|
||||
program: "MeteoraDLMM",
|
||||
event: TxEventBuy,
|
||||
swapMode: SwapModeExactIn,
|
||||
fixedAmount: "17684137",
|
||||
fixedAmountSide: SwapAmountSideQuote,
|
||||
fixedMint: "So11111111111111111111111111111111111111112",
|
||||
limitAmountType: SwapLimitTypeMinOut,
|
||||
limitAmount: "0",
|
||||
limitAmountSide: SwapAmountSideBase,
|
||||
limitMint: "METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m",
|
||||
actualLimitAmount: "50437818",
|
||||
actualLimitAmountSide: SwapAmountSideBase,
|
||||
slippageBps: "10000",
|
||||
},
|
||||
{
|
||||
name: "orca whirlpool exact in",
|
||||
txHash: "5uw9Uwe9KDLCzUNf1sRr7EjKqE2Xs8qUHaoC1CRFhkE3TMpo5TimwApW65pd6pmB7Cp92XXMaZ9jQav6aXRZGtoS",
|
||||
index: 1,
|
||||
program: "OrcaWhirPool",
|
||||
event: TxEventSell,
|
||||
swapMode: SwapModeExactIn,
|
||||
fixedAmount: "50437818",
|
||||
fixedAmountSide: SwapAmountSideBase,
|
||||
fixedMint: "METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m",
|
||||
limitAmountType: SwapLimitTypeMinOut,
|
||||
limitAmount: "0",
|
||||
limitAmountSide: SwapAmountSideQuote,
|
||||
limitMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
||||
actualLimitAmount: "1438802",
|
||||
actualLimitAmountSide: SwapAmountSideQuote,
|
||||
slippageBps: "10000",
|
||||
},
|
||||
{
|
||||
name: "raydium v4 exact in",
|
||||
txHash: "5uw9Uwe9KDLCzUNf1sRr7EjKqE2Xs8qUHaoC1CRFhkE3TMpo5TimwApW65pd6pmB7Cp92XXMaZ9jQav6aXRZGtoS",
|
||||
index: 2,
|
||||
program: "RaydiumV4",
|
||||
event: TxEventBuy,
|
||||
swapMode: SwapModeExactIn,
|
||||
fixedAmount: "1438802",
|
||||
fixedAmountSide: SwapAmountSideQuote,
|
||||
fixedMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
||||
limitAmountType: SwapLimitTypeMinOut,
|
||||
limitAmount: "0",
|
||||
limitAmountSide: SwapAmountSideBase,
|
||||
limitMint: "So11111111111111111111111111111111111111112",
|
||||
actualLimitAmount: "19059759",
|
||||
actualLimitAmountSide: SwapAmountSideBase,
|
||||
slippageBps: "10000",
|
||||
},
|
||||
{
|
||||
name: "raydium clmm exact in",
|
||||
txHash: "3XoRKna49qCAuF75ctmaYupNmYWuFm5AU73ULQjxNUxz9qJzuKqMRqq5Z88L6DooWTF44UxnxMXwqLn5t9NsoCoZ",
|
||||
index: 2,
|
||||
program: "RaydiumCLMM",
|
||||
event: TxEventSell,
|
||||
swapMode: SwapModeExactIn,
|
||||
fixedAmount: "1569519567845",
|
||||
fixedAmountSide: SwapAmountSideBase,
|
||||
fixedMint: "CiKu4eHsVrc1eueVQeHn7qhXTcVu95gSQmBpX4utjL9z",
|
||||
limitAmountType: SwapLimitTypeMinOut,
|
||||
limitAmount: "0",
|
||||
limitAmountSide: SwapAmountSideQuote,
|
||||
limitMint: "6sQgvhAYtYFrahcjB1hKfB3ZC5YDVdfYvAqK1GKe93c9",
|
||||
actualLimitAmount: "366578",
|
||||
actualLimitAmountSide: SwapAmountSideQuote,
|
||||
slippageBps: "10000",
|
||||
},
|
||||
{
|
||||
name: "raydium cpmm exact in",
|
||||
txHash: "288FAsrj7h6hTKywtVaqCqHAbNZ6x3Xuich9kQMGVarVUnUjkqTabxQE9JHyranGY9eqUivZbBTzC5dH1BEuJ6pa",
|
||||
index: 2,
|
||||
program: "RaydiumCPMM",
|
||||
event: TxEventSell,
|
||||
swapMode: SwapModeExactIn,
|
||||
fixedAmount: "1260040377905",
|
||||
fixedAmountSide: SwapAmountSideBase,
|
||||
fixedMint: "3f7wfg9yHLtGKvy75MmqsVT1ueTFoqyySQbusrX1YAQ4",
|
||||
limitAmountType: SwapLimitTypeMinOut,
|
||||
limitAmount: "0",
|
||||
limitAmountSide: SwapAmountSideQuote,
|
||||
limitMint: "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp",
|
||||
actualLimitAmount: "802507591",
|
||||
actualLimitAmountSide: SwapAmountSideQuote,
|
||||
slippageBps: "10000",
|
||||
},
|
||||
{
|
||||
name: "raydium launchlab exact in",
|
||||
txHash: "1r3gfEse3WAy5H6h4jMSNq1K5KZNCrMdAtCnpBSE1xkHQEt3EJ2J6Lk6ihQshrfsrS5FbqP5WuUSZG6zPCJB5TE",
|
||||
index: 0,
|
||||
program: "RaydiumLaunchLab",
|
||||
event: TxEventBuy,
|
||||
swapMode: SwapModeExactIn,
|
||||
fixedAmount: "10000000",
|
||||
fixedAmountSide: SwapAmountSideQuote,
|
||||
fixedMint: "So11111111111111111111111111111111111111112",
|
||||
limitAmountType: SwapLimitTypeMinOut,
|
||||
limitAmount: "5976144139694",
|
||||
limitAmountSide: SwapAmountSideBase,
|
||||
limitMint: "Attr2sqaXr76XqaDxdtnQ4QAEsaFdgTGr599F7ytgray",
|
||||
actualLimitAmount: "6129378604814",
|
||||
actualLimitAmountSide: SwapAmountSideBase,
|
||||
slippageBps: "249.9999999994289796",
|
||||
},
|
||||
{
|
||||
name: "meteora pools exact in",
|
||||
txHash: "5jQk6mbhtExpUFskRy2AfKWbLgXDv2USiGkq9tQWauGVKduGdTqscgxyDCPgBryr4kz5hDT5CE9TpVTKDoPhkBmt",
|
||||
index: 0,
|
||||
program: "MeteoraPools",
|
||||
event: TxEventBuy,
|
||||
swapMode: SwapModeExactIn,
|
||||
fixedAmount: "75404052467",
|
||||
fixedAmountSide: SwapAmountSideQuote,
|
||||
fixedMint: "STrikemJEk2tFVYpg7SMo9nGPrnJ56fHnS1K7PV2fPw",
|
||||
limitAmountType: SwapLimitTypeMinOut,
|
||||
limitAmount: "30605141",
|
||||
limitAmountSide: SwapAmountSideBase,
|
||||
limitMint: "So11111111111111111111111111111111111111112",
|
||||
actualLimitAmount: "31556751",
|
||||
actualLimitAmountSide: SwapAmountSideBase,
|
||||
slippageBps: "301.5551252408715967",
|
||||
},
|
||||
{
|
||||
name: "meteora bonding curve exact in",
|
||||
txHash: "5Qsq1ueenSs4KgVRgwXmBVFvMR3Asq9MmXwmqQimxDdWLdiJy6dVfmYqa2YCvkNH1Gx7aCzJqg4t9gN9ECfxH2JS",
|
||||
index: 0,
|
||||
program: "MeteoraBondingCurve",
|
||||
event: TxEventSell,
|
||||
swapMode: SwapModeExactIn,
|
||||
fixedAmount: "11022737683",
|
||||
fixedAmountSide: SwapAmountSideBase,
|
||||
fixedMint: "8FosqFryatEMV4ZeFR1gLmSmxBLcQ2NCibpZxFRPPF34",
|
||||
limitAmountType: SwapLimitTypeMinOut,
|
||||
limitAmount: "49672101",
|
||||
limitAmountSide: SwapAmountSideQuote,
|
||||
limitMint: "So11111111111111111111111111111111111111112",
|
||||
actualLimitAmount: "49672101",
|
||||
actualLimitAmountSide: SwapAmountSideQuote,
|
||||
slippageBps: "0",
|
||||
},
|
||||
{
|
||||
name: "meteora damm v2 exact in",
|
||||
txHash: "43EouSYkeVmLBZSKW1KptiQcAVvB6KX49wjztjgzkQ9iU38A6fF68k77bNy9Wn6fwjykqYsPorUKj8m6SFY7naf1",
|
||||
index: 0,
|
||||
program: "MeteoraAmmV2",
|
||||
event: TxEventSell,
|
||||
swapMode: SwapModeExactIn,
|
||||
fixedAmount: "11846",
|
||||
fixedAmountSide: SwapAmountSideBase,
|
||||
fixedMint: "So11111111111111111111111111111111111111112",
|
||||
limitAmountType: SwapLimitTypeMinOut,
|
||||
limitAmount: "30893426",
|
||||
limitAmountSide: SwapAmountSideQuote,
|
||||
limitMint: "CdDoeyd67nuzmMCF8Dd3RzbxiTRk41Xd922Veu9kGvDE",
|
||||
actualLimitAmount: "33325162",
|
||||
actualLimitAmountSide: SwapAmountSideQuote,
|
||||
slippageBps: "729.6996785792069068",
|
||||
},
|
||||
{
|
||||
name: "meteora damm v2 exact out",
|
||||
txHash: "BD7GZaXaJc2hzSNPe6Q5yeej7rZLQFMpdx4rZwPhGTyHP43iMAR7LxymRSPGXnefAxSqi5sMsEPS1cjyQjup3Eu",
|
||||
index: 0,
|
||||
program: "MeteoraAmmV2",
|
||||
event: TxEventBuy,
|
||||
swapMode: SwapModeExactOut,
|
||||
fixedAmount: "512761043",
|
||||
fixedAmountSide: SwapAmountSideBase,
|
||||
fixedMint: "DPfZc59DLrKyVTJDoKB8CBFgCndsjUzxy6fdbxk4Zms9",
|
||||
limitAmountType: SwapLimitTypeMaxIn,
|
||||
limitAmount: "71386496",
|
||||
limitAmountSide: SwapAmountSideQuote,
|
||||
limitMint: "So11111111111111111111111111111111111111112",
|
||||
actualLimitAmount: "70020377",
|
||||
actualLimitAmountSide: SwapAmountSideQuote,
|
||||
slippageBps: "191.3693872857970225",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tx := mustParseRPCFixtureTx(t, tc.txHash)
|
||||
if tc.index >= len(tx.Swaps) {
|
||||
t.Fatalf("swap index %d out of range, len=%d", tc.index, len(tx.Swaps))
|
||||
}
|
||||
|
||||
swap := tx.Swaps[tc.index]
|
||||
if swap.Program != tc.program {
|
||||
t.Fatalf("program = %q, want %q", swap.Program, tc.program)
|
||||
}
|
||||
if swap.Event != tc.event {
|
||||
t.Fatalf("event = %q, want %q", swap.Event, tc.event)
|
||||
}
|
||||
if swap.SwapMode != tc.swapMode {
|
||||
t.Fatalf("swap mode = %s, want %s", swap.SwapMode.String(), tc.swapMode.String())
|
||||
}
|
||||
|
||||
assertDecimalString(t, "fixed_amount", swap.FixedAmount, tc.fixedAmount)
|
||||
if swap.FixedAmountSide != tc.fixedAmountSide {
|
||||
t.Fatalf("fixed amount side = %s, want %s", swap.FixedAmountSide.String(), tc.fixedAmountSide.String())
|
||||
}
|
||||
assertPublicKey(t, "fixed_mint", swap.FixedMint, tc.fixedMint)
|
||||
|
||||
if swap.LimitAmountType != tc.limitAmountType {
|
||||
t.Fatalf("limit amount type = %s, want %s", swap.LimitAmountType.String(), tc.limitAmountType.String())
|
||||
}
|
||||
assertDecimalString(t, "limit_amount", swap.LimitAmount, tc.limitAmount)
|
||||
if swap.LimitAmountSide != tc.limitAmountSide {
|
||||
t.Fatalf("limit amount side = %s, want %s", swap.LimitAmountSide.String(), tc.limitAmountSide.String())
|
||||
}
|
||||
assertPublicKey(t, "limit_mint", swap.LimitMint, tc.limitMint)
|
||||
|
||||
assertDecimalString(t, "actual_limit_amount", swap.ActualLimitAmount, tc.actualLimitAmount)
|
||||
if swap.ActualLimitAmountSide != tc.actualLimitAmountSide {
|
||||
t.Fatalf("actual limit amount side = %s, want %s", swap.ActualLimitAmountSide.String(), tc.actualLimitAmountSide.String())
|
||||
}
|
||||
assertDecimalString(t, "slippage_bps", swap.SlippageBps, tc.slippageBps)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustParseRPCFixtureTx(t *testing.T, txHash string) *Tx {
|
||||
t.Helper()
|
||||
|
||||
fixturePath := filepath.Join("testdata", "rpc", txHash+".json")
|
||||
raw, err := os.ReadFile(fixturePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read fixture %s: %v", fixturePath, err)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Result *rpc.GetTransactionResult `json:"result"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &response); err != nil {
|
||||
t.Fatalf("unmarshal fixture %s: %v", fixturePath, err)
|
||||
}
|
||||
if response.Result == nil || response.Result.Transaction == nil || response.Result.Meta == nil {
|
||||
t.Fatalf("fixture %s is missing transaction data", fixturePath)
|
||||
}
|
||||
|
||||
rawBinary := response.Result.Transaction.GetBinary()
|
||||
if len(rawBinary) == 0 {
|
||||
t.Fatalf("fixture %s has empty transaction bytes", fixturePath)
|
||||
}
|
||||
|
||||
txWithMeta := rpc.TransactionWithMeta{
|
||||
Slot: response.Result.Slot,
|
||||
BlockTime: response.Result.BlockTime,
|
||||
Transaction: rpc.DataBytesOrJSONFromBytes(rawBinary),
|
||||
Meta: response.Result.Meta,
|
||||
Version: response.Result.Version,
|
||||
}
|
||||
|
||||
var blockTime *uint64
|
||||
if response.Result.BlockTime != nil {
|
||||
bt := uint64(*response.Result.BlockTime)
|
||||
blockTime = &bt
|
||||
}
|
||||
|
||||
rawTx, err := FromRpcTransactionWithMeta(txWithMeta, blockTime, response.Result.Slot, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("convert fixture %s: %v", fixturePath, err)
|
||||
}
|
||||
|
||||
tx, err := ParseRawTx(rawTx)
|
||||
if err != nil {
|
||||
t.Fatalf("parse fixture %s: %v", fixturePath, err)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
func assertDecimalString(t *testing.T, field string, got decimal.Decimal, want string) {
|
||||
t.Helper()
|
||||
|
||||
wantDecimal, err := decimal.NewFromString(want)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid expected decimal for %s: %v", field, err)
|
||||
}
|
||||
if !got.Equal(wantDecimal) {
|
||||
t.Fatalf("%s = %s, want %s", field, got.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func assertPublicKey(t *testing.T, field string, got solana.PublicKey, want string) {
|
||||
t.Helper()
|
||||
|
||||
wantKey := solana.MustPublicKeyFromBase58(want)
|
||||
if !got.Equals(wantKey) {
|
||||
t.Fatalf("%s = %s, want %s", field, got, wantKey)
|
||||
}
|
||||
}
|
||||
206
swap_amounts_test.go
Normal file
206
swap_amounts_test.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func TestSetSwapAmountInfoExactInBuy(t *testing.T) {
|
||||
swap := Swap{
|
||||
Event: TxEventBuy,
|
||||
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
|
||||
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
|
||||
BaseAmount: decimal.NewFromInt(120),
|
||||
QuoteAmount: decimal.NewFromInt(100),
|
||||
}
|
||||
|
||||
swap.SetSwapAmountInfo(SwapModeExactIn, decimal.NewFromInt(100), decimal.NewFromInt(110))
|
||||
|
||||
if swap.FixedAmountSide != SwapAmountSideQuote {
|
||||
t.Fatalf("fixed side = %s, want quote", swap.FixedAmountSide.String())
|
||||
}
|
||||
if swap.LimitAmountType != SwapLimitTypeMinOut {
|
||||
t.Fatalf("limit type = %s, want min_out", swap.LimitAmountType.String())
|
||||
}
|
||||
if swap.LimitAmountSide != SwapAmountSideBase {
|
||||
t.Fatalf("limit side = %s, want base", swap.LimitAmountSide.String())
|
||||
}
|
||||
if !swap.ActualLimitAmount.Equal(decimal.NewFromInt(120)) {
|
||||
t.Fatalf("actual limit amount = %s, want 120", swap.ActualLimitAmount)
|
||||
}
|
||||
if got := swap.SlippageBps.StringFixed(4); got != "833.3333" {
|
||||
t.Fatalf("slippage bps = %s, want 833.3333", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSwapAmountInfoExactOutSell(t *testing.T) {
|
||||
swap := Swap{
|
||||
Event: TxEventSell,
|
||||
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
|
||||
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
|
||||
BaseAmount: decimal.NewFromInt(95),
|
||||
QuoteAmount: decimal.NewFromInt(100),
|
||||
}
|
||||
|
||||
swap.SetSwapAmountInfo(SwapModeExactOut, decimal.NewFromInt(100), decimal.NewFromInt(105))
|
||||
|
||||
if swap.FixedAmountSide != SwapAmountSideQuote {
|
||||
t.Fatalf("fixed side = %s, want quote", swap.FixedAmountSide.String())
|
||||
}
|
||||
if swap.LimitAmountType != SwapLimitTypeMaxIn {
|
||||
t.Fatalf("limit type = %s, want max_in", swap.LimitAmountType.String())
|
||||
}
|
||||
if swap.LimitAmountSide != SwapAmountSideBase {
|
||||
t.Fatalf("limit side = %s, want base", swap.LimitAmountSide.String())
|
||||
}
|
||||
if !swap.ActualLimitAmount.Equal(decimal.NewFromInt(95)) {
|
||||
t.Fatalf("actual limit amount = %s, want 95", swap.ActualLimitAmount)
|
||||
}
|
||||
if got := swap.SlippageBps.StringFixed(4); got != "952.3810" {
|
||||
t.Fatalf("slippage bps = %s, want 952.3810", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSwapAmountInfoExactInZeroLimitUsesMaxSlippage(t *testing.T) {
|
||||
swap := Swap{
|
||||
Event: TxEventSell,
|
||||
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
|
||||
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
|
||||
BaseAmount: decimal.NewFromInt(50),
|
||||
QuoteAmount: decimal.NewFromInt(25),
|
||||
}
|
||||
|
||||
swap.SetSwapAmountInfo(SwapModeExactIn, decimal.NewFromInt(50), decimal.Zero)
|
||||
|
||||
if got := swap.SlippageBps.String(); got != "10000" {
|
||||
t.Fatalf("slippage bps = %s, want 10000", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSwapAmountInfoExactInNegativeHeadroomClampsToZero(t *testing.T) {
|
||||
swap := Swap{
|
||||
Event: TxEventBuy,
|
||||
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
|
||||
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
|
||||
BaseAmount: decimal.NewFromInt(90),
|
||||
QuoteAmount: decimal.NewFromInt(100),
|
||||
}
|
||||
|
||||
swap.SetSwapAmountInfo(SwapModeExactIn, decimal.NewFromInt(100), decimal.NewFromInt(110))
|
||||
|
||||
if got := swap.SlippageBps.String(); got != "0" {
|
||||
t.Fatalf("slippage bps = %s, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSwapAmountInfoExactOutNegativeHeadroomClampsToZero(t *testing.T) {
|
||||
swap := Swap{
|
||||
Event: TxEventSell,
|
||||
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
|
||||
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
|
||||
BaseAmount: decimal.NewFromInt(120),
|
||||
QuoteAmount: decimal.NewFromInt(100),
|
||||
}
|
||||
|
||||
swap.SetSwapAmountInfo(SwapModeExactOut, decimal.NewFromInt(100), decimal.NewFromInt(105))
|
||||
|
||||
if got := swap.SlippageBps.String(); got != "0" {
|
||||
t.Fatalf("slippage bps = %s, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeteoraDammSwapAmountInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
event string
|
||||
params *struct {
|
||||
Amount0 uint64
|
||||
Amount1 uint64
|
||||
SwapMode uint8
|
||||
}
|
||||
wantMode SwapMode
|
||||
wantFixed int64
|
||||
wantLimit int64
|
||||
}{
|
||||
{
|
||||
name: "sell exact in uses amount0 as input and amount1 as min out",
|
||||
event: TxEventSell,
|
||||
params: &struct {
|
||||
Amount0 uint64
|
||||
Amount1 uint64
|
||||
SwapMode uint8
|
||||
}{Amount0: 100, Amount1: 95, SwapMode: 0},
|
||||
wantMode: SwapModeExactIn,
|
||||
wantFixed: 100,
|
||||
wantLimit: 95,
|
||||
},
|
||||
{
|
||||
name: "sell partial fill follows exact in semantics",
|
||||
event: TxEventSell,
|
||||
params: &struct {
|
||||
Amount0 uint64
|
||||
Amount1 uint64
|
||||
SwapMode uint8
|
||||
}{Amount0: 101, Amount1: 96, SwapMode: 1},
|
||||
wantMode: SwapModeExactIn,
|
||||
wantFixed: 101,
|
||||
wantLimit: 96,
|
||||
},
|
||||
{
|
||||
name: "buy exact in keeps amount0 as input and amount1 as min out",
|
||||
event: TxEventBuy,
|
||||
params: &struct {
|
||||
Amount0 uint64
|
||||
Amount1 uint64
|
||||
SwapMode uint8
|
||||
}{Amount0: 130, Amount1: 120, SwapMode: 0},
|
||||
wantMode: SwapModeExactIn,
|
||||
wantFixed: 130,
|
||||
wantLimit: 120,
|
||||
},
|
||||
{
|
||||
name: "buy exact out uses amount0 as target output and amount1 as max input",
|
||||
event: TxEventBuy,
|
||||
params: &struct {
|
||||
Amount0 uint64
|
||||
Amount1 uint64
|
||||
SwapMode uint8
|
||||
}{Amount0: 120, Amount1: 130, SwapMode: 2},
|
||||
wantMode: SwapModeExactOut,
|
||||
wantFixed: 120,
|
||||
wantLimit: 130,
|
||||
},
|
||||
{
|
||||
name: "sell exact out keeps amount0 as target output and amount1 as max input",
|
||||
event: TxEventSell,
|
||||
params: &struct {
|
||||
Amount0 uint64
|
||||
Amount1 uint64
|
||||
SwapMode uint8
|
||||
}{Amount0: 140, Amount1: 150, SwapMode: 2},
|
||||
wantMode: SwapModeExactOut,
|
||||
wantFixed: 140,
|
||||
wantLimit: 150,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotMode, gotFixed, gotLimit, ok := meteoraDammSwapAmountInfo(tt.event, tt.params)
|
||||
if !ok {
|
||||
t.Fatal("ok = false, want true")
|
||||
}
|
||||
if gotMode != tt.wantMode {
|
||||
t.Fatalf("mode = %s, want %s", gotMode.String(), tt.wantMode.String())
|
||||
}
|
||||
if !gotFixed.Equal(decimal.NewFromInt(tt.wantFixed)) {
|
||||
t.Fatalf("fixed = %s, want %d", gotFixed, tt.wantFixed)
|
||||
}
|
||||
if !gotLimit.Equal(decimal.NewFromInt(tt.wantLimit)) {
|
||||
t.Fatalf("limit = %s, want %d", gotLimit, tt.wantLimit)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
11
system.go
11
system.go
@@ -31,9 +31,18 @@ func TransferParser(result *RawTx, instruction Instruction, offset [2]uint, tx *
|
||||
}
|
||||
var lamports uint64 = binary.LittleEndian.Uint64(decodeData)
|
||||
|
||||
//from := result.accountList[result.Transaction.Message.Instructions[offset[0]].Accounts[0]]
|
||||
from := result.accountList[result.Transaction.Message.Instructions[offset[0]].Accounts[0]]
|
||||
to := result.accountList[instruction.Accounts[1]]
|
||||
|
||||
if result.Meta.Err == nil {
|
||||
if offset[1] == 0 {
|
||||
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
File diff suppressed because one or more lines are too long
141
tx.go
141
tx.go
@@ -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 {
|
||||
@@ -45,15 +86,30 @@ type mevInfo struct {
|
||||
MevAgentFee decimal.Decimal
|
||||
}
|
||||
|
||||
type SolTransfer struct {
|
||||
From solana.PublicKey
|
||||
To solana.PublicKey
|
||||
Amount decimal.Decimal
|
||||
}
|
||||
type ChainLink struct {
|
||||
Timestamp int64
|
||||
Price decimal.Decimal
|
||||
}
|
||||
|
||||
type Tx struct {
|
||||
rawTx *RawTx
|
||||
Signer solana.PublicKey
|
||||
Err interface{} `json:"err,omitempty"`
|
||||
Swaps []Swap `json:"swaps,omitempty"`
|
||||
Block uint64 `json:"block"`
|
||||
BlockIndex uint64 `json:"index"`
|
||||
TxHash *[64]byte `json:"-"`
|
||||
BlockAt int64 `json:"block_at"`
|
||||
rawTx *RawTx
|
||||
Vote bool
|
||||
Signer solana.PublicKey
|
||||
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"`
|
||||
|
||||
CuFee decimal.Decimal `json:"cu_fee"`
|
||||
|
||||
cachedTxHash string
|
||||
|
||||
@@ -67,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
|
||||
}
|
||||
@@ -103,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 {
|
||||
@@ -115,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
|
||||
}
|
||||
case PlatformGMGN:
|
||||
if !checkBonkGmgnBuy(rawTx) {
|
||||
platform = PlatformFake
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quoteAmount := swap.QuoteAmount
|
||||
if swap.BaseMint.Equals(solana.WrappedSol) {
|
||||
quoteAmount = swap.BaseAmount
|
||||
}
|
||||
|
||||
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
|
||||
} 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
|
||||
}
|
||||
|
||||
platform != PlatformFake &&
|
||||
platformFee.LessThan(quoteAmount.Mul(decimal.NewFromInt(9)).Div(decimal.New(10000, 9))) {
|
||||
platform = PlatformFake
|
||||
}
|
||||
if platform == "" {
|
||||
platform = PlatformNone
|
||||
@@ -174,3 +218,34 @@ 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 {
|
||||
return name
|
||||
}
|
||||
return s.EntryContract.String()
|
||||
}
|
||||
|
||||
2229
tx_binary.go
Normal file
2229
tx_binary.go
Normal file
File diff suppressed because it is too large
Load Diff
146
tx_binary_realdata_test.go
Normal file
146
tx_binary_realdata_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gagliardetto/solana-go/rpc"
|
||||
)
|
||||
|
||||
func TestTxBinaryRealFixtureSizes(t *testing.T) {
|
||||
fixtures, err := filepath.Glob(filepath.Join("testdata", "rpc", "*.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("glob fixtures: %v", err)
|
||||
}
|
||||
if len(fixtures) == 0 {
|
||||
t.Fatal("no rpc fixtures found")
|
||||
}
|
||||
sort.Strings(fixtures)
|
||||
|
||||
type sizeResult struct {
|
||||
name string
|
||||
swaps int
|
||||
platforms int
|
||||
mevAgents int
|
||||
addresses int
|
||||
encodedBytes int
|
||||
fixtureBytes int
|
||||
txBinaryBytes int
|
||||
}
|
||||
|
||||
results := make([]sizeResult, 0, len(fixtures))
|
||||
totalEncoded := 0
|
||||
|
||||
for _, fixture := range fixtures {
|
||||
tx, rawTxBytesLen, fixtureBytesLen := mustParseRPCFixtureTxForBinarySize(t, fixture)
|
||||
binaryTx, err := NewTxBinary(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("build tx binary fixture %s: %v", fixture, err)
|
||||
}
|
||||
encoded, err := binaryTx.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("encode fixture %s: %v", fixture, err)
|
||||
}
|
||||
|
||||
result := sizeResult{
|
||||
name: strings.TrimSuffix(filepath.Base(fixture), filepath.Ext(fixture)),
|
||||
swaps: len(tx.Swaps),
|
||||
platforms: len(tx.Platform),
|
||||
mevAgents: len(tx.MevAgent),
|
||||
addresses: len(binaryTx.AddressTable),
|
||||
encodedBytes: len(encoded),
|
||||
fixtureBytes: fixtureBytesLen,
|
||||
txBinaryBytes: rawTxBytesLen,
|
||||
}
|
||||
results = append(results, result)
|
||||
totalEncoded += result.encodedBytes
|
||||
}
|
||||
|
||||
for _, result := range results {
|
||||
t.Logf(
|
||||
"%s encoded=%dB swaps=%d platforms=%d mev=%d addresses=%d fixture_json=%dB raw_tx=%dB",
|
||||
result.name,
|
||||
result.encodedBytes,
|
||||
result.swaps,
|
||||
result.platforms,
|
||||
result.mevAgents,
|
||||
result.addresses,
|
||||
result.fixtureBytes,
|
||||
result.txBinaryBytes,
|
||||
)
|
||||
}
|
||||
|
||||
minResult := results[0]
|
||||
maxResult := results[0]
|
||||
for _, result := range results[1:] {
|
||||
if result.encodedBytes < minResult.encodedBytes {
|
||||
minResult = result
|
||||
}
|
||||
if result.encodedBytes > maxResult.encodedBytes {
|
||||
maxResult = result
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf(
|
||||
"summary fixtures=%d avg=%dB min=%dB(%s) max=%dB(%s)",
|
||||
len(results),
|
||||
totalEncoded/len(results),
|
||||
minResult.encodedBytes,
|
||||
minResult.name,
|
||||
maxResult.encodedBytes,
|
||||
maxResult.name,
|
||||
)
|
||||
}
|
||||
|
||||
func mustParseRPCFixtureTxForBinarySize(t *testing.T, fixturePath string) (*Tx, int, int) {
|
||||
t.Helper()
|
||||
|
||||
raw, err := os.ReadFile(fixturePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read fixture %s: %v", fixturePath, err)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Result *rpc.GetTransactionResult `json:"result"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &response); err != nil {
|
||||
t.Fatalf("unmarshal fixture %s: %v", fixturePath, err)
|
||||
}
|
||||
if response.Result == nil || response.Result.Transaction == nil || response.Result.Meta == nil {
|
||||
t.Fatalf("fixture %s is missing transaction data", fixturePath)
|
||||
}
|
||||
|
||||
rawBinary := response.Result.Transaction.GetBinary()
|
||||
if len(rawBinary) == 0 {
|
||||
t.Fatalf("fixture %s has empty transaction bytes", fixturePath)
|
||||
}
|
||||
|
||||
txWithMeta := rpc.TransactionWithMeta{
|
||||
Slot: response.Result.Slot,
|
||||
BlockTime: response.Result.BlockTime,
|
||||
Transaction: rpc.DataBytesOrJSONFromBytes(rawBinary),
|
||||
Meta: response.Result.Meta,
|
||||
Version: response.Result.Version,
|
||||
}
|
||||
|
||||
var blockTime *uint64
|
||||
if response.Result.BlockTime != nil {
|
||||
bt := uint64(*response.Result.BlockTime)
|
||||
blockTime = &bt
|
||||
}
|
||||
|
||||
rawTx, err := FromRpcTransactionWithMeta(txWithMeta, blockTime, response.Result.Slot, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("convert fixture %s: %v", fixturePath, err)
|
||||
}
|
||||
|
||||
tx, err := ParseRawTx(rawTx)
|
||||
if err != nil {
|
||||
t.Fatalf("parse fixture %s: %v", fixturePath, err)
|
||||
}
|
||||
return tx, len(rawBinary), len(raw)
|
||||
}
|
||||
998
tx_binary_test.go
Normal file
998
tx_binary_test.go
Normal file
@@ -0,0 +1,998 @@
|
||||
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"),
|
||||
},
|
||||
MevAgentZan: {
|
||||
MevAgent: MevAgentZan,
|
||||
MevAgentFee: decimal.RequireFromString("0.040000000"),
|
||||
},
|
||||
MevAgentTunneling: {
|
||||
MevAgent: MevAgentTunneling,
|
||||
MevAgentFee: decimal.RequireFromString("0.050000000"),
|
||||
},
|
||||
},
|
||||
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 !decoded.MevAgent[MevAgentZan].MevAgentFee.Equal(original.MevAgent[MevAgentZan].MevAgentFee) {
|
||||
t.Fatalf("Zan MevAgent fee mismatch")
|
||||
}
|
||||
if !decoded.MevAgent[MevAgentTunneling].MevAgentFee.Equal(original.MevAgent[MevAgentTunneling].MevAgentFee) {
|
||||
t.Fatalf("Tunneling MevAgent fee mismatch")
|
||||
}
|
||||
if len(decoded.Swaps) != 1 {
|
||||
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 TestTxBinaryLabelEnumsFallbackToUnknown(t *testing.T) {
|
||||
original := &Tx{
|
||||
Signer: solana.WrappedSol,
|
||||
Platform: map[string]platformInfo{
|
||||
"future-platform": {
|
||||
Platform: "future-platform",
|
||||
PlatformFee: decimal.RequireFromString("0.010000000"),
|
||||
},
|
||||
},
|
||||
MevAgent: map[string]mevInfo{
|
||||
"future-mev-agent": {
|
||||
MevAgent: "future-mev-agent",
|
||||
MevAgentFee: decimal.RequireFromString("0.020000000"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded, err := EncodeTxBinary(original)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeTxBinary() error = %v", err)
|
||||
}
|
||||
decoded, err := DecodeTxBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeTxBinary() error = %v", err)
|
||||
}
|
||||
|
||||
if len(decoded.Platform) != 1 {
|
||||
t.Fatalf("Platform len = %d, want 1", len(decoded.Platform))
|
||||
}
|
||||
if _, exists := decoded.Platform["future-platform"]; exists {
|
||||
t.Fatalf("future platform was preserved, want fallback")
|
||||
}
|
||||
if !decoded.Platform[PlatformNone].PlatformFee.Equal(original.Platform["future-platform"].PlatformFee) {
|
||||
t.Fatalf("PlatformNone fee = %s, want %s", decoded.Platform[PlatformNone].PlatformFee, original.Platform["future-platform"].PlatformFee)
|
||||
}
|
||||
|
||||
if len(decoded.MevAgent) != 1 {
|
||||
t.Fatalf("MevAgent len = %d, want 1", len(decoded.MevAgent))
|
||||
}
|
||||
if _, exists := decoded.MevAgent["future-mev-agent"]; exists {
|
||||
t.Fatalf("future mev agent was preserved, want fallback")
|
||||
}
|
||||
if !decoded.MevAgent[MevAgentUnknown].MevAgentFee.Equal(original.MevAgent["future-mev-agent"].MevAgentFee) {
|
||||
t.Fatalf("MevAgentUnknown fee = %s, want %s", decoded.MevAgent[MevAgentUnknown].MevAgentFee, original.MevAgent["future-mev-agent"].MevAgentFee)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxBinaryReadLabelEnumUnknownIDsFallback(t *testing.T) {
|
||||
enumTable := txBinaryEnumTables[txBinaryEnumVersionV1]
|
||||
|
||||
platformFee := uint64(123)
|
||||
platformEnc := txBinaryEncoder{}
|
||||
platformEnc.writeUint32(1)
|
||||
platformEnc.writeUint16(uint16(len(enumTable.platforms.values) + 10))
|
||||
platformEnc.writeUint64(platformFee)
|
||||
|
||||
platformDec := txBinaryDecoder{reader: bytes.NewReader(platformEnc.bytes())}
|
||||
platforms, err := txBinaryReadPlatformEntries(&platformDec, enumTable)
|
||||
if err != nil {
|
||||
t.Fatalf("txBinaryReadPlatformEntries() error = %v", err)
|
||||
}
|
||||
if len(platforms) != 1 || platforms[0].Platform != PlatformNone || platforms[0].PlatformFee != platformFee {
|
||||
t.Fatalf("platform fallback = %+v, want %s/%d", platforms, PlatformNone, platformFee)
|
||||
}
|
||||
|
||||
mevFee := uint64(456)
|
||||
mevEnc := txBinaryEncoder{}
|
||||
mevEnc.writeUint32(1)
|
||||
mevEnc.writeUint16(uint16(len(enumTable.mevAgents.values) + 10))
|
||||
mevEnc.writeUint64(mevFee)
|
||||
|
||||
mevDec := txBinaryDecoder{reader: bytes.NewReader(mevEnc.bytes())}
|
||||
mevAgents, err := txBinaryReadMevAgentEntries(&mevDec, enumTable)
|
||||
if err != nil {
|
||||
t.Fatalf("txBinaryReadMevAgentEntries() error = %v", err)
|
||||
}
|
||||
if len(mevAgents) != 1 || mevAgents[0].MevAgent != MevAgentUnknown || mevAgents[0].MevAgentFee != mevFee {
|
||||
t.Fatalf("mev agent fallback = %+v, want %s/%d", mevAgents, MevAgentUnknown, mevFee)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxBinaryAcceptsKnownEventEnums(t *testing.T) {
|
||||
events := []string{
|
||||
TxEventAddLP,
|
||||
TxEventRemoveLP,
|
||||
TxEventBuy,
|
||||
TxEventSell,
|
||||
TxEventBuyFailed,
|
||||
TxEventSellFailed,
|
||||
TxEventBurn,
|
||||
TxEventCreate,
|
||||
TxEventComplete,
|
||||
TxEventMigrate,
|
||||
TxEventDeposit,
|
||||
TxEventWithdraw,
|
||||
TxEventOpen,
|
||||
TxEventClose,
|
||||
TxEventClaimFee,
|
||||
TxEventAddLiquidity,
|
||||
TxEventAddLiquidityOneSide,
|
||||
TxEventRemoveLiquidity,
|
||||
TxEventRemoveLiquidityOneSide,
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
t.Run(event, func(t *testing.T) {
|
||||
txBinary := &TxBinary{
|
||||
SchemaVersion: txBinarySchemaVersionCurrent,
|
||||
EnumVersion: txBinaryEnumVersionV1,
|
||||
AddressTable: []solana.PublicKey{
|
||||
mustPubKey("11111111111111111111111111111111"),
|
||||
mustPubKey("So11111111111111111111111111111111111111112"),
|
||||
solana.TokenProgramID,
|
||||
mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||
mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||
},
|
||||
Swaps: []SwapBinary{
|
||||
{
|
||||
Program: SolProgramPump,
|
||||
Event: event,
|
||||
Pool: 0,
|
||||
BaseMint: 1,
|
||||
QuoteMint: 1,
|
||||
BaseTokenProgram: 2,
|
||||
QuoteTokenProgram: 2,
|
||||
Creator: 3,
|
||||
User: 4,
|
||||
FixedMint: 1,
|
||||
LimitMint: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded, err := txBinary.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalBinary() error = %v", err)
|
||||
}
|
||||
|
||||
var decoded TxBinary
|
||||
if err := decoded.UnmarshalBinary(encoded); err != nil {
|
||||
t.Fatalf("UnmarshalBinary() error = %v", err)
|
||||
}
|
||||
if got := decoded.Swaps[0].Event; got != event {
|
||||
t.Fatalf("decoded event = %q, want %q", got, event)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxBinaryPreservesFractionalReserves(t *testing.T) {
|
||||
tx := &Tx{
|
||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||
Block: 1,
|
||||
BlockIndex: 1,
|
||||
CuFee: decimal.NewFromInt(1),
|
||||
CUPrice: decimal.RequireFromString("0.000001"),
|
||||
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
|
||||
ComputeUnitsConsumed: 1,
|
||||
CuLimit: 1,
|
||||
Swaps: []Swap{
|
||||
{
|
||||
Program: SolProgramMeteoraPools,
|
||||
Event: TxEventAddLiquidity,
|
||||
Pool: mustPubKey("11111111111111111111111111111111"),
|
||||
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||
QuoteMint: solana.WrappedSol,
|
||||
BaseTokenProgram: solana.TokenProgramID,
|
||||
QuoteTokenProgram: solana.TokenProgramID,
|
||||
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||
BaseMintDecimals: 6,
|
||||
QuoteMintDecimals: 9,
|
||||
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||
BaseAmount: decimal.NewFromInt(10),
|
||||
QuoteAmount: decimal.NewFromInt(20),
|
||||
SwapMode: SwapModeExactIn,
|
||||
FixedAmount: decimal.NewFromInt(20),
|
||||
FixedAmountSide: SwapAmountSideQuote,
|
||||
FixedMint: solana.WrappedSol,
|
||||
LimitAmountType: SwapLimitTypeMinOut,
|
||||
LimitAmount: decimal.NewFromInt(9),
|
||||
LimitAmountSide: SwapAmountSideBase,
|
||||
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||
ActualLimitAmount: decimal.NewFromInt(10),
|
||||
ActualLimitAmountSide: SwapAmountSideBase,
|
||||
BaseReserve: decimal.RequireFromString("123.4"),
|
||||
QuoteReserve: decimal.RequireFromString("710079483.625409498"),
|
||||
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded, err := EncodeTxBinary(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeTxBinary() error = %v", err)
|
||||
}
|
||||
|
||||
decoded, err := DecodeTxBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeTxBinary() error = %v", err)
|
||||
}
|
||||
if got := decoded.Swaps[0].BaseReserve.String(); got != "123.4" {
|
||||
t.Fatalf("BaseReserve = %s, want 123.4", got)
|
||||
}
|
||||
diff := decoded.Swaps[0].QuoteReserve.Sub(decimal.RequireFromString("710079483.625409498")).Abs()
|
||||
if diff.GreaterThan(decimal.RequireFromString("0.0000001")) {
|
||||
t.Fatalf("QuoteReserve diff = %s, want <= 0.0000001", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxBinaryCanonicalizesOnSideEventAlias(t *testing.T) {
|
||||
tx := &Tx{
|
||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||
Block: 1,
|
||||
BlockIndex: 1,
|
||||
CuFee: decimal.NewFromInt(1),
|
||||
CUPrice: decimal.RequireFromString("0.000001"),
|
||||
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
|
||||
ComputeUnitsConsumed: 1,
|
||||
CuLimit: 1,
|
||||
Swaps: []Swap{
|
||||
{
|
||||
Program: SolProgramOrcaWhirPool,
|
||||
Event: "remove_liquidity_on_side",
|
||||
Pool: mustPubKey("11111111111111111111111111111111"),
|
||||
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||
QuoteMint: solana.WrappedSol,
|
||||
BaseTokenProgram: solana.TokenProgramID,
|
||||
QuoteTokenProgram: solana.TokenProgramID,
|
||||
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||
BaseMintDecimals: 6,
|
||||
QuoteMintDecimals: 9,
|
||||
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||
BaseAmount: decimal.NewFromInt(10),
|
||||
QuoteAmount: decimal.Zero,
|
||||
SwapMode: SwapModeExactIn,
|
||||
FixedAmount: decimal.NewFromInt(10),
|
||||
FixedAmountSide: SwapAmountSideBase,
|
||||
FixedMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||
LimitAmountType: SwapLimitTypeMinOut,
|
||||
LimitAmount: decimal.Zero,
|
||||
LimitAmountSide: SwapAmountSideQuote,
|
||||
ActualLimitAmount: decimal.Zero,
|
||||
ActualLimitAmountSide: SwapAmountSideQuote,
|
||||
BaseReserve: decimal.RequireFromString("123.4"),
|
||||
QuoteReserve: decimal.RequireFromString("456.7"),
|
||||
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded, err := EncodeTxBinary(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeTxBinary() error = %v", err)
|
||||
}
|
||||
|
||||
decoded, err := DecodeTxBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeTxBinary() error = %v", err)
|
||||
}
|
||||
if got := decoded.Swaps[0].Event; got != TxEventRemoveLiquidityOneSide {
|
||||
t.Fatalf("Event = %q, want %q", got, TxEventRemoveLiquidityOneSide)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user