Compare commits
49 Commits
v0.2.0
...
273e87b8ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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) {
|
func computeUnitLimitParser(offset [2]uint, tx *Tx, decodedData []byte) ([2]uint, error) {
|
||||||
if len(decodedData) < 8 {
|
if len(decodedData) < 4 {
|
||||||
return increaseOffset(offset), nil
|
return increaseOffset(offset), nil
|
||||||
}
|
}
|
||||||
|
tx.CuLimit = binary.LittleEndian.Uint32(decodedData[:4])
|
||||||
return increaseOffset(offset), nil
|
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
|
||||||
|
}
|
||||||
576
checking.go
576
checking.go
@@ -1,576 +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")
|
|
||||||
axiomProgramID = solana.MustPublicKeyFromBase58("AxiomfHaWDemCFBLBayqnEnNwE6b7B2Qz3UmzMpgbMG6")
|
|
||||||
gmgnProgramID = solana.MustPublicKeyFromBase58("GMgnVFR8Jb39LoXsEVzb3DvBy3ywCmdmJquHUy1Lrkqb")
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkPumpFunAxiomBuy(rawTx *RawTx) bool {
|
|
||||||
|
|
||||||
// 检查交易版本
|
|
||||||
if rawTx.Version == "legacy" || len(rawTx.Transaction.Message.AddressTableLookups) != 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 addressLookupTable 是否是 Axiom 的
|
|
||||||
if rawTx.Transaction.Message.AddressTableLookups[0].AccountKey != axiomTxLoopupTable {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查交易指令数量
|
|
||||||
if len(rawTx.Transaction.Message.Instructions) != 6 {
|
|
||||||
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, "jitodontfront") || !strings.HasSuffix(accountId, "TradeWithAxiomDotTrade") {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// axiom 会先创建 token 账户, 而不是 wsol 账户
|
|
||||||
accountId := accountList[instruction.Accounts[3]]
|
|
||||||
if accountId == solana.WrappedSol {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查调用axiom合约
|
|
||||||
{
|
|
||||||
instruction := rawTx.Transaction.Message.Instructions[3]
|
|
||||||
programId := accountList[instruction.ProgramIDIndex]
|
|
||||||
if programId != axiomProgramID {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 transfer
|
|
||||||
{
|
|
||||||
instruction := rawTx.Transaction.Message.Instructions[4]
|
|
||||||
programId := accountList[instruction.ProgramIDIndex]
|
|
||||||
if programId != solana.SystemProgramID {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 transfer
|
|
||||||
{
|
|
||||||
instruction := rawTx.Transaction.Message.Instructions[5]
|
|
||||||
programId := accountList[instruction.ProgramIDIndex]
|
|
||||||
if programId != solana.SystemProgramID {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkPumpFunGmgnBuy(rawTx *RawTx) bool {
|
|
||||||
|
|
||||||
// 检查交易版本
|
|
||||||
if rawTx.Version != "legacy" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查交易指令数量
|
|
||||||
if len(rawTx.Transaction.Message.Instructions) != 6 {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
accountId := accountList[instruction.Accounts[3]]
|
|
||||||
if accountId == solana.WrappedSol {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查调用 gmgn 合约
|
|
||||||
{
|
|
||||||
instruction := rawTx.Transaction.Message.Instructions[3]
|
|
||||||
programId := accountList[instruction.ProgramIDIndex]
|
|
||||||
if programId != gmgnProgramID {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 transfer
|
|
||||||
{
|
|
||||||
instruction := rawTx.Transaction.Message.Instructions[4]
|
|
||||||
programId := accountList[instruction.ProgramIDIndex]
|
|
||||||
if programId != solana.SystemProgramID {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 transfer
|
|
||||||
{
|
|
||||||
instruction := rawTx.Transaction.Message.Instructions[5]
|
|
||||||
programId := accountList[instruction.ProgramIDIndex]
|
|
||||||
if programId != solana.SystemProgramID {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
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 = "2AhpL5KhVmG3D38CwMzrHuRyTucEQ43GzBXL2mo5WiugdZMVmK1dtX8brGe3sxvvFDY6iSSviJTvqCtr4UL3Pc7J"
|
||||||
|
}
|
||||||
|
|
||||||
|
if txHash == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "txHash is empty; set it in cmd/rpc_parse/main.go")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, err := solana.SignatureFromBase58(txHash)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "invalid txHash: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := rpc.New(rpcURL)
|
||||||
|
maxSupportedVersion := uint64(0)
|
||||||
|
out, err := client.GetTransaction(context.Background(), sig, &rpc.GetTransactionOpts{
|
||||||
|
Encoding: solana.EncodingBase64,
|
||||||
|
Commitment: rpc.CommitmentConfirmed,
|
||||||
|
MaxSupportedTransactionVersion: &maxSupportedVersion,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "rpc getTransaction error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if out == nil || out.Transaction == nil || out.Meta == nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "rpc getTransaction returned empty response")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawBinary := out.Transaction.GetBinary()
|
||||||
|
if len(rawBinary) == 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, "rpc getTransaction returned empty transaction data")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
txWithMeta := rpc.TransactionWithMeta{
|
||||||
|
Slot: out.Slot,
|
||||||
|
BlockTime: out.BlockTime,
|
||||||
|
Transaction: rpc.DataBytesOrJSONFromBytes(rawBinary),
|
||||||
|
Meta: out.Meta,
|
||||||
|
Version: out.Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockTime *uint64
|
||||||
|
if out.BlockTime != nil {
|
||||||
|
bt := uint64(*out.BlockTime)
|
||||||
|
blockTime = &bt
|
||||||
|
}
|
||||||
|
|
||||||
|
rawTx, err := pump_parser.FromRpcTransactionWithMeta(txWithMeta, blockTime, out.Slot, 0)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "convert rpc transaction error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pump_parser.EnableAllParsers()
|
||||||
|
|
||||||
|
parsed, err := pump_parser.ParseRawTx(rawTx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "parse raw tx error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if len(parsed.Swaps) == 0 {
|
||||||
|
fmt.Println("no swaps parsed from tx")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, swap := range parsed.Swaps {
|
||||||
|
fmt.Printf("swap[%d]\n", i)
|
||||||
|
fmt.Printf(" program: %s\n", swap.Program)
|
||||||
|
fmt.Printf(" event: %s\n", swap.Event)
|
||||||
|
fmt.Printf(" pool: %s\n", swap.Pool)
|
||||||
|
fmt.Printf(" user: %s\n", swap.User)
|
||||||
|
fmt.Printf(" base_mint: %s (decimals=%d)\n", swap.BaseMint, swap.BaseMintDecimals)
|
||||||
|
fmt.Printf(" quote_mint: %s (decimals=%d)\n", swap.QuoteMint, swap.QuoteMintDecimals)
|
||||||
|
fmt.Printf(" base_amount: %s\n", swap.BaseAmount.String())
|
||||||
|
fmt.Printf(" quote_amount: %s\n", swap.QuoteAmount.String())
|
||||||
|
if swap.SwapMode != pump_parser.SwapModeUnknown {
|
||||||
|
fmt.Printf(" swap_mode: %s\n", swap.SwapMode.String())
|
||||||
|
fmt.Printf(" fixed_amount: %s\n", swap.FixedAmount.String())
|
||||||
|
fmt.Printf(" fixed_amount_side: %s\n", swap.FixedAmountSide.String())
|
||||||
|
fmt.Printf(" fixed_mint: %s\n", swap.FixedMint)
|
||||||
|
fmt.Printf(" limit_amount_type: %s\n", swap.LimitAmountType.String())
|
||||||
|
fmt.Printf(" limit_amount: %s\n", swap.LimitAmount.String())
|
||||||
|
fmt.Printf(" limit_amount_side: %s\n", swap.LimitAmountSide.String())
|
||||||
|
fmt.Printf(" limit_mint: %s\n", swap.LimitMint)
|
||||||
|
fmt.Printf(" actual_limit_amount: %s\n", swap.ActualLimitAmount.String())
|
||||||
|
fmt.Printf(" slippage_bps: %s\n", swap.SlippageBps.String())
|
||||||
|
}
|
||||||
|
if !swap.FeeAmount.IsZero() || swap.FeeSide != "" {
|
||||||
|
fmt.Printf(" fee_amount: %s\n", swap.FeeAmount.String())
|
||||||
|
fmt.Printf(" lp_fee_amount: %s\n", swap.LpFeeAmount.String())
|
||||||
|
fmt.Printf(" fee_side: %s\n", swap.FeeSide)
|
||||||
|
fmt.Printf(" fee_mint: %s (decimals=%d)\n", swap.FeeMint, swap.FeeMintDecimals)
|
||||||
|
fmt.Printf(" fee_token_program: %s\n", swap.FeeTokenProgram)
|
||||||
|
}
|
||||||
|
fmt.Printf(" base_reserve: %s\n", swap.BaseReserve.String())
|
||||||
|
fmt.Printf(" quote_reserve: %s\n", swap.QuoteReserve.String())
|
||||||
|
fmt.Printf(" base_token_program: %s\n", swap.BaseTokenProgram)
|
||||||
|
fmt.Printf(" quote_token_program: %s\n", swap.QuoteTokenProgram)
|
||||||
|
fmt.Printf(" entry_contract: %s\n", swap.EntryContract)
|
||||||
|
fmt.Printf(" user_base_balance: %s\n", swap.UserBaseBalance.String())
|
||||||
|
fmt.Printf(" user_quote_balance: %s\n", swap.UserQuoteBalance.String())
|
||||||
|
fmt.Printf(" active_bin_id: %d\n", swap.ActiveBinId)
|
||||||
|
fmt.Printf(" start_bin_id: %d\n", swap.StartBinId)
|
||||||
|
fmt.Printf(" end_bin_id: %d\n", swap.EndBinId)
|
||||||
|
fmt.Printf(" remove_bp: %d\n", swap.RemoveBp)
|
||||||
|
fmt.Printf(" position_account: %s\n", swap.PositionAccount)
|
||||||
|
if swap.Mayhem {
|
||||||
|
fmt.Printf(" mayhem: true\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" mayhem: false\n")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
188
consts.go
188
consts.go
@@ -39,6 +39,14 @@ var platformFeeAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("F4hJ3Ee3c5UuaorKAMfELBjYCjiiLH75haZTKqTywRP3"): PlatformBullX,
|
solana.MustPublicKeyFromBase58("F4hJ3Ee3c5UuaorKAMfELBjYCjiiLH75haZTKqTywRP3"): PlatformBullX,
|
||||||
solana.MustPublicKeyFromBase58("47hEzz83VFR23rLTEeVm9A7eFzjJwjvdupPPmX3cePqF"): PlatformBanana,
|
solana.MustPublicKeyFromBase58("47hEzz83VFR23rLTEeVm9A7eFzjJwjvdupPPmX3cePqF"): PlatformBanana,
|
||||||
solana.MustPublicKeyFromBase58("9yMwSPk9mrXSN7yDHUuZurAh1sjbJsfpUqjZ7SvVtdco"): PlatformTrojan,
|
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("4mih95RmBqfHYvEfqq6uGGLp1Fr3gVS3VNSEa3JVRfQK"): PlatformRaybot,
|
||||||
solana.MustPublicKeyFromBase58("3udvfL24waJcLhskRAsStNMoNUvtyXdxrWQz4hgi953N"): PlatformMoonshot,
|
solana.MustPublicKeyFromBase58("3udvfL24waJcLhskRAsStNMoNUvtyXdxrWQz4hgi953N"): PlatformMoonshot,
|
||||||
solana.MustPublicKeyFromBase58("3kxSQybWEeQZsMuNWMRJH4TxrhwoDwfv41TNMLRzFP5A"): PlatformMEVX,
|
solana.MustPublicKeyFromBase58("3kxSQybWEeQZsMuNWMRJH4TxrhwoDwfv41TNMLRzFP5A"): PlatformMEVX,
|
||||||
@@ -59,6 +67,7 @@ var platformFeeAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("HkJYryz2BNeMQfuuSWDYktWt5fZLV26eK6nqu7EJycoG"): PlatformAxiom,
|
solana.MustPublicKeyFromBase58("HkJYryz2BNeMQfuuSWDYktWt5fZLV26eK6nqu7EJycoG"): PlatformAxiom,
|
||||||
solana.MustPublicKeyFromBase58("BfFX9rUm8qTZiZjmeq9BktWVTNuG3YWMc5AvkrCKJike"): PlatformAxiom,
|
solana.MustPublicKeyFromBase58("BfFX9rUm8qTZiZjmeq9BktWVTNuG3YWMc5AvkrCKJike"): PlatformAxiom,
|
||||||
solana.MustPublicKeyFromBase58("2ApLdwLrGayEmxgpLX9BTR47Q2QprfMg5SpjrLeaK8s7"): PlatformAxiom,
|
solana.MustPublicKeyFromBase58("2ApLdwLrGayEmxgpLX9BTR47Q2QprfMg5SpjrLeaK8s7"): PlatformAxiom,
|
||||||
|
solana.MustPublicKeyFromBase58("AVahywMVNRYzdgWrufSWrtdGXAeNEvfpJFxhVFK516mT"): PlatformDexScreener,
|
||||||
}
|
}
|
||||||
|
|
||||||
var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
||||||
@@ -176,6 +185,7 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("soyascXFW5wEEYiwfEmHy2pNwomqzvggJosGVD6TJdY"): MevAgentSoyas,
|
solana.MustPublicKeyFromBase58("soyascXFW5wEEYiwfEmHy2pNwomqzvggJosGVD6TJdY"): MevAgentSoyas,
|
||||||
solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas,
|
solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas,
|
||||||
solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas,
|
solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas,
|
||||||
|
solana.MustPublicKeyFromBase58("soyasF3QPWPAKKmgA3GjfWax1kmTT1aoqSGxPzVLNUQ"): MevAgentSoyas,
|
||||||
solana.MustPublicKeyFromBase58("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium,
|
solana.MustPublicKeyFromBase58("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium,
|
||||||
solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium,
|
solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium,
|
||||||
solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium,
|
solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium,
|
||||||
@@ -188,6 +198,7 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("ASde6y8pBCU1aityWHRpqT7pEAcEonjCgFUMeh5egRes"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("ASde6y8pBCU1aityWHRpqT7pEAcEonjCgFUMeh5egRes"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("ASUv6G8Cj6zt71UAqD1aVtDC3CRn6FFddqF17ZiegrES"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("ASUv6G8Cj6zt71UAqD1aVtDC3CRn6FFddqF17ZiegrES"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane,
|
||||||
@@ -199,6 +210,177 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("B1ooMauwuJPhHsXqt3uj7B92CAFG8kaD1Q2iGEmGYnx"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("B1ooMauwuJPhHsXqt3uj7B92CAFG8kaD1Q2iGEmGYnx"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("B1ooMdjcY7zemxDWiH8jVZPxEMdHnE5AraWPHdHQoPj"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("B1ooMdjcY7zemxDWiH8jVZPxEMdHnE5AraWPHdHQoPj"): MevAgentAstralane,
|
||||||
solana.MustPublicKeyFromBase58("B1ooMKzu6siJzQutP6a6oLiY3fpzgQnBZsAjxuAm9qo"): MevAgentAstralane,
|
solana.MustPublicKeyFromBase58("B1ooMKzu6siJzQutP6a6oLiY3fpzgQnBZsAjxuAm9qo"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("AstrA1ejL4UeXC2SBP4cpeEmtcFPZVLxx3XGKXyCW6to"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("AsTra79FET4aCKWspPqeSFvjJNyp96SvAnrmyAxqg5b7"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("AsTRADtvb6tTmrsqULQ9Wji9PigDMjhfEMza6zkynEvV"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("AsTRAEoyMofR3vUPpf9k68Gsfb6ymTZttEtsAbv8Bk4d"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("AStrAJv2RN2hKCHxwUMtqmSxgdcNZbihCwc1mCSnG83W"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("Astran35aiQUF57XZsmkWMtNCtXGLzs8upfiqXxth2bz"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("AStRAnpi6kFrKypragExgeRoJ1QnKH7pbSjLAKQVWUum"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("ASTRaoF93eYt73TYvwtsv6fMWHWbGmMUZfVZPo3CRU9C"): MevAgentAstralane,
|
||||||
|
solana.MustPublicKeyFromBase58("Gu2UGEfze3Gg5cHuEC4jGbyCufgpev75RkVvBdKKtf12"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("E8wD3SMD1trozPrvSN9F6SyuUXD7rrFDuR3WexGziKG5"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("18hCV7f9CPmZRAH3QCNZaGHhHeNSfisQKeKuFkQsPLY"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("2sYKRWBNVY6UomMBi4juoMrrL98bqizDMn98cJ3cBmye"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("CZubxabMM7CPFSDAfMUhxNuvXRDLjDf6yVVq1RoJ66rk"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("Dz8rMcdokTLfbnNz2ZdYocZixgaA1TMqbA31xtwPgcxb"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("ForLDu55GfA2U1aTUaitmjzjs92vvVn1MSqzY3D9HtAK"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("6MgjyQU7G988jgL6EGAgfHYoeesCnwYMyPeh1fpJ71FP"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("12pHu2j2DDShyCVFU7vtSLXga74et9y83VD38mw6XYhB"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("5QuV4TS5TJFWPu7Yd56VaPvf4nKUicPvTfC3mwnb7dNW"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("4gh9m7RV7G4WwRftA6qV7RhDfytdepb3XbxFRfTtneYJ"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("AumQWSLrWwDXRq1yDEYPiw8vT5NUBYzrbdWCprJ4ZUa8"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("3vGEsQA5jzvN8TBgytuYEdZxW6P2pK1c6pq56JiFuygS"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("AsEF2SWSEZ1xpGZ5fdzDKaoka1XEtFSjGo39YUXkpvAh"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("2WoQNgmc4SEXrR3rKQypmeWmsxGqHHE6rApnVrP6Pt77"): MevAgent0slot,
|
||||||
|
solana.MustPublicKeyFromBase58("9vTpfGYN2jtjZgXQ7gihyHmN3FseLP7uW1CWMdsgcny"): MevAgentBlocxRoute,
|
||||||
|
solana.MustPublicKeyFromBase58("CyL8mfycXYbWHVoTTsfvnAfF2MvfcqeQAmmsqNQLxF7g"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("Eg85QSYLwtZfBBPF4CsNmijJDXUAeCMjoh36L1cwboqg"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("9gBzvLKedrs9HxaLPhBdkPaeFTxEDNDGfqJmqvHjfiZp"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("7BxoFqM3swL46Lt9EWzL9z2LeXYfmJL7MVzpFrDpLPei"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("91Ht2gq1CMPcLySuq8NjHaA1rXysm8zzoiiyfT4uSE7u"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("2zCYpNSWcHX9AzFndF1mcT1bMkG1EXMzzjFcBjSnJq9f"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("4Kfqkx3c8TxLX74J1nzfzfHCGdoDCuZ8k84sGpnVh1a4"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("GeiVfSfUBVxjJA6F2SNSASoK8JaSCiSmsC2hBrPLfpiv"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("DggsS83MWeUHZdrV2jyMUh8GDfLrU5P9Es36h7Uf3wRp"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("2d5viHZBHKt5DgEpMckXEfndR1CoZ1tHvcbL9fU4xqT7"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("73VnqgMJq29j4HMzF6GRdBeVpZgz7ibouyKQvyAKbVZy"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("HvgA9hTyrTQCU5869fhZ7My9WkkHK2yBo4Wu6ojHmMio"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("4xJEQnuMpoUNxhNew4AechRBo1DnpVfLyUe68BXTTF73"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("7ZKL8BAPfKKa6FNmds48QKFnckrcj4mkppRnsBAR2xVH"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("BYpBPSRkVSvutxHngtxnqeoTBrENZ8iM56Ywnsmy829w"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("4LEkLhb2u5qCUXS1Hc3eL2zTxk2kjSzQeFK4ZgWsV3EM"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("96Zc2GT7ZmMvF7rXgcwHAyJ7KmK8RaS4Z3VZw2b7GjJx"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("9Kaz3Q9KJ3x8SXvui37FK5m1AwcwqkYLvS9Xg1Why9Q1"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("muV321VhQ4XgJkVtsZP13zbCqg9HokT222bWS3DBxp3"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("Hth7qf5dv683k3ZJffjJvJ8gSU21dfPWy3mBEyRRhCiN"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("pD7KfmGkxHqQFNLqYv3zshSzkGaAB99vjNDKz6e7nGC"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("AQChXZ1ZWvPH8EjdPxXXsC8VqCaBmPVruJbswhE3xNZ8"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("2L2DgQ5ZXRYnv8K97NFDJvsNrA1MsrCGr3CvokPtDy8D"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("4BjQeBGZmGNWeHfQC4scHK5d4RtDr79h1hZNPcrLDS8C"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("E99TTcqBPAY1F4ZppMRkDX3pTqaSnRC24tUErfd2opNL"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("8zi6AG7oSKoswSEMaxNmXrwBYmDwuQ4GLiY4Q1j9Rayu"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("8GgU7tKJSA97G97kD9AbxYgsC9Hcjfg7RpAofWuA6oHt"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("JDxQoXGFRwEojWzkirDNeHz88SDEPzdDakjsobJ4YHrj"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("GQgHdPuDNcss3BoKrMfS6bgGekjitmKQRJxnuhUBu921"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("8FoxPbnucCZ3wuzhMofKE5VdYKcHfWmYNrnC2whVBAhS"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("3L9UZWLAprLtB2xddEHsCmgXbPc2PidgSjtHGZd2MzB3"): MevAgentBlockRazor,
|
||||||
|
solana.MustPublicKeyFromBase58("FAST3dMFZvESiEipBvLSiXq3QCV51o3xuoHScqRU6cB6"): MevAgentFast,
|
||||||
|
solana.MustPublicKeyFromBase58("FASTCKnwwY6iL3CknRgg3Zqir7jeagDDhxSnBQQy5a1C"): MevAgentFast,
|
||||||
|
solana.MustPublicKeyFromBase58("FASTHPW6akdGh9PFSdhMTbCuGkCSX7LsUjjnaB2RTQ4v"): MevAgentFast,
|
||||||
|
solana.MustPublicKeyFromBase58("FASTKL1AamNKrwnvbKwo4PU8434BBdqVrTtugM6oDU71"): MevAgentFast,
|
||||||
|
solana.MustPublicKeyFromBase58("FASTPB76TxKPMZ7Q29m8v4zJn8gUjbWyvTEQaaxhwN7M"): MevAgentFast,
|
||||||
|
solana.MustPublicKeyFromBase58("FASTYKWXRfAoty7SQCM1mGVrmPUyyNcF4tc3DUkLDAu9"): MevAgentFast,
|
||||||
|
solana.MustPublicKeyFromBase58("FASTYmSidNfLwdwiQEhCTtzghxEtaipeNSDSwh9xDPs3"): MevAgentFast,
|
||||||
|
solana.MustPublicKeyFromBase58("FASTs6ctgbsuZegMzUs4DPUYhRSZUPCjgCVnttHbpQAp"): MevAgentFast,
|
||||||
|
solana.MustPublicKeyFromBase58("node1FdMPnJBN7QTuhzNw3VS823nxFuDTizrrbcEqzp"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1zrVjcY2XB3Au8qYj5MxjbNfGu3baHaqZMkPM7Z"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1E3hguapYA18HCpEEkRHQmLNiyv9pdfE9s2zo5X"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1CVxtFas2Pw5Vcf86Pq89Hqx4jveo1ntY7ARFMK"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1EoLojAvoUmyDytcvgdXs6GPtY3zpQXPCRVncEA"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1VwH169UqyJHr5MYCH3EBuwrdvn5KHXAkhEEfav"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1JkDqyiEg7CDNj3ATPiRmWaAG2gnrAEiMJ4Rzcc"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1GS8pZnP6MzGSXwhA2MXH6EBfCpFaAE64G2ubpB"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1AVfbcSi98LAgGyAHUGS4eYkYTbS5vUPZYQnViF"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1L7Xat2tSkRNNi6TSuUScMYfj64ovhr2aceJm9g"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1kMY97W3LPXaKKV43yRa2Q3BLg4WZiT27VifUDc"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1Zi3r7hmGYwF9cJAkfCHh9EKWbkSrYdvcvLukF4"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1G3fmoCuEJzcPNF4hLbSZ2ypcUuh9CB3k9E7Q8k"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node18nQgpjoKe1fM72GiV6tHXg5dMKbVPFGwRBD9MU"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1spgxXR8HCbm4LyZNoisFLmBXxy2qnZrv63WxMp"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("node1rmmFXeLh94mBGtDHbSwCrBJqDnc16xrURHRYD9"): MevAgentNode1,
|
||||||
|
solana.MustPublicKeyFromBase58("EnchantKMZ93cDKwsnyvnD5WCpZLFTLVRWozFjAUzTko"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("CJwbwPfVFZDPGKJKCtLkzDJPFrGyyroEPFjXigmJB6mr"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("3Thhhj3omvVFfbhEHdFe8djwDZT5oS6BQ4k5KrZkYt1r"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("6CQzBpGJn6XYcCkm77xNd944MpbjLHLsP6sCEWSZVUHS"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("DeMZbwKtu9kteFdxL1yh6aTWqDwYfH79DKzYrgfTwAc2"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("3Pn9ZFCsNTf9MvWbpemQccWuyHNMbBjxg1eW53ikHcpH"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("DjfRXWegRn9bWnBvZFxAnpu1jNikcoy8iiu6ZX9AxAd5"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("8mbjzuz8ka3zVGnry6xMEwm96tzk4yKnWgvwAT1LwEGx"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("5KpS5Q3nUtp1cUynUxzH2bA93SWzmx2y3GwU45AeEEP5"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgK8f5H4ocVdNrkUrspUFmAaEosGQtbc1JCMqLwvvRe"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bg7BgfutLpjFdxDNcbwQFGFkLGQT9Kww9wv6EWUHQr3"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bg9zUQnVkYLgAWJvL9MjP4tFDecCxbvmQRqrAuZpQUA"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgu2xgHEJocs4tggHDEwNnmgduftnXfJuWoLiUYfiLW"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgd4MvpBH3LaVz6sHvqFphoUex4taUe2E5mKuk4sVXn"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgSfpx2Pr3bHYev6ikwTqdBo2aaPGgjEseAWhjxp6F5"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgfaB4sngcm7cARjjiEvKfWE87owf2HuDfYDy8EyP45"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgzmGhu9qcyLW6qR1HKQuLTY6PWktNSAuzLNmo7aiQY"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgMTi1qFtbiFiHsURKW4Bfg4wjXtT8iJL7HC1z3gXsm"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgDRbhSLK62ApA2PbZs1W7SecodGhTFf6udU3MWDadu"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bg67LJN9Ngvfq4hJbSmm7tZ2wqmn2f1pxXbXW5QfxRz"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgmnrKWgN5jE8pF3PbFxRWYaho1bjCtmcTZ9VfRbhxf"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgTZqxCX4ej98P1UyYJjgGmGDmst7nteSyUWDwzMxNj"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgauKkwFcT8w7SHau9NufDfvmq1cy79X52bRbL6yzEB"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bg8WP8cEtWhdCjDd7rrwzsnz7K9f3oiEm2Qqu7TYmDn"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("DzrVK357ynzkPtdC7jzUbXgsUY8ULUeR2ihoPcX1JB3n"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfn2d1g6xkwhykkyjtoccFbC7r19ADf5dGB2YnT1Hgw"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfnXi2FdpFUUn6VyoxUohNyWk2Nup3ruguTgK8jaZaF"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bg1rCzhyASbzib75ohpRfNY3mGJaX1k6v56WCrUkh3a"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgsouue9XeHUzNwwuAKqBj1Fk1RbJkcBjvs4zkmUhLc"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("mwGELGMgGGrNL1UibNCQeJHDE7qdPptWRYB6noUHmTj"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfn9b35be4L7xh7G8P2jUzWsJAigrKDSoBeRMiyg75p"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfnSbG36fCGpT8WsB1NEbQ2BH11iog6qjFqMEVCZZgV"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfnaxrmMoemfvbhXek6offTNXas11GtepGQMN9UF3gk"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfnKWwarhjuKKg3WV3nw3wAE8zuymigT3vuJHwZeL4s"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfnaZXdkjJq26auzzFKeQm7YKphuNCdDGcJVqqb6awr"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfnP85qobXv2wETniKjXBhxKvgivpfT8EGAcS8sb3bq"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfnSAoQWtJCDKnjmR8oduqbZYXr69Q4cFQ6VhgFkvgT"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfnSbziLrSSVNqPBD9tpx3Ud4VtbxwsXjdfYv9SmBDx"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("Vn7tfMvrvrymGYMnxhj1DV16Sz2R9YXmaXF3hiSAHuC"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfnTcJ1i4mRYzbqGduF71RsooUCFkPSpk8UE7drCkjh"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfnUxCuZcfP6yidkG3EsqyR5DTbyie3R74fGoA5oB3J"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfnEJvqLGddJxQTA9DcYLTbVwiFdT3KmLXo6UcnmcgC"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfn6dyanKiTTinHs887D7qe2S4727wzK7xi7ERGaizC"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgDETv6tnt9mwYqAKebLXY5B5o6akiKJmAdU7Gd9G7H"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("bgH7YhymSykyvMa3nAZpzvrn73owJHU5iB75S1aiLT9"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("pfngGVVQLiVRFbLWw3Ektiv17ef9NiRZbcgdAhh4ZEW"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("nEFs3jph8HJt7honu3k7XtGUufMnwAvSXmXcKSPxryP"): MevAgentNozomi,
|
||||||
|
solana.MustPublicKeyFromBase58("Fa1con11xLjPddfzRwRUB16sbFZggp2JeJkCeWREyR8X"): MevagentFa1con,
|
||||||
|
solana.MustPublicKeyFromBase58("Fa1con11TM1RuAQzbQzYjTy4Ekfap9Lnc9fnEbQYEd6Q"): MevagentFa1con,
|
||||||
|
solana.MustPublicKeyFromBase58("Fa1con113Bvi76nS5AzUiRDC2fqjfzkNMUNRLgQybMYt"): MevagentFa1con,
|
||||||
|
solana.MustPublicKeyFromBase58("Fa1con1QGHJK232s8yZpzZZwqPexnAKcoyKj626LNsMv"): MevagentFa1con,
|
||||||
|
solana.MustPublicKeyFromBase58("Fa1con1zUzb6qJVFz5tNkPq1Ahm8H1qKW7Q48252QbkQ"): MevagentFa1con,
|
||||||
|
solana.MustPublicKeyFromBase58("Fa1con16d3MSwd3SAiwvr2LwgkpE7ot8zntbpuec8HAx"): MevagentFa1con,
|
||||||
|
solana.MustPublicKeyFromBase58("Fa1con1i7mpa7Qc6epYJ6r4P9AbU77DFFz173r59Df1x"): MevagentFa1con,
|
||||||
|
solana.MustPublicKeyFromBase58("Fa1con18nWn8TdAGL7JX8PertfMUGVSc899NawokJ4Bq"): MevagentFa1con,
|
||||||
|
solana.MustPublicKeyFromBase58("Fa1con1GKusK2EqsfzrDzGPaYZSxQtFGzJiRMMU9Zm2g"): MevagentFa1con,
|
||||||
|
solana.MustPublicKeyFromBase58("Fa1con1RDwVwM9VrJ53CwVefD3VU9c58EMpDawV7fLMi"): MevagentFa1con,
|
||||||
|
solana.MustPublicKeyFromBase58("Sp1x2AqpQckPLaWnWCJUNg8k6qQexfaEWcSRKf5JcDV"): MevagentBlocksprint,
|
||||||
|
solana.MustPublicKeyFromBase58("Sp4JHSh9cksfzXbgK7Pq2ovtn8LirLQydaJKTsiNT77"): MevagentBlocksprint,
|
||||||
|
solana.MustPublicKeyFromBase58("Sp1xMS2cbw83SZDNr4AGqkBYYLjb3LvVnmDSrTMaHkr"): MevagentBlocksprint,
|
||||||
|
solana.MustPublicKeyFromBase58("SpagSJmnh8E9cGT5Y431xPPaS2c1xLREGGCWN9yDeUf"): MevagentBlocksprint,
|
||||||
|
solana.MustPublicKeyFromBase58("SpWrza9E63MQuHeGnnfzmtLVCs3pBdjyKPXUABPo9nq"): MevagentBlocksprint,
|
||||||
|
solana.MustPublicKeyFromBase58("moon17L6BgxXRX5uHKudAmqVF96xia9h8ygcmG2sL3F"): MevAgentMoon,
|
||||||
|
solana.MustPublicKeyFromBase58("moon26Sek222Md7ZydcAGxoKG832DK36CkLrS3PQY4c"): MevAgentMoon,
|
||||||
|
solana.MustPublicKeyFromBase58("moon7fwyajcVstMoBnVy7UBcTx87SBtNoGGAaH2Cb8V"): MevAgentMoon,
|
||||||
|
solana.MustPublicKeyFromBase58("moonBtH9HvLHjLqi9ivyrMVKgFUsSfrz9BwQ9khhn1u"): MevAgentMoon,
|
||||||
|
solana.MustPublicKeyFromBase58("moonCJg8476LNFLptX1qrK8PdRsA1HD1R6XWyu9MB93"): MevAgentMoon,
|
||||||
|
solana.MustPublicKeyFromBase58("moonF2sz7qwAtdETnrgxNbjonnhGGjd6r4W4UC9284s"): MevAgentMoon,
|
||||||
|
solana.MustPublicKeyFromBase58("moonKfftMiGSak3cezvhEqvkPSzwrmQxQHXuspC96yj"): MevAgentMoon,
|
||||||
|
solana.MustPublicKeyFromBase58("moonQBUKBpkifLcTd78bfxxt4PYLwmJ5admLW6cBBs8"): MevAgentMoon,
|
||||||
|
solana.MustPublicKeyFromBase58("moonXwpKwoVkMegt5Bc776cSW793X1irL5hHV1vJ3JA"): MevAgentMoon,
|
||||||
|
solana.MustPublicKeyFromBase58("moonZ6u9E2fgk6eWd82621eLPHt9zuJuYECXAYjMY1C"): MevAgentMoon,
|
||||||
|
solana.MustPublicKeyFromBase58("SpEEdz8S1KorkMZqjMUxfxrmWwofmp6ReNP2Nx6CUmq"): MevAgentSpeedlanding,
|
||||||
|
solana.MustPublicKeyFromBase58("SpeeDy3GJM4wcrQmk1itRFWgidvxX4rwjTLMv78wwjE"): MevAgentSpeedlanding,
|
||||||
|
solana.MustPublicKeyFromBase58("SPeEdva37vW8vRtqgYjprQs1g3965icfVN5Rt7SMAyh"): MevAgentSpeedlanding,
|
||||||
|
solana.MustPublicKeyFromBase58("speEdrSEpox5GUfHWcBc7tQjRuSfUin2yvB7qoYvvJh"): MevAgentSpeedlanding,
|
||||||
|
solana.MustPublicKeyFromBase58("SPeEDmkHkN3A2roSZf6aZyEMsmrGqTHKqwP51y2Y4rV"): MevAgentSpeedlanding,
|
||||||
|
solana.MustPublicKeyFromBase58("SpeedLdTJXh2RKpXEaP8JCxkWoUVXhtdPQ1EnxBJMxc"): MevAgentSpeedlanding,
|
||||||
|
solana.MustPublicKeyFromBase58("SpEediGKLbbXndSYTzwmz6Z3NDgHQLDcTDEvGFkSMH9"): MevAgentSpeedlanding,
|
||||||
|
solana.MustPublicKeyFromBase58("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"): MevAgentSpeedlanding,
|
||||||
|
solana.MustPublicKeyFromBase58("harkEpXoJv5qVzHaN7HSuUAd6PHjyMcFMcDYBMDJCEQ"): MevAgentAllenhark,
|
||||||
|
solana.MustPublicKeyFromBase58("harkm2BTWxZuszoNpZnfe84jRbQTg6KGHaQBmWzDGQQ"): MevAgentAllenhark,
|
||||||
|
solana.MustPublicKeyFromBase58("harkR2YJ4Dpt4UDJTcBirjnSPBhNpQFcoFkNpCkVqNk"): MevAgentAllenhark,
|
||||||
|
solana.MustPublicKeyFromBase58("t3QLYyXH4vZYbEifLqjD581t5dPVhq9LABxWceySzL2"): MevAgentRaiden,
|
||||||
|
solana.MustPublicKeyFromBase58("t46SqGmwStEffUMp1fr2xmv5uyR85TB9annJuLKLf83"): MevAgentRaiden,
|
||||||
|
solana.MustPublicKeyFromBase58("t1TcSg9biJsz4NjKjhopK8QZzPS4KzBgFSszu5QTGgF"): MevAgentRaiden,
|
||||||
|
solana.MustPublicKeyFromBase58("t2XFAFBaUkCzxJwEbLWFX9PKFjfBCp2tSyFtx5z4RZM"): MevAgentRaiden,
|
||||||
|
solana.MustPublicKeyFromBase58("t55hdzzftxWkYy3J8t32C9RRcZDuMZ4LDuBmbTzJFkU"): MevAgentRaiden,
|
||||||
|
solana.MustPublicKeyFromBase58("t6UtTQLUGHJJrzxAb8PBBZdZKra8SWUqvTv9zPnxKNz"): MevAgentRaiden,
|
||||||
|
solana.MustPublicKeyFromBase58("t7qUQU35sLpPydh42BcPmtEfTWW8gBe4Ry3gjwVnokJ"): MevAgentRaiden,
|
||||||
|
solana.MustPublicKeyFromBase58("t8pPgarSK3TnuLbbHmoE1RCQdLxfxuPqNTyFjBKahok"): MevAgentRaiden,
|
||||||
|
solana.MustPublicKeyFromBase58("t96GGdw3MiaGR993XN8PSsRpKGXx56t5Wf6zcF1hBpY"): MevAgentRaiden,
|
||||||
}
|
}
|
||||||
|
|
||||||
var entryContractAddresses = map[solana.PublicKey]string{
|
var entryContractAddresses = map[solana.PublicKey]string{
|
||||||
@@ -239,8 +421,14 @@ var entryContractAddresses = map[solana.PublicKey]string{
|
|||||||
solana.MustPublicKeyFromBase58("BBRouter1cVunVXvkcqeKkZQcBK7ruan37PPm3xzWaXD"): EntryContractBonkBot,
|
solana.MustPublicKeyFromBase58("BBRouter1cVunVXvkcqeKkZQcBK7ruan37PPm3xzWaXD"): EntryContractBonkBot,
|
||||||
solana.MustPublicKeyFromBase58("B3111yJCeHBcA1bizdJjUFPALfhAfSRnAbJzGUtnt56A"): EntryContractBinanceWallet,
|
solana.MustPublicKeyFromBase58("B3111yJCeHBcA1bizdJjUFPALfhAfSRnAbJzGUtnt56A"): EntryContractBinanceWallet,
|
||||||
solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9"): EntryContractAxiom,
|
solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9"): EntryContractAxiom,
|
||||||
|
solana.MustPublicKeyFromBase58("F5tfvbLog9VdGUPqBDTT8rgXvTTcq7e5UiGnupL1zvBq"): EntryContractAxiom,
|
||||||
|
solana.MustPublicKeyFromBase58("Gz9VPiSLQYbvKyb3jZPjNfyA6n4T4qVFUuAukgL964nL"): EntryContractAxiom,
|
||||||
solana.MustPublicKeyFromBase58("B3jytJa6Tzpn4Ly7GNnDm3dMGqUin5aMRm5aPsJGU5G7"): EntryContractTradewiz,
|
solana.MustPublicKeyFromBase58("B3jytJa6Tzpn4Ly7GNnDm3dMGqUin5aMRm5aPsJGU5G7"): EntryContractTradewiz,
|
||||||
|
solana.MustPublicKeyFromBase58("DBotWvSso9oD1ZB3aHx2LiD2ZoFpF8PbKjaT4uHKLLVs"): EntryContractDbot,
|
||||||
|
solana.MustPublicKeyFromBase58("term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3"): EntryContractPadre,
|
||||||
}
|
}
|
||||||
|
|
||||||
var okxDexRoutersV2 = solana.MustPublicKeyFromBase58("proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u")
|
var okxDexRoutersV2 = solana.MustPublicKeyFromBase58("proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u")
|
||||||
var okxAggregatorV2 = solana.MustPublicKeyFromBase58("6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma")
|
var okxAggregatorV2 = solana.MustPublicKeyFromBase58("6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma")
|
||||||
|
|
||||||
|
var axiomOuterContract = solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9")
|
||||||
|
|||||||
63
enum.go
63
enum.go
@@ -1,19 +1,26 @@
|
|||||||
package pump_parser
|
package pump_parser
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MevAgentJito = "jito"
|
MevAgentJito = "jito"
|
||||||
MevAgent0slot = "0slot"
|
MevAgent0slot = "0slot"
|
||||||
MevAgentBlocxRoute = "blocxroute"
|
MevAgentBlocxRoute = "blocxroute"
|
||||||
MevAgentNozomi = "nozomi"
|
MevAgentNozomi = "nozomi"
|
||||||
MevAgentNextBlock = "nextblock"
|
MevAgentNextBlock = "nextblock"
|
||||||
MevAgentHelius = "helius"
|
MevAgentHelius = "helius"
|
||||||
MevAgentNode1 = "node1"
|
MevAgentNode1 = "node1"
|
||||||
MevAgentFlashBlock = "flashBlock"
|
MevAgentFlashBlock = "flashBlock"
|
||||||
MevAgentUnknown = "unknown"
|
MevAgentUnknown = "unknown"
|
||||||
MevAgentBlockRazor = "blockrazor"
|
MevAgentBlockRazor = "blockrazor"
|
||||||
MevAgentSoyas = "soyas"
|
MevAgentFast = "fast"
|
||||||
MevAgentStellium = "stellium"
|
MevAgentSoyas = "soyas"
|
||||||
MevAgentAstralane = "astralane"
|
MevAgentStellium = "stellium"
|
||||||
|
MevAgentAstralane = "astralane"
|
||||||
|
MevagentFa1con = "fa1con"
|
||||||
|
MevagentBlocksprint = "blocksprint"
|
||||||
|
MevAgentMoon = "moon"
|
||||||
|
MevAgentSpeedlanding = "speedlanding"
|
||||||
|
MevAgentAllenhark = "allenhark"
|
||||||
|
MevAgentRaiden = "raiden"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -45,14 +52,16 @@ const (
|
|||||||
EntryContractFluxbeamDEX = "fluxbeamDEX"
|
EntryContractFluxbeamDEX = "fluxbeamDEX"
|
||||||
EntryContractNovaBotsProgram = "novaBotsProgram"
|
EntryContractNovaBotsProgram = "novaBotsProgram"
|
||||||
EntryContractTaggedSearcher = "taggedSearcher"
|
EntryContractTaggedSearcher = "taggedSearcher"
|
||||||
|
EntryContractPadre = "padre"
|
||||||
EntryContractDFlow = "dflow"
|
EntryContractDFlow = "dflow"
|
||||||
EntryContractMaestroBot = "maestroBot"
|
EntryContractMaestroBot = "maestroBot"
|
||||||
EntryContractBonkBot = "bonkBot"
|
EntryContractBonkBot = "bonkBot"
|
||||||
EntryContractBinanceWallet = "binanceWallet"
|
EntryContractBinanceWallet = "binanceWallet"
|
||||||
EntryContractMayhem = "pumpMayhem"
|
EntryContractMayhem = "pumpMayhem"
|
||||||
EntryContractTerm = "term"
|
EntryContractTerm = "term"
|
||||||
EntryContractUnknown = "unknown"
|
|
||||||
EntryContractTradewiz = "tradewiz"
|
EntryContractTradewiz = "tradewiz"
|
||||||
|
EntryContractDbot = "dbot"
|
||||||
|
EntryContractUnknown = "unknown"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -72,6 +81,7 @@ const (
|
|||||||
PlatformMaestro = "maestro"
|
PlatformMaestro = "maestro"
|
||||||
PlatformBonkBot = "bonkbot"
|
PlatformBonkBot = "bonkbot"
|
||||||
PlatformPadre = "padre"
|
PlatformPadre = "padre"
|
||||||
|
PlatformDexScreener = "dexscreener"
|
||||||
|
|
||||||
// used to flag transactions impersonating platform users
|
// used to flag transactions impersonating platform users
|
||||||
PlatformFake = "fake"
|
PlatformFake = "fake"
|
||||||
@@ -109,9 +119,24 @@ func GetConditionByProgram(program string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TxEventAddLP = "add"
|
TxEventAddLP = "add"
|
||||||
TxEventRemoveLP = "remove"
|
TxEventRemoveLP = "remove"
|
||||||
TxEventBuy = "buy"
|
TxEventBuy = "buy"
|
||||||
TxEventSell = "sell"
|
TxEventSell = "sell"
|
||||||
TxEventBurn = "burn"
|
TxEventBuyFailed = "buy_failed"
|
||||||
|
TxEventSellFailed = "sell_failed"
|
||||||
|
TxEventBurn = "burn"
|
||||||
|
TxEventCreate = "create"
|
||||||
|
TxEventComplete = "complete"
|
||||||
|
TxEventMigrate = "migrate"
|
||||||
|
TxEventDeposit = "deposit"
|
||||||
|
TxEventWithdraw = "withdraw"
|
||||||
|
TxEventOpen = "open"
|
||||||
|
TxEventClose = "close"
|
||||||
|
TxEventClaimFee = "claim_fee"
|
||||||
|
|
||||||
|
TxEventAddLiquidity = "add_liquidity"
|
||||||
|
TxEventAddLiquidityOneSide = "add_liquidity_one_side"
|
||||||
|
TxEventRemoveLiquidity = "remove_liquidity"
|
||||||
|
TxEventRemoveLiquidityOneSide = "remove_liquidity_one_side"
|
||||||
)
|
)
|
||||||
|
|||||||
154
error.go
154
error.go
@@ -79,54 +79,54 @@ const (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
GenericError InstructionErrorVariant = iota
|
GenericError InstructionErrorVariant = iota
|
||||||
/// The arguments provided to a program were invalid
|
// InvalidArgument / The arguments provided to a program were invalid
|
||||||
InvalidArgument
|
InvalidArgument
|
||||||
/// An instruction's data contents were invalid
|
// InvalidInstructionData / An instruction's data contents were invalid
|
||||||
InvalidInstructionData
|
InvalidInstructionData
|
||||||
/// An account's data contents was invalid
|
// InvalidAccountData / An account's data contents was invalid
|
||||||
InvalidAccountData
|
InvalidAccountData
|
||||||
/// An account's data was too small
|
// AccountDataTooSmall / An account's data was too small
|
||||||
AccountDataTooSmall
|
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
|
InsufficientFunds
|
||||||
/// The account did not have the expected program id
|
// IncorrectProgramId / The account did not have the expected program id
|
||||||
IncorrectProgramId
|
IncorrectProgramId
|
||||||
/// A signature was required but not found
|
// MissingRequiredSignature / A signature was required but not found
|
||||||
MissingRequiredSignature
|
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
|
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
|
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
|
UnbalancedInstruction
|
||||||
/// Program illegally modified an account's program id
|
// ModifiedProgramId / Program illegally modified an account's program id
|
||||||
ModifiedProgramId
|
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
|
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
|
ExternalAccountDataModified
|
||||||
/// Read-only account's lamports modified
|
// ReadonlyLamportChange / Read-only account's lamports modified
|
||||||
ReadonlyLamportChange
|
ReadonlyLamportChange
|
||||||
/// Read-only account's data was modified
|
// ReadonlyDataModified / Read-only account's data was modified
|
||||||
ReadonlyDataModified
|
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
|
// Deprecated, instructions can now contain duplicate accounts
|
||||||
DuplicateAccountIndex
|
DuplicateAccountIndex
|
||||||
/// Executable bit on account changed, but shouldn't have
|
// ExecutableModified / Executable bit on account changed, but shouldn't have
|
||||||
ExecutableModified
|
ExecutableModified
|
||||||
/// Rent_epoch account changed, but shouldn't have
|
// RentEpochModified / Rent_epoch account changed, but shouldn't have
|
||||||
RentEpochModified
|
RentEpochModified
|
||||||
/// The instruction expected additional account keys
|
// NotEnoughAccountKeys / The instruction expected additional account keys
|
||||||
NotEnoughAccountKeys
|
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
|
AccountDataSizeChanged
|
||||||
/// The instruction expected an executable account
|
// AccountNotExecutable / The instruction expected an executable account
|
||||||
AccountNotExecutable
|
AccountNotExecutable
|
||||||
/// Failed to borrow a reference to account data, already borrowed
|
// AccountBorrowFailed / Failed to borrow a reference to account data, already borrowed
|
||||||
AccountBorrowFailed
|
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
|
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
|
/// 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
|
/// the runtime cannot determine which changes to pick or how to merge them if both are modified
|
||||||
DuplicateAccountOutOfSync
|
DuplicateAccountOutOfSync
|
||||||
@@ -136,42 +136,42 @@ const (
|
|||||||
|
|
||||||
Custom // Custom(u32),
|
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.
|
/// error value or a user-defined error in the lower 32 bits.
|
||||||
InvalidError
|
InvalidError
|
||||||
/// Executable account's data was modified
|
// ExecutableDataModified / Executable account's data was modified
|
||||||
ExecutableDataModified
|
ExecutableDataModified
|
||||||
/// Executable account's lamports modified
|
// ExecutableLamportChange / Executable account's lamports modified
|
||||||
ExecutableLamportChange
|
ExecutableLamportChange
|
||||||
/// Executable accounts must be rent exempt
|
// ExecutableAccountNotRentExempt / Executable accounts must be rent exempt
|
||||||
ExecutableAccountNotRentExempt
|
ExecutableAccountNotRentExempt
|
||||||
/// Unsupported program id
|
// UnsupportedProgramId / Unsupported program id
|
||||||
UnsupportedProgramId
|
UnsupportedProgramId
|
||||||
/// Cross-program invocation call depth too deep
|
// CallDepth / Cross-program invocation call depth too deep
|
||||||
CallDepth
|
CallDepth
|
||||||
/// An account required by the instruction is missing
|
// MissingAccount / An account required by the instruction is missing
|
||||||
MissingAccount
|
MissingAccount
|
||||||
/// Cross-program invocation reentrancy not allowed for this instruction
|
// ReentrancyNotAllowed / Cross-program invocation reentrancy not allowed for this instruction
|
||||||
ReentrancyNotAllowed
|
ReentrancyNotAllowed
|
||||||
/// Length of the seed is too long for address generation
|
// MaxSeedLengthExceeded / Length of the seed is too long for address generation
|
||||||
MaxSeedLengthExceeded
|
MaxSeedLengthExceeded
|
||||||
/// Provided seeds do not result in a valid address
|
// InvalidSeeds / Provided seeds do not result in a valid address
|
||||||
InvalidSeeds
|
InvalidSeeds
|
||||||
/// Failed to reallocate account data of this length
|
// InvalidRealloc / Failed to reallocate account data of this length
|
||||||
InvalidRealloc
|
InvalidRealloc
|
||||||
/// Computational budget exceeded
|
// ComputationalBudgetExceeded / Computational budget exceeded
|
||||||
ComputationalBudgetExceeded
|
ComputationalBudgetExceeded
|
||||||
/// Cross-program invocation with unauthorized signer or writable account
|
// PrivilegeEscalation / Cross-program invocation with unauthorized signer or writable account
|
||||||
PrivilegeEscalation
|
PrivilegeEscalation
|
||||||
/// Failed to create program execution environment
|
// ProgramEnvironmentSetupFailure / Failed to create program execution environment
|
||||||
ProgramEnvironmentSetupFailure
|
ProgramEnvironmentSetupFailure
|
||||||
/// Program failed to complete
|
// ProgramFailedToComplete / Program failed to complete
|
||||||
ProgramFailedToComplete
|
ProgramFailedToComplete
|
||||||
/// Program failed to compile
|
// ProgramFailedToCompile / Program failed to compile
|
||||||
ProgramFailedToCompile
|
ProgramFailedToCompile
|
||||||
/// Account is immutable
|
// Immutable / Account is immutable
|
||||||
Immutable
|
Immutable
|
||||||
/// Incorrect authority provided
|
// IncorrectAuthority / Incorrect authority provided
|
||||||
IncorrectAuthority
|
IncorrectAuthority
|
||||||
/// Failed to serialize or deserialize account data
|
/// Failed to serialize or deserialize account data
|
||||||
///
|
///
|
||||||
@@ -185,23 +185,23 @@ const (
|
|||||||
|
|
||||||
BorshIoError // BorshIoError(String)
|
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
|
AccountNotRentExempt
|
||||||
/// Invalid account owner
|
// InvalidAccountOwner Invalid account owner
|
||||||
InvalidAccountOwner
|
InvalidAccountOwner
|
||||||
/// Program arithmetic overflowed
|
// ArithmeticOverflow Program arithmetic overflowed
|
||||||
ArithmeticOverflow
|
ArithmeticOverflow
|
||||||
/// Unsupported sysvar
|
// UnsupportedSysvar Unsupported sysvar
|
||||||
UnsupportedSysvar
|
UnsupportedSysvar
|
||||||
/// Illegal account owner
|
// IllegalOwner Illegal account owner
|
||||||
IllegalOwner
|
IllegalOwner
|
||||||
/// Accounts data allocations exceeded the maximum allowed per transaction
|
// MaxAccountsDataAllocationsExceeded / Accounts data allocations exceeded the maximum allowed per transaction
|
||||||
MaxAccountsDataAllocationsExceeded
|
MaxAccountsDataAllocationsExceeded
|
||||||
/// Max accounts exceeded
|
// MaxAccountsExceeded Max accounts exceeded
|
||||||
MaxAccountsExceeded
|
MaxAccountsExceeded
|
||||||
/// Max instruction trace length exceeded
|
// MaxInstructionTraceLengthExceeded Max instruction trace length exceeded
|
||||||
MaxInstructionTraceLengthExceeded
|
MaxInstructionTraceLengthExceeded
|
||||||
/// Builtin programs must consume compute units
|
// BuiltinProgramsMustConsumeComputeUnits Builtin programs must consume compute units
|
||||||
BuiltinProgramsMustConsumeComputeUnits
|
BuiltinProgramsMustConsumeComputeUnits
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -210,6 +210,15 @@ type TransactionError struct {
|
|||||||
rest []byte
|
rest []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TransactionParsedError struct {
|
||||||
|
Index uint8
|
||||||
|
Variant TransactionErrorVariant
|
||||||
|
Enum InstructionErrorVariant
|
||||||
|
CustomCode uint32
|
||||||
|
|
||||||
|
UnKnown string
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrInvalidTransactionError = errors.New("invalid transaction error")
|
ErrInvalidTransactionError = errors.New("invalid transaction error")
|
||||||
NotAnInstructionError = errors.New("not an instruction error")
|
NotAnInstructionError = errors.New("not an instruction error")
|
||||||
@@ -233,6 +242,49 @@ func DecodeTransactionError(data []byte) (*TransactionError, error) {
|
|||||||
return &err, nil
|
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) {
|
func (e *TransactionError) GetCustomErrorCode() (uint8, uint32, error) {
|
||||||
instr, err := e.GetInstructionError()
|
instr, err := e.GetInstructionError()
|
||||||
if err != nil {
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gagliardetto/solana-go"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
parser "github.com/thloyi/pump-parser"
|
parser "github.com/thloyi/pump-parser"
|
||||||
example "github.com/thloyi/pump-parser/internal/example"
|
example "github.com/thloyi/pump-parser/internal/example"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pumpProgram = solana.MustPublicKeyFromBase58("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||||
|
pumpAmmProgram = solana.MustPublicKeyFromBase58("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
//pool, err := ants.NewPool(100, ants.WithPreAlloc(true), ants.WithNonblocking(true))
|
//pool, err := ants.NewPool(100, ants.WithPreAlloc(true), ants.WithNonblocking(true))
|
||||||
//if err != nil {
|
//if err != nil {
|
||||||
@@ -18,7 +25,7 @@ func main() {
|
|||||||
// laserstream-mainnet-slc.helius-rpc.com:80
|
// laserstream-mainnet-slc.helius-rpc.com:80
|
||||||
|
|
||||||
ch := make(chan example.SubscriptionMessage, 1)
|
ch := make(chan example.SubscriptionMessage, 1)
|
||||||
go example.RunLoopWithReConnect(context.Background(), "127.0.0.1:10001", parser.SolProgramPump, ch)
|
go example.RunLoopWithReConnect(context.Background(), "", "", parser.SolProgramPump, ch)
|
||||||
// var tokenTxs = make(map[string]*types.Tx)
|
// var tokenTxs = make(map[string]*types.Tx)
|
||||||
// currentBlock := uint64(0)
|
// currentBlock := uint64(0)
|
||||||
for msg := range ch {
|
for msg := range ch {
|
||||||
@@ -31,6 +38,7 @@ func main() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ptx := msg.Tx
|
ptx := msg.Tx
|
||||||
|
// fmt.Println("consume", ptx.ComputeUnitsConsumed, "limit", ptx.CuLimit, "hash", ptx.GetTxHash())
|
||||||
//data, _ := json.Marshal(tx)
|
//data, _ := json.Marshal(tx)
|
||||||
//fmt.Println(string(data))
|
//fmt.Println(string(data))
|
||||||
//continue
|
//continue
|
||||||
@@ -43,42 +51,23 @@ func main() {
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
// 处理交易
|
// 处理交易
|
||||||
txErr, ok := ptx.Err.(*parser.TransactionError)
|
if len(ptx.Swaps) > 0 {
|
||||||
var customerErrCode uint32
|
for _, swap := range ptx.Swaps {
|
||||||
var instructorErrIndex uint8
|
if swap.SlippageBps.LessThan(decimal.Zero) || swap.SlippageBps.GreaterThan(decimal.NewFromInt(10000)) {
|
||||||
if ok {
|
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(),
|
||||||
instructorErrIndex, customerErrCode, _ = txErr.GetCustomErrorCode()
|
swap.BaseAmount.Div(decimal.NewFromInt(1e6)), swap.QuoteAmount.Div(decimal.NewFromInt(1e9)))
|
||||||
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.SolProgramPumpAMM {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if tx.EntryContract == "" || tx.EntryContract == parser.SolProgramPumpAMM || tx.EntryContract == parser.EntryContractOKXDexRouterV2 || tx.EntryContract == "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" {
|
if swap.SlippageBps.Equal(decimal.Zero) && (swap.Event == "buy" || swap.Event == "sell") {
|
||||||
continue
|
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())
|
||||||
}
|
}
|
||||||
//if tx.Token1Amount.GreaterThanOrEqual(decimal.NewFromFloat(0.1)) || tx.Event != "buy" {
|
|
||||||
// continue
|
|
||||||
//}
|
|
||||||
// printed = true
|
|
||||||
fmt.Printf("t: %s, block: %d, hash: %s, maker: %s, program: %s, event: %s, token0: %s, entryContract: %s, token balance: %s, EntryContract: %s\n",
|
|
||||||
time.Now().Format(time.RFC3339Nano),
|
|
||||||
tx.Block, tx.GetTxHash(), tx.Maker, tx.Program, tx.Event, tx.Token0Amount, tx.EntryContract, tx.AfterSignerToken0Balance, tx.EntryContract)
|
|
||||||
//break
|
|
||||||
}
|
}
|
||||||
//if !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
|
// currentBlock = ptx.Block
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package parser
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
types "github.com/thloyi/pump-parser"
|
types "github.com/thloyi/pump-parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,24 +22,14 @@ func NewPumpHandler(cb func(*types.Tx)) *PumpHandler {
|
|||||||
func (h *PumpHandler) HandleMessage(rawTx *types.RawTx) {
|
func (h *PumpHandler) HandleMessage(rawTx *types.RawTx) {
|
||||||
if rawTx.Meta.Err != nil {
|
if rawTx.Meta.Err != nil {
|
||||||
// Notify the channel about the failed transaction
|
// Notify the channel about the failed transaction
|
||||||
beforeSolBalance := decimal.Zero
|
var parsedTx = &types.Tx{}
|
||||||
afterSolBalance := decimal.Zero
|
parsedTx.SetRawTx(rawTx)
|
||||||
if rawTx.Meta.PreBalances != nil && len(rawTx.Meta.PreBalances) > 0 {
|
err := parsedTx.Parser()
|
||||||
beforeSolBalance = decimal.NewFromUint64(rawTx.Meta.PreBalances[0]).Div(decimal.NewFromInt(1e9))
|
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 {
|
h.callback(parsedTx)
|
||||||
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,
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ type Tx struct {
|
|||||||
AfterSOLBalance decimal.Decimal `gorm:"column:after_sol_balance;type:numeric" json:"after_sol_balance"`
|
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"`
|
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 {
|
func (tx *Tx) GetTxHash() string {
|
||||||
@@ -121,6 +122,7 @@ func FromTx(tx *parser.Tx) []*Tx {
|
|||||||
|
|
||||||
EntryContract: s.CheckEntryContract(),
|
EntryContract: s.CheckEntryContract(),
|
||||||
Mayhem: s.Mayhem,
|
Mayhem: s.Mayhem,
|
||||||
|
Cashback: s.Cashback,
|
||||||
}
|
}
|
||||||
} else if s.Program == "PumpAMM" {
|
} else if s.Program == "PumpAMM" {
|
||||||
if s.BaseMint.Equals(solana.WrappedSol) {
|
if s.BaseMint.Equals(solana.WrappedSol) {
|
||||||
@@ -175,6 +177,7 @@ func FromTx(tx *parser.Tx) []*Tx {
|
|||||||
|
|
||||||
EntryContract: s.CheckEntryContract(),
|
EntryContract: s.CheckEntryContract(),
|
||||||
Mayhem: s.Mayhem,
|
Mayhem: s.Mayhem,
|
||||||
|
Cashback: s.Cashback,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newTx = &Tx{
|
newTx = &Tx{
|
||||||
@@ -219,6 +222,7 @@ func FromTx(tx *parser.Tx) []*Tx {
|
|||||||
|
|
||||||
EntryContract: s.CheckEntryContract(),
|
EntryContract: s.CheckEntryContract(),
|
||||||
Mayhem: s.Mayhem,
|
Mayhem: s.Mayhem,
|
||||||
|
Cashback: s.Cashback,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,26 +45,25 @@ type Client struct {
|
|||||||
firstMessage bool
|
firstMessage bool
|
||||||
|
|
||||||
handler Handler
|
handler Handler
|
||||||
|
|
||||||
|
xToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client {
|
func NewClientWithPumpSwap(endpoint string, xtoken string, ch chan SubscriptionMessage) *Client {
|
||||||
var subscription pb.SubscribeRequest
|
var subscription pb.SubscribeRequest
|
||||||
|
|
||||||
var failed = false
|
//var failed = true
|
||||||
var vote = false
|
var vote = false
|
||||||
subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions)
|
subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions)
|
||||||
subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{
|
subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{
|
||||||
Failed: &failed,
|
//Failed: &failed,
|
||||||
Vote: &vote,
|
Vote: &vote,
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.Transactions["transactions_sub"].AccountInclude = []string{
|
//subscription.Transactions["transactions_sub"].AccountInclude = []string{
|
||||||
"pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
|
// "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
|
||||||
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
|
// "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
|
||||||
}
|
//}
|
||||||
subscription.Transactions["transactions_sub"].AccountRequired = []string{
|
|
||||||
"ARu4n5mFdZogZAravu7CcizaojWnS6oqka37gdLT5SZn",
|
|
||||||
}
|
|
||||||
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
|
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
|
||||||
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
|
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
|
||||||
|
|
||||||
@@ -75,6 +74,7 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client
|
|||||||
lastReceiveTime: time.Now(),
|
lastReceiveTime: time.Now(),
|
||||||
subStatus: false,
|
subStatus: false,
|
||||||
subscription: &subscription,
|
subscription: &subscription,
|
||||||
|
xToken: xtoken,
|
||||||
}
|
}
|
||||||
c.handler = NewPumpHandler(func(tx *types.Tx) {
|
c.handler = NewPumpHandler(func(tx *types.Tx) {
|
||||||
c.sendTx(tx)
|
c.sendTx(tx)
|
||||||
@@ -85,12 +85,12 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client
|
|||||||
func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Client {
|
func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Client {
|
||||||
var subscription pb.SubscribeRequest
|
var subscription pb.SubscribeRequest
|
||||||
|
|
||||||
var failed = false
|
//var failed = false
|
||||||
var vote = false
|
var vote = false
|
||||||
subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions)
|
subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions)
|
||||||
subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{
|
subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{
|
||||||
Failed: &failed,
|
//Failed: &failed,
|
||||||
Vote: &vote,
|
Vote: &vote,
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.Transactions["transactions_sub"].AccountInclude = []string{
|
subscription.Transactions["transactions_sub"].AccountInclude = []string{
|
||||||
@@ -115,12 +115,12 @@ func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Clien
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunLoopWithReConnect(ctx context.Context, endpoint, program string, ch chan SubscriptionMessage) {
|
func RunLoopWithReConnect(ctx context.Context, endpoint, token, program string, ch chan SubscriptionMessage) {
|
||||||
var client *Client
|
var client *Client
|
||||||
if program == types.SolProgramRaydiumLaunchLab {
|
if program == types.SolProgramRaydiumLaunchLab {
|
||||||
client = NewClientWithLaunchLab(endpoint, ch)
|
client = NewClientWithLaunchLab(endpoint, ch)
|
||||||
} else {
|
} else {
|
||||||
client = NewClientWithPumpSwap(endpoint, ch)
|
client = NewClientWithPumpSwap(endpoint, token, ch)
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -209,12 +209,13 @@ func (c *Client) grpcSubscribe(ctx context.Context, conn *grpc.ClientConn) error
|
|||||||
log.Printf("Subscription request: %s", string(subscriptionJson))
|
log.Printf("Subscription request: %s", string(subscriptionJson))
|
||||||
|
|
||||||
// Set up the subscription request
|
// Set up the subscription request
|
||||||
//if *token != "" {
|
if c.xToken != "" {
|
||||||
// md := metadata.New(map[string]string{"x-token": *token})
|
fmt.Println("xtoken", c.xToken)
|
||||||
// ctx = metadata.NewOutgoingContext(ctx, md)
|
md := metadata.New(map[string]string{"x-token": c.xToken})
|
||||||
//}
|
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||||
md := metadata.New(map[string]string{"x-token": "5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"})
|
}
|
||||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
//md := metadata.New(map[string]string{"x-token": "5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"})
|
||||||
|
//ctx = metadata.NewOutgoingContext(ctx, md)
|
||||||
|
|
||||||
stream, err := client.Subscribe(ctx)
|
stream, err := client.Subscribe(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ func (spg SwapGetter) GetLiquidityTx(tx *solana_parser.Tx, index uint64) (*Tx, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
mevName, mevFee := tx.CheckMevAgent()
|
mevName, mevFee := tx.CheckMevAgent()
|
||||||
platformName, platformFee := tx.CheckPlatformOnSig(spg.Swap)
|
platformName, platformFee := tx.CheckPlatform(spg.Swap)
|
||||||
|
|
||||||
pairString := ""
|
pairString := ""
|
||||||
if spg.Program == solana_parser.SolProgramPump {
|
if spg.Program == solana_parser.SolProgramPump {
|
||||||
@@ -501,7 +501,7 @@ func (spg SwapGetter) GetTx(tx *solana_parser.Tx, index uint64, price decimal.De
|
|||||||
}
|
}
|
||||||
|
|
||||||
mevName, mevFee := tx.CheckMevAgent()
|
mevName, mevFee := tx.CheckMevAgent()
|
||||||
platformName, platformFee := tx.CheckPlatformOnSig(spg.Swap)
|
platformName, platformFee := tx.CheckPlatform(spg.Swap)
|
||||||
|
|
||||||
if mevName == "" {
|
if mevName == "" {
|
||||||
mevName = "none"
|
mevName = "none"
|
||||||
|
|||||||
@@ -2,30 +2,19 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"log/slog"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gagliardetto/solana-go"
|
"github.com/gagliardetto/solana-go"
|
||||||
"github.com/gagliardetto/solana-go/rpc"
|
"github.com/gagliardetto/solana-go/rpc"
|
||||||
"github.com/jackc/pgtype"
|
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
solana_parser "github.com/thloyi/pump-parser"
|
solana_parser "github.com/thloyi/pump-parser"
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ()
|
var ()
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
var slot uint64 = 399015152
|
var slot uint64 = 414696178
|
||||||
var data = NewBlockData(decimal.NewFromFloat(100.0))
|
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
|
||||||
client := rpc.New("https://staked.helius-rpc.com?api-key=")
|
|
||||||
var rewards = false
|
var rewards = false
|
||||||
var version uint64 = 0
|
var version uint64 = 0
|
||||||
blocks, err := client.GetBlockWithOpts(context.Background(), slot, &rpc.GetBlockOpts{
|
blocks, err := client.GetBlockWithOpts(context.Background(), slot, &rpc.GetBlockOpts{
|
||||||
@@ -42,7 +31,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
solana_parser.EnableAllParsers()
|
solana_parser.EnableAllParsers()
|
||||||
|
|
||||||
var txs []*solana_parser.Tx
|
var txs []solana_parser.Tx
|
||||||
for i, tx := range blocks.Transactions {
|
for i, tx := range blocks.Transactions {
|
||||||
var blockTime uint64
|
var blockTime uint64
|
||||||
if blocks.BlockTime != nil {
|
if blocks.BlockTime != nil {
|
||||||
@@ -53,776 +42,19 @@ func main() {
|
|||||||
fmt.Println("from rpc tx error:", i, err)
|
fmt.Println("from rpc tx error:", i, err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if rawTx.Meta.Err != nil {
|
//if rawTx.Meta.Err != nil {
|
||||||
continue
|
// continue
|
||||||
}
|
//}
|
||||||
parsedTx, err := solana_parser.ParseRawTx(rawTx)
|
parsedTx, err := solana_parser.ParseRawTx(rawTx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("parse tx error:", i, rawTx.TxHash(), err)
|
fmt.Println("parse tx error:", i, rawTx.TxHash(), err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
txs = append(txs, parsedTx)
|
txs = append(txs, *parsedTx)
|
||||||
}
|
}
|
||||||
for _, result := range txs {
|
_, err = solana_parser.EncodeTxsBinary(txs)
|
||||||
swapsLen := len(result.Swaps)
|
|
||||||
for i := 0; i < swapsLen; i++ {
|
|
||||||
action := result.Swaps[i]
|
|
||||||
var actions []solana_parser.Swap = make([]solana_parser.Swap, 0, 2)
|
|
||||||
actions = append(actions, action)
|
|
||||||
if i+1 < swapsLen {
|
|
||||||
nextAction := result.Swaps[i+1]
|
|
||||||
if action.Event == "buy" && nextAction.Event == "complete" &&
|
|
||||||
action.Program == solana_parser.SolProgramPump &&
|
|
||||||
nextAction.Program == solana_parser.SolProgramPump &&
|
|
||||||
action.BaseMint == nextAction.BaseMint {
|
|
||||||
actions = append(actions, nextAction)
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if action.Event == "migrate" && nextAction.Event == "create" &&
|
|
||||||
action.Program == solana_parser.SolProgramPump &&
|
|
||||||
nextAction.Program == solana_parser.SolProgramPumpAMM &&
|
|
||||||
action.BaseMint == nextAction.BaseMint {
|
|
||||||
actions = append(actions, nextAction)
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err = HandleAction(context.Background(), result, actions, data); err != nil {
|
|
||||||
//h.logger.Errorf("handle action error: %s - %v", result.RawTx.Transaction.Signatures[0].String(), err)
|
|
||||||
fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if result.GetTxHash() == "4h3yrAfMfHYHgf2DBnaecRjuSw4UTirySej65PSapPPPASvBADo143NhptQyVQdiCKypoSs2tzh3EhYxcgxVNLHD" {
|
|
||||||
fmt.Println("xxx")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Println("slot", slot, "tx count: ", len(data.Txs))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
meteoraDammV2Program = solana.MustPublicKeyFromBase58("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG")
|
|
||||||
raydiumCPmmProgramID = solana.MustPublicKeyFromBase58("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C")
|
|
||||||
)
|
|
||||||
|
|
||||||
func HandleAction(ctx context.Context, tx *solana_parser.Tx, swaps []solana_parser.Swap, data *BlockData) error {
|
|
||||||
swapLen := len(swaps)
|
|
||||||
if len(swaps) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if swaps[0].BaseMint != solana_parser.WSOL && swaps[0].QuoteMint != solana_parser.WSOL && !swaps[0].QuoteMint.IsZero() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(swaps) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
event := swaps[0].Event
|
|
||||||
swap := swaps[0]
|
|
||||||
action := SwapGetter{swap}
|
|
||||||
switch event {
|
|
||||||
case "buy", "sell":
|
|
||||||
|
|
||||||
data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price))
|
|
||||||
if swap.Program == solana_parser.SolProgramPump {
|
|
||||||
if swapLen == 2 && swaps[1].Event == "complete" {
|
|
||||||
t := pgtype.Timestamptz{}
|
|
||||||
t.Set(time.Unix(tx.BlockAt, 0))
|
|
||||||
data.AppendAction(Action{
|
|
||||||
Maker: swaps[1].User.String(),
|
|
||||||
Token: swaps[1].BaseMint.String(),
|
|
||||||
Pair: swaps[1].Pool.String(),
|
|
||||||
Action: "pump-migrate",
|
|
||||||
Block: tx.Block,
|
|
||||||
BlockAt: t,
|
|
||||||
TxHash: tx.GetTxHash(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data.SetPair(action, tx.Block, "")
|
|
||||||
|
|
||||||
case "create":
|
|
||||||
pair, err := action.GetPair(tx.Block, "")
|
|
||||||
if err != nil {
|
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
fmt.Println("EncodeTxsBinary err", 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.CheckPlatformOnSig(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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
72
meta.go
72
meta.go
@@ -68,25 +68,50 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
meteoraInitializeLbPairDiscriminator = calculateDiscriminator("global:initialize_lb_pair2")
|
meteoraInitializeCustomizablePermissionlessLbPairDiscriminator = calculateDiscriminator("global:initialize_customizable_permissionless_lb_pair")
|
||||||
meteoraInitializeLbPairEventDiscriminator = calculateDiscriminator("event:LbPairCreate")
|
meteoraInitializeCustomizablePermissionlessLbPair2Discriminator = calculateDiscriminator("global:initialize_customizable_permissionless_lb_pair2")
|
||||||
meteoraDlmmSwapDiscriminator = calculateDiscriminator("global:swap")
|
meteoraInitializeLbPairDiscriminator = calculateDiscriminator("global:initialize_lb_pair")
|
||||||
meteoraDlmmSwap2Discriminator = calculateDiscriminator("global:swap2")
|
meteoraInitializeLbPair2Discriminator = calculateDiscriminator("global:initialize_lb_pair2")
|
||||||
meteoraDlmmSwapExactOutDiscriminator = calculateDiscriminator("global:swap_exact_out")
|
meteoraInitializePermissionLbPairDiscriminator = calculateDiscriminator("global:initialize_permission_lb_pair")
|
||||||
meteoraDlmmSwapExactOut2Discriminator = calculateDiscriminator("global:swap_exact_out2")
|
meteoraInitializeLbPairEventDiscriminator = calculateDiscriminator("event:LbPairCreate")
|
||||||
meteoraDlmmSwapWithPriceImpactDiscriminator = calculateDiscriminator("global:swap_with_price_impact")
|
meteoraDlmmSwapDiscriminator = calculateDiscriminator("global:swap")
|
||||||
meteoraDlmmSwapWithPriceImpact2Discriminator = calculateDiscriminator("global:swap_with_price_impact2")
|
meteoraDlmmSwap2Discriminator = calculateDiscriminator("global:swap2")
|
||||||
meteoraDlmmSwapEventDiscriminator = calculateDiscriminator("event:Swap")
|
meteoraDlmmSwapExactOutDiscriminator = calculateDiscriminator("global:swap_exact_out")
|
||||||
meteoraDlmmAddLiquidityDiscriminator = calculateDiscriminator("global:add_liquidity")
|
meteoraDlmmSwapExactOut2Discriminator = calculateDiscriminator("global:swap_exact_out2")
|
||||||
meteoraDlmmAddLiquidity2Discriminator = calculateDiscriminator("global:add_liquidity2")
|
meteoraDlmmSwapWithPriceImpactDiscriminator = calculateDiscriminator("global:swap_with_price_impact")
|
||||||
meteoraDlmmAddLiquidityByStrategyDiscriminator = calculateDiscriminator("global:add_liquidity_by_strategy")
|
meteoraDlmmSwapWithPriceImpact2Discriminator = calculateDiscriminator("global:swap_with_price_impact2")
|
||||||
meteoraDlmmAddLiquidityByStrategy2Discriminator = calculateDiscriminator("global:add_liquidity_by_strategy2")
|
meteoraDlmmInitializePositionDiscriminator = calculateDiscriminator("global:initialize_position")
|
||||||
meteoraDlmmRemoveLiquidityDiscriminator = calculateDiscriminator("global:remove_liquidity")
|
meteoraDlmmInitializePosition2Discriminator = calculateDiscriminator("global:initialize_position2")
|
||||||
meteoraDlmmRemoveLiquidity2Discriminator = calculateDiscriminator("global:remove_liquidity2")
|
meteoraDlmmInitializePositionByOperatorDiscriminator = calculateDiscriminator("global:initialize_position_by_operator")
|
||||||
meteoraDlmmRemoveLiquidityByRangeDiscriminator = calculateDiscriminator("global:remove_liquidity_by_range")
|
meteoraDlmmInitializePositionPdaDiscriminator = calculateDiscriminator("global:initialize_position_pda")
|
||||||
meteoraDlmmRemoveLiquidityByRange2Discriminator = calculateDiscriminator("global:remove_liquidity_by_range2")
|
meteoraDlmmClosePositionDiscriminator = calculateDiscriminator("global:close_position")
|
||||||
meteoraDlmmAddLiquidityEventDiscriminator = calculateDiscriminator("event:AddLiquidity")
|
meteoraDlmmClosePosition2Discriminator = calculateDiscriminator("global:close_position2")
|
||||||
meteoraDlmmRemoveLiquidityEventDiscriminator = calculateDiscriminator("event:RemoveLiquidity")
|
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 (
|
var (
|
||||||
@@ -200,8 +225,10 @@ var (
|
|||||||
const (
|
const (
|
||||||
raydiumV4InitializePoolDiscriminator = uint8(1)
|
raydiumV4InitializePoolDiscriminator = uint8(1)
|
||||||
|
|
||||||
raydiumV4SwapBaseInDiscriminator = uint8(9)
|
raydiumV4SwapBaseInDiscriminator = uint8(9)
|
||||||
raydiumV4SwapBaseOutDiscriminator = uint8(11)
|
raydiumV4SwapBaseOutDiscriminator = uint8(11)
|
||||||
|
raydiumV4SwapBaseInV2Discriminator = uint8(16)
|
||||||
|
raydiumV4SwapBaseOutV2Discriminator = uint8(17)
|
||||||
|
|
||||||
raydiumV4AddLiquidityDiscriminator = uint8(3)
|
raydiumV4AddLiquidityDiscriminator = uint8(3)
|
||||||
raydiumV4RemoveLiquidityDiscriminator = uint8(4)
|
raydiumV4RemoveLiquidityDiscriminator = uint8(4)
|
||||||
@@ -234,5 +261,8 @@ var transferDiscriminator = uint32(2)
|
|||||||
var createAccountWithSeedDiscriminator = uint32(3)
|
var createAccountWithSeedDiscriminator = uint32(3)
|
||||||
|
|
||||||
var systemProgram = solana.MustPublicKeyFromBase58("11111111111111111111111111111111")
|
var systemProgram = solana.MustPublicKeyFromBase58("11111111111111111111111111111111")
|
||||||
|
var momoProgram = solana.MustPublicKeyFromBase58("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")
|
||||||
|
|
||||||
|
var chainLinkProgram = solana.MustPublicKeyFromBase58("cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ")
|
||||||
|
|
||||||
var eventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29}
|
var eventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29}
|
||||||
|
|||||||
1529
metaoradlmm.go
1529
metaoradlmm.go
File diff suppressed because it is too large
Load Diff
424
metaoradlmm_test.go
Normal file
424
metaoradlmm_test.go
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,11 @@ type metaoraPoolInitializePoolData struct {
|
|||||||
TokenBAmount uint64 `json:"tokenBAmount"`
|
TokenBAmount uint64 `json:"tokenBAmount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type metaoraPoolSwapArgs struct {
|
||||||
|
InAmount uint64
|
||||||
|
MinimumOutAmount uint64
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
meteoraVaultProgram = solana.MustPublicKeyFromBase58("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi")
|
meteoraVaultProgram = solana.MustPublicKeyFromBase58("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi")
|
||||||
meteoraVaultDepositDiscriminator = []byte{0xf2, 0x23, 0xc6, 0x89, 0x52, 0xe1, 0xf2, 0xb6}
|
meteoraVaultDepositDiscriminator = []byte{0xf2, 0x23, 0xc6, 0x89, 0x52, 0xe1, 0xf2, 0xb6}
|
||||||
@@ -371,6 +376,7 @@ func metaoraPoolAddLiquidity(tx *Tx, instruction Instruction, innerInstructions
|
|||||||
if len(instruction.Accounts) < 14 {
|
if len(instruction.Accounts) < 14 {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
||||||
}
|
}
|
||||||
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
|
||||||
pool := tx.rawTx.accountList[instruction.Accounts[0]]
|
pool := tx.rawTx.accountList[instruction.Accounts[0]]
|
||||||
lpMint := tx.rawTx.accountList[instruction.Accounts[1]]
|
lpMint := tx.rawTx.accountList[instruction.Accounts[1]]
|
||||||
@@ -494,8 +500,6 @@ func metaoraPoolAddLiquidity(tx *Tx, instruction Instruction, innerInstructions
|
|||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to find deposit instructions")
|
return nil, increaseOffset(offset), fmt.Errorf("failed to find deposit instructions")
|
||||||
}
|
}
|
||||||
|
|
||||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
||||||
|
|
||||||
var event = "add_liquidity_one_side"
|
var event = "add_liquidity_one_side"
|
||||||
if baseFound && quoteFound {
|
if baseFound && quoteFound {
|
||||||
// both sides
|
// both sides
|
||||||
@@ -530,6 +534,7 @@ func metaoraPoolRemoveLiquidity(tx *Tx, instruction Instruction, innerInstructio
|
|||||||
if len(instruction.Accounts) < 14 {
|
if len(instruction.Accounts) < 14 {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
||||||
}
|
}
|
||||||
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
|
||||||
pool := tx.rawTx.accountList[instruction.Accounts[0]]
|
pool := tx.rawTx.accountList[instruction.Accounts[0]]
|
||||||
lpMint := tx.rawTx.accountList[instruction.Accounts[1]]
|
lpMint := tx.rawTx.accountList[instruction.Accounts[1]]
|
||||||
@@ -700,8 +705,6 @@ func metaoraPoolRemoveLiquidity(tx *Tx, instruction Instruction, innerInstructio
|
|||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to find withdraw instructions, baseFound: %v, quoteFound: %v", baseFound, quoteFound)
|
return nil, increaseOffset(offset), fmt.Errorf("failed to find withdraw instructions, baseFound: %v, quoteFound: %v", baseFound, quoteFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
||||||
|
|
||||||
var event = "remove_liquidity_one_side"
|
var event = "remove_liquidity_one_side"
|
||||||
if baseFound && quoteFound {
|
if baseFound && quoteFound {
|
||||||
event = "remove_liquidity"
|
event = "remove_liquidity"
|
||||||
@@ -728,6 +731,10 @@ func metaoraPoolRemoveLiquidity(tx *Tx, instruction Instruction, innerInstructio
|
|||||||
}
|
}
|
||||||
|
|
||||||
func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
|
var args metaoraPoolSwapArgs
|
||||||
|
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {
|
||||||
|
return nil, increaseOffset(offset), fmt.Errorf("failed to decode meteora pools swap args: %w", err)
|
||||||
|
}
|
||||||
pool := tx.rawTx.accountList[instruction.Accounts[0]]
|
pool := tx.rawTx.accountList[instruction.Accounts[0]]
|
||||||
payer := tx.rawTx.accountList[instruction.Accounts[12]]
|
payer := tx.rawTx.accountList[instruction.Accounts[12]]
|
||||||
|
|
||||||
@@ -848,6 +855,9 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !baseFound || !quoteFound {
|
if !baseFound || !quoteFound {
|
||||||
|
if args.InAmount == 0 {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to find meteora pool event in inner instructions")
|
return nil, increaseOffset(offset), fmt.Errorf("failed to find meteora pool event in inner instructions")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -876,5 +886,10 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
swaps[0].SetSwapAmountInfo(
|
||||||
|
SwapModeExactIn,
|
||||||
|
decimal.NewFromUint64(args.InAmount),
|
||||||
|
decimal.NewFromUint64(args.MinimumOutAmount),
|
||||||
|
)
|
||||||
return swaps, offset, nil
|
return swaps, offset, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -385,5 +385,12 @@ func metaBcSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerIn
|
|||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if swapEvent.Params != nil {
|
||||||
|
swaps[0].SetSwapAmountInfo(
|
||||||
|
SwapModeExactIn,
|
||||||
|
decimal.NewFromUint64(swapEvent.Params.AmountIn),
|
||||||
|
decimal.NewFromUint64(swapEvent.Params.MinimumAmountOut),
|
||||||
|
)
|
||||||
|
}
|
||||||
return swaps, offset, nil
|
return swaps, offset, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func meteoraDammV2InitializePoolParser(tx *Tx, instruction Instruction, innerIns
|
|||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||||
if err != nil {
|
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])
|
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 loadedEvent bool
|
||||||
var initializePoolEvent MetaoraDammInitializePoolEvent
|
var initializePoolEvent MetaoraDammInitializePoolEvent
|
||||||
@@ -119,6 +119,13 @@ func meteoraDammV2InitializePoolParser(tx *Tx, instruction Instruction, innerIns
|
|||||||
}
|
}
|
||||||
baseVaultAccountIndex := instruction.Accounts[10]
|
baseVaultAccountIndex := instruction.Accounts[10]
|
||||||
quoteVaultAccountIndex := instruction.Accounts[11]
|
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)
|
baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -181,6 +188,36 @@ type meteoraDammSwapEvent struct {
|
|||||||
ReserveBAmount 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) {
|
func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if len(instruction.Accounts) < 9 {
|
if len(instruction.Accounts) < 9 {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
|
||||||
@@ -269,28 +306,30 @@ func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerI
|
|||||||
return nil, offset, fmt.Errorf("invalid trade direction")
|
return nil, offset, fmt.Errorf("invalid trade direction")
|
||||||
}
|
}
|
||||||
|
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramMeteoraAmmV2,
|
||||||
Program: SolProgramMeteoraAmmV2,
|
Event: event,
|
||||||
Event: event,
|
Pool: swapEvent.Pool,
|
||||||
Pool: swapEvent.Pool,
|
BaseMint: baseMint,
|
||||||
BaseMint: baseMint,
|
QuoteMint: quoteMint,
|
||||||
QuoteMint: quoteMint,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
QuoteTokenProgram: quoteTokenProgram,
|
Creator: solana.PublicKey{},
|
||||||
Creator: solana.PublicKey{},
|
BaseMintDecimals: baseMintDecimals,
|
||||||
BaseMintDecimals: baseMintDecimals,
|
QuoteMintDecimals: quoteMintDecimals,
|
||||||
QuoteMintDecimals: quoteMintDecimals,
|
User: payer,
|
||||||
User: payer,
|
BaseAmount: baseAmount,
|
||||||
BaseAmount: baseAmount,
|
QuoteAmount: quoteAmount,
|
||||||
QuoteAmount: quoteAmount,
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
if swapMode, fixedAmount, limitAmount, ok := meteoraDammSwapAmountInfo(event, swapEvent.Params); ok {
|
||||||
}, offset, nil
|
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||||
|
}
|
||||||
|
return []Swap{swap}, offset, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
259
orcawhirpool.go
259
orcawhirpool.go
@@ -1,12 +1,33 @@
|
|||||||
package pump_parser
|
package pump_parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gagliardetto/solana-go"
|
"github.com/gagliardetto/solana-go"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func decodeOrcaWhirlpoolSwapArgs(data []byte) (amount uint64, otherAmountThreshold uint64, amountSpecifiedIsInput bool, err error) {
|
||||||
|
if len(data) < 42 {
|
||||||
|
return 0, 0, false, fmt.Errorf("orca whirlpool swap instruction data too short")
|
||||||
|
}
|
||||||
|
amount = binary.LittleEndian.Uint64(data[8:16])
|
||||||
|
otherAmountThreshold = binary.LittleEndian.Uint64(data[16:24])
|
||||||
|
amountSpecifiedIsInput = data[40] != 0
|
||||||
|
return amount, otherAmountThreshold, amountSpecifiedIsInput, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeOrcaWhirlpoolTwoHopSwapArgs(data []byte) (amount uint64, otherAmountThreshold uint64, amountSpecifiedIsInput bool, err error) {
|
||||||
|
if len(data) < 27 {
|
||||||
|
return 0, 0, false, fmt.Errorf("orca whirlpool two-hop swap instruction data too short")
|
||||||
|
}
|
||||||
|
amount = binary.LittleEndian.Uint64(data[8:16])
|
||||||
|
otherAmountThreshold = binary.LittleEndian.Uint64(data[16:24])
|
||||||
|
amountSpecifiedIsInput = data[24] != 0
|
||||||
|
return amount, otherAmountThreshold, amountSpecifiedIsInput, nil
|
||||||
|
}
|
||||||
|
|
||||||
func orcaWhirPoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func orcaWhirPoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(orcaProgramID) {
|
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(orcaProgramID) {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("orcawhirpoolprogram instruction not found, offset, %d, %d", offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("orcawhirpoolprogram instruction not found, offset, %d, %d", offset[0], offset[1])
|
||||||
@@ -242,10 +263,10 @@ func orcaWhirPoolLiquidityParser(tx *Tx, instruction Instruction, innerInstructi
|
|||||||
//return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
//return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
||||||
baseAmount = decimal.NewFromInt(int64(amount))
|
baseAmount = decimal.NewFromUint64(amount)
|
||||||
baseFound = true
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
||||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
quoteAmount = decimal.NewFromUint64(amount)
|
||||||
quoteFound = true
|
quoteFound = true
|
||||||
}
|
}
|
||||||
if baseFound && quoteFound {
|
if baseFound && quoteFound {
|
||||||
@@ -260,7 +281,7 @@ func orcaWhirPoolLiquidityParser(tx *Tx, instruction Instruction, innerInstructi
|
|||||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
}
|
}
|
||||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
||||||
instructionName += "_on_side"
|
instructionName += "_one_side"
|
||||||
}
|
}
|
||||||
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
||||||
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
||||||
@@ -349,10 +370,10 @@ func orcaWhirPoolLiquidityV2Parser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
||||||
baseAmount = decimal.NewFromInt(int64(amount))
|
baseAmount = decimal.NewFromUint64(amount)
|
||||||
baseFound = true
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
||||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
quoteAmount = decimal.NewFromUint64(amount)
|
||||||
quoteFound = true
|
quoteFound = true
|
||||||
}
|
}
|
||||||
if baseFound && quoteFound {
|
if baseFound && quoteFound {
|
||||||
@@ -367,7 +388,7 @@ func orcaWhirPoolLiquidityV2Parser(tx *Tx, instruction Instruction, innerInstruc
|
|||||||
return nil, offset, InstructionIgnoredError
|
return nil, offset, InstructionIgnoredError
|
||||||
}
|
}
|
||||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
||||||
instructionName += "_on_side"
|
instructionName += "_one_side"
|
||||||
}
|
}
|
||||||
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
||||||
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
||||||
@@ -454,10 +475,10 @@ func orcaWhirPoolCollectFeeParser(tx *Tx, instruction Instruction, innerInstruct
|
|||||||
//return nil, increaseOffset(offset), fmt.Errorf("orca whirpool parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
//return nil, increaseOffset(offset), fmt.Errorf("orca whirpool parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
||||||
baseAmount = decimal.NewFromInt(int64(amount))
|
baseAmount = decimal.NewFromUint64(amount)
|
||||||
baseFound = true
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
||||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
quoteAmount = decimal.NewFromUint64(amount)
|
||||||
quoteFound = true
|
quoteFound = true
|
||||||
}
|
}
|
||||||
if (baseFound && quoteFound) || i >= 6 {
|
if (baseFound && quoteFound) || i >= 6 {
|
||||||
@@ -472,7 +493,7 @@ func orcaWhirPoolCollectFeeParser(tx *Tx, instruction Instruction, innerInstruct
|
|||||||
return nil, offset, InstructionIgnoredError
|
return nil, offset, InstructionIgnoredError
|
||||||
}
|
}
|
||||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
||||||
instructionName += "_on_side"
|
instructionName += "_one_side"
|
||||||
}
|
}
|
||||||
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
||||||
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
||||||
@@ -556,10 +577,10 @@ func orcaWhirPoolCollectFeeV2Parser(tx *Tx, instruction Instruction, innerInstru
|
|||||||
//return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
//return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
||||||
baseAmount = decimal.NewFromInt(int64(amount))
|
baseAmount = decimal.NewFromUint64(amount)
|
||||||
baseFound = true
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
||||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
quoteAmount = decimal.NewFromUint64(amount)
|
||||||
quoteFound = true
|
quoteFound = true
|
||||||
}
|
}
|
||||||
if (baseFound && quoteFound) || i >= 6 {
|
if (baseFound && quoteFound) || i >= 6 {
|
||||||
@@ -574,7 +595,7 @@ func orcaWhirPoolCollectFeeV2Parser(tx *Tx, instruction Instruction, innerInstru
|
|||||||
return nil, offset, InstructionIgnoredError
|
return nil, offset, InstructionIgnoredError
|
||||||
}
|
}
|
||||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
||||||
instructionName += "_on_side"
|
instructionName += "_one_side"
|
||||||
}
|
}
|
||||||
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
||||||
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
||||||
@@ -658,10 +679,10 @@ func orcaWhirPoolCollectProtocolFeeV2Parser(tx *Tx, instruction Instruction, inn
|
|||||||
// return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
// return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
||||||
baseAmount = decimal.NewFromInt(int64(amount))
|
baseAmount = decimal.NewFromUint64(amount)
|
||||||
baseFound = true
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
||||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
quoteAmount = decimal.NewFromUint64(amount)
|
||||||
quoteFound = true
|
quoteFound = true
|
||||||
}
|
}
|
||||||
if (baseFound && quoteFound) || i >= 6 {
|
if (baseFound && quoteFound) || i >= 6 {
|
||||||
@@ -676,7 +697,7 @@ func orcaWhirPoolCollectProtocolFeeV2Parser(tx *Tx, instruction Instruction, inn
|
|||||||
return nil, offset, InstructionIgnoredError
|
return nil, offset, InstructionIgnoredError
|
||||||
}
|
}
|
||||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
||||||
instructionName += "_on_side"
|
instructionName += "_one_side"
|
||||||
}
|
}
|
||||||
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
||||||
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
||||||
@@ -709,6 +730,14 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
|
|||||||
}
|
}
|
||||||
|
|
||||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
amount, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolSwapArgs(instruction.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, increaseOffset(offset), err
|
||||||
|
}
|
||||||
|
swapMode := SwapModeExactOut
|
||||||
|
if amountSpecifiedIsInput {
|
||||||
|
swapMode = SwapModeExactIn
|
||||||
|
}
|
||||||
|
|
||||||
user := tx.rawTx.accountList[instruction.Accounts[1]]
|
user := tx.rawTx.accountList[instruction.Accounts[1]]
|
||||||
pool := tx.rawTx.accountList[instruction.Accounts[2]]
|
pool := tx.rawTx.accountList[instruction.Accounts[2]]
|
||||||
@@ -755,7 +784,7 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
|
|||||||
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
|
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
|
||||||
baseAmount = decimal.NewFromInt(int64(amount))
|
baseAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(vault0Account) && to.Equals(token0Account) {
|
if from.Equals(vault0Account) && to.Equals(token0Account) {
|
||||||
event = "buy"
|
event = "buy"
|
||||||
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
|
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
|
||||||
@@ -763,7 +792,7 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
|
|||||||
}
|
}
|
||||||
baseFound = true
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
|
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
|
||||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
quoteAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(vault1Account) && to.Equals(token1Account) {
|
if from.Equals(vault1Account) && to.Equals(token1Account) {
|
||||||
event = "sell"
|
event = "sell"
|
||||||
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
|
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
|
||||||
@@ -781,27 +810,28 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
|
|||||||
return nil, offset, fmt.Errorf("orca whirpool swap failed to find both base and quote token transfer in inner instructions")
|
return nil, offset, fmt.Errorf("orca whirpool swap failed to find both base and quote token transfer in inner instructions")
|
||||||
}
|
}
|
||||||
|
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramOrcaWhirPool,
|
||||||
Program: SolProgramOrcaWhirPool,
|
Event: event,
|
||||||
Event: event,
|
Pool: pool,
|
||||||
Pool: pool,
|
BaseMint: baseTokenBalance.MintAccount,
|
||||||
BaseMint: baseTokenBalance.MintAccount,
|
QuoteMint: quoteTokenBalance.MintAccount,
|
||||||
QuoteMint: quoteTokenBalance.MintAccount,
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
||||||
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
||||||
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
||||||
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
||||||
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
BaseAmount: baseAmount,
|
||||||
BaseAmount: baseAmount,
|
QuoteAmount: quoteAmount,
|
||||||
QuoteAmount: quoteAmount,
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
User: user,
|
||||||
User: user,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
|
||||||
}, offset, nil
|
|
||||||
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
@@ -810,6 +840,14 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
|
|||||||
}
|
}
|
||||||
|
|
||||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
amount, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolSwapArgs(instruction.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, increaseOffset(offset), err
|
||||||
|
}
|
||||||
|
swapMode := SwapModeExactOut
|
||||||
|
if amountSpecifiedIsInput {
|
||||||
|
swapMode = SwapModeExactIn
|
||||||
|
}
|
||||||
|
|
||||||
user := tx.rawTx.accountList[instruction.Accounts[3]]
|
user := tx.rawTx.accountList[instruction.Accounts[3]]
|
||||||
pool := tx.rawTx.accountList[instruction.Accounts[4]]
|
pool := tx.rawTx.accountList[instruction.Accounts[4]]
|
||||||
@@ -848,15 +886,15 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
|
|||||||
var baseFound, quoteFound bool
|
var baseFound, quoteFound bool
|
||||||
var skipOffset = 0
|
var skipOffset = 0
|
||||||
for i, inner := range inners {
|
for i, inner := range inners {
|
||||||
|
if !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.Token2022ProgramID) && !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.TokenProgramID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if i <= 1 { //maybe momo??
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swapv2 parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swapv2 parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
|
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
|
||||||
baseAmount = decimal.NewFromInt(int64(amount))
|
baseAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(vault0Account) && to.Equals(token0Account) {
|
if from.Equals(vault0Account) && to.Equals(token0Account) {
|
||||||
event = "buy"
|
event = "buy"
|
||||||
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
|
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
|
||||||
@@ -864,7 +902,7 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
|
|||||||
}
|
}
|
||||||
baseFound = true
|
baseFound = true
|
||||||
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
|
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
|
||||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
quoteAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(vault1Account) && to.Equals(token1Account) {
|
if from.Equals(vault1Account) && to.Equals(token1Account) {
|
||||||
event = "sell"
|
event = "sell"
|
||||||
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
|
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
|
||||||
@@ -883,27 +921,28 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
|
|||||||
}
|
}
|
||||||
offset[1] += uint(skipOffset + 1)
|
offset[1] += uint(skipOffset + 1)
|
||||||
|
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramOrcaWhirPool,
|
||||||
Program: SolProgramOrcaWhirPool,
|
Event: event,
|
||||||
Event: event,
|
Pool: pool,
|
||||||
Pool: pool,
|
BaseMint: baseTokenBalance.MintAccount,
|
||||||
BaseMint: baseTokenBalance.MintAccount,
|
QuoteMint: quoteTokenBalance.MintAccount,
|
||||||
QuoteMint: quoteTokenBalance.MintAccount,
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
||||||
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
||||||
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
||||||
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
||||||
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
BaseAmount: baseAmount,
|
||||||
BaseAmount: baseAmount,
|
QuoteAmount: quoteAmount,
|
||||||
QuoteAmount: quoteAmount,
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
User: user,
|
||||||
User: user,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
|
||||||
}, offset, nil
|
|
||||||
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
@@ -912,6 +951,14 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
|
|||||||
}
|
}
|
||||||
|
|
||||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
amountSpecified, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolTwoHopSwapArgs(instruction.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, increaseOffset(offset), err
|
||||||
|
}
|
||||||
|
swapMode := SwapModeExactOut
|
||||||
|
if amountSpecifiedIsInput {
|
||||||
|
swapMode = SwapModeExactIn
|
||||||
|
}
|
||||||
|
|
||||||
user := tx.rawTx.accountList[instruction.Accounts[1]]
|
user := tx.rawTx.accountList[instruction.Accounts[1]]
|
||||||
pool1 := tx.rawTx.accountList[instruction.Accounts[2]]
|
pool1 := tx.rawTx.accountList[instruction.Accounts[2]]
|
||||||
@@ -964,7 +1011,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
|
|||||||
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
||||||
baseAmount = decimal.NewFromInt(int64(amount))
|
baseAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
|
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
|
||||||
event = "buy"
|
event = "buy"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
||||||
@@ -972,7 +1019,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
|
|||||||
}
|
}
|
||||||
baseFound = true
|
baseFound = true
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
||||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
quoteAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool1UserQuote]) {
|
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool1UserQuote]) {
|
||||||
event = "sell"
|
event = "sell"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool1UserQuote]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
} else if from.Equals(tx.rawTx.accountList[pool1UserQuote]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
||||||
@@ -1010,11 +1057,11 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
|
|||||||
{
|
{
|
||||||
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultBase)
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultBase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token0 vault balance after tx: %v", err)
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool2 token0 vault balance after tx: %v", err)
|
||||||
}
|
}
|
||||||
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultQuote)
|
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultQuote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token1 vault balance after tx: %v", err)
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool2 token1 vault balance after tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
userBase := getAccountBalanceAfterTx(tx.rawTx, pool2UserBase)
|
userBase := getAccountBalanceAfterTx(tx.rawTx, pool2UserBase)
|
||||||
@@ -1040,7 +1087,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
|
|||||||
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
||||||
baseAmount = decimal.NewFromInt(int64(amount))
|
baseAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool2UserBase]) {
|
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool2UserBase]) {
|
||||||
event = "buy"
|
event = "buy"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool2UserBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
} else if from.Equals(tx.rawTx.accountList[pool2UserBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
||||||
@@ -1048,7 +1095,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
|
|||||||
}
|
}
|
||||||
baseFound = true
|
baseFound = true
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
||||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
quoteAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
|
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
|
||||||
event = "sell"
|
event = "sell"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
||||||
@@ -1082,6 +1129,30 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
|
|||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fixedSide := fixedSwapAmountSide(swaps[0].Event, swapMode)
|
||||||
|
fixedMint := swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, fixedSide)
|
||||||
|
limitSide := oppositeSwapAmountSide(fixedSide)
|
||||||
|
limitMint := swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, limitSide)
|
||||||
|
actualLimitAmount := swapAmountForSide(swaps[1].BaseAmount, swaps[1].QuoteAmount, limitSide)
|
||||||
|
if swapMode == SwapModeExactOut {
|
||||||
|
fixedSide = fixedSwapAmountSide(swaps[1].Event, swapMode)
|
||||||
|
fixedMint = swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, fixedSide)
|
||||||
|
limitSide = oppositeSwapAmountSide(fixedSwapAmountSide(swaps[0].Event, swapMode))
|
||||||
|
limitMint = swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, limitSide)
|
||||||
|
actualLimitAmount = swapAmountForSide(swaps[0].BaseAmount, swaps[0].QuoteAmount, limitSide)
|
||||||
|
}
|
||||||
|
swaps[0].SetSwapAmountInfoDetailed(
|
||||||
|
swapMode,
|
||||||
|
decimal.NewFromUint64(amountSpecified),
|
||||||
|
fixedSide,
|
||||||
|
fixedMint,
|
||||||
|
limitSwapAmountType(swapMode),
|
||||||
|
decimal.NewFromUint64(otherAmountThreshold),
|
||||||
|
limitSide,
|
||||||
|
limitMint,
|
||||||
|
actualLimitAmount,
|
||||||
|
)
|
||||||
|
swaps[0].SlippageBps = decimal.Zero
|
||||||
return swaps, offset, nil
|
return swaps, offset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1091,6 +1162,14 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
|||||||
}
|
}
|
||||||
|
|
||||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
amountSpecified, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolTwoHopSwapArgs(instruction.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, increaseOffset(offset), err
|
||||||
|
}
|
||||||
|
swapMode := SwapModeExactOut
|
||||||
|
if amountSpecifiedIsInput {
|
||||||
|
swapMode = SwapModeExactIn
|
||||||
|
}
|
||||||
|
|
||||||
user := tx.rawTx.accountList[instruction.Accounts[14]]
|
user := tx.rawTx.accountList[instruction.Accounts[14]]
|
||||||
pool1 := tx.rawTx.accountList[instruction.Accounts[0]]
|
pool1 := tx.rawTx.accountList[instruction.Accounts[0]]
|
||||||
@@ -1105,8 +1184,8 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
|||||||
//pool2UserBase := instruction.Accounts[8]
|
//pool2UserBase := instruction.Accounts[8]
|
||||||
pool2VaultBase := instruction.Accounts[11]
|
pool2VaultBase := instruction.Accounts[11]
|
||||||
|
|
||||||
pool2UserQuote := instruction.Accounts[12]
|
pool2VaultQuote := instruction.Accounts[12]
|
||||||
pool2VaultQuote := instruction.Accounts[13]
|
pool2UserQuote := instruction.Accounts[13]
|
||||||
|
|
||||||
swaps := make([]Swap, 2)
|
swaps := make([]Swap, 2)
|
||||||
{
|
{
|
||||||
@@ -1142,7 +1221,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
|||||||
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
||||||
baseAmount = decimal.NewFromInt(int64(amount))
|
baseAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
|
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
|
||||||
event = "buy"
|
event = "buy"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
||||||
@@ -1150,7 +1229,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
|||||||
}
|
}
|
||||||
baseFound = true
|
baseFound = true
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
||||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
quoteAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
||||||
event = "sell"
|
event = "sell"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
} else if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
||||||
@@ -1186,11 +1265,11 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
|||||||
{
|
{
|
||||||
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultBase)
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultBase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token0 vault balance after tx: %v", err)
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool2 token0 vault balance after tx: %v", err)
|
||||||
}
|
}
|
||||||
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultQuote)
|
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultQuote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token1 vault balance after tx: %v", err)
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool2 token1 vault balance after tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
userBase := decimal.Zero
|
userBase := decimal.Zero
|
||||||
@@ -1216,7 +1295,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
|||||||
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
||||||
baseAmount = decimal.NewFromInt(int64(amount))
|
baseAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
||||||
event = "buy"
|
event = "buy"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
} else if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
||||||
@@ -1224,7 +1303,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
|||||||
}
|
}
|
||||||
baseFound = true
|
baseFound = true
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
||||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
quoteAmount = decimal.NewFromUint64(amount)
|
||||||
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
|
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
|
||||||
event = "sell"
|
event = "sell"
|
||||||
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
||||||
@@ -1258,5 +1337,29 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
|||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fixedSide := fixedSwapAmountSide(swaps[0].Event, swapMode)
|
||||||
|
fixedMint := swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, fixedSide)
|
||||||
|
limitSide := oppositeSwapAmountSide(fixedSide)
|
||||||
|
limitMint := swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, limitSide)
|
||||||
|
actualLimitAmount := swapAmountForSide(swaps[1].BaseAmount, swaps[1].QuoteAmount, limitSide)
|
||||||
|
if swapMode == SwapModeExactOut {
|
||||||
|
fixedSide = fixedSwapAmountSide(swaps[1].Event, swapMode)
|
||||||
|
fixedMint = swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, fixedSide)
|
||||||
|
limitSide = oppositeSwapAmountSide(fixedSwapAmountSide(swaps[0].Event, swapMode))
|
||||||
|
limitMint = swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, limitSide)
|
||||||
|
actualLimitAmount = swapAmountForSide(swaps[0].BaseAmount, swaps[0].QuoteAmount, limitSide)
|
||||||
|
}
|
||||||
|
swaps[0].SetSwapAmountInfoDetailed(
|
||||||
|
swapMode,
|
||||||
|
decimal.NewFromUint64(amountSpecified),
|
||||||
|
fixedSide,
|
||||||
|
fixedMint,
|
||||||
|
limitSwapAmountType(swapMode),
|
||||||
|
decimal.NewFromUint64(otherAmountThreshold),
|
||||||
|
limitSide,
|
||||||
|
limitMint,
|
||||||
|
actualLimitAmount,
|
||||||
|
)
|
||||||
|
swaps[0].SlippageBps = decimal.Zero
|
||||||
return swaps, offset, nil
|
return swaps, offset, nil
|
||||||
}
|
}
|
||||||
|
|||||||
23
orcawhirpool_test.go
Normal file
23
orcawhirpool_test.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package pump_parser
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestOrcaWhirlpoolRemoveLiquidityPreservesLargeUint64TransferAmounts(t *testing.T) {
|
||||||
|
EnableAllParsers()
|
||||||
|
|
||||||
|
tx := mustParseRPCFixtureTx(t, "4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri")
|
||||||
|
if len(tx.Swaps) == 0 {
|
||||||
|
t.Fatal("expected parsed swaps")
|
||||||
|
}
|
||||||
|
|
||||||
|
swap := tx.Swaps[0]
|
||||||
|
if swap.Program != SolProgramOrcaWhirPool {
|
||||||
|
t.Fatalf("program = %s, want %s", swap.Program, SolProgramOrcaWhirPool)
|
||||||
|
}
|
||||||
|
if swap.Event != TxEventRemoveLiquidity {
|
||||||
|
t.Fatalf("event = %s, want %s", swap.Event, TxEventRemoveLiquidity)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertDecimalString(t, "base_amount", swap.BaseAmount, "101086439062")
|
||||||
|
assertDecimalString(t, "quote_amount", swap.QuoteAmount, "9863327902766042414")
|
||||||
|
}
|
||||||
69
parser.go
69
parser.go
@@ -2,7 +2,6 @@ package pump_parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/gagliardetto/solana-go"
|
"github.com/gagliardetto/solana-go"
|
||||||
@@ -14,6 +13,11 @@ var defaultSwapPrograms = map[solana.PublicKey]swapParser{
|
|||||||
pumpProgram: pumpParser,
|
pumpProgram: pumpParser,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errTxParserPrograms = map[solana.PublicKey]swapParser{
|
||||||
|
pumpAmmProgram: pumpAmmParser,
|
||||||
|
pumpProgram: pumpParser,
|
||||||
|
}
|
||||||
|
|
||||||
var swapPrograms = cloneSwapPrograms(defaultSwapPrograms)
|
var swapPrograms = cloneSwapPrograms(defaultSwapPrograms)
|
||||||
|
|
||||||
type ParserOption func(*parserConfig)
|
type ParserOption func(*parserConfig)
|
||||||
@@ -56,8 +60,9 @@ func WithMeteoraDlmm() ParserOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var actionPrograms = map[solana.PublicKey]actionParser{
|
var actionPrograms = map[solana.PublicKey]actionParser{
|
||||||
systemProgram: systemParser,
|
systemProgram: systemParser,
|
||||||
budgGetProgram: budgetParser,
|
budgGetProgram: budgetParser,
|
||||||
|
chainLinkProgram: chainLinkParser,
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseRawTx(rawTx *RawTx) (*Tx, error) {
|
func ParseRawTx(rawTx *RawTx) (*Tx, error) {
|
||||||
@@ -91,11 +96,55 @@ func (tx *Tx) Parser() error {
|
|||||||
tx.BlockAt = tx.rawTx.BlockTime
|
tx.BlockAt = tx.rawTx.BlockTime
|
||||||
tx.CuFee = decimal.NewFromUint64(tx.rawTx.Meta.Fee)
|
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.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.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[0]).Div(decimal.NewFromInt(1e9))
|
||||||
|
|
||||||
tx.Token = make(map[solana.PublicKey]TokenMeta)
|
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)
|
var innersMap = make(map[int]InnerInstructions)
|
||||||
for _, inner := range tx.rawTx.Meta.InnerInstructions {
|
for _, inner := range tx.rawTx.Meta.InnerInstructions {
|
||||||
innersMap[inner.Index] = inner
|
innersMap[inner.Index] = inner
|
||||||
@@ -126,6 +175,7 @@ func (tx *Tx) Parser() error {
|
|||||||
swap.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[userIdx]).Div(decimal.NewFromInt(1e9))
|
swap.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[userIdx]).Div(decimal.NewFromInt(1e9))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
swap.InstrIdx = uint8(i)
|
||||||
tx.Swaps = append(tx.Swaps, swap)
|
tx.Swaps = append(tx.Swaps, swap)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +193,7 @@ func (tx *Tx) Parser() error {
|
|||||||
innerLength := len(innersMap[i].Instructions)
|
innerLength := len(innersMap[i].Instructions)
|
||||||
for j := 1; j <= innerLength; {
|
for j := 1; j <= innerLength; {
|
||||||
if j <= 0 || j > innerLength {
|
if j <= 0 || j > innerLength {
|
||||||
log.Printf("inner instruction index is out if range, block: %d, tx: %s, outerIndex: %d, innerIndex: %d", tx.Block, tx.GetTxHash(), ii, j)
|
//log.Printf("inner instruction index is out if range, block: %d, tx: %s, outerIndex: %d, innerIndex: %d", tx.Block, tx.GetTxHash(), ii, j)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
innerInstr := innersMap[i].Instructions[j-1]
|
innerInstr := innersMap[i].Instructions[j-1]
|
||||||
@@ -173,11 +223,18 @@ func (tx *Tx) Parser() error {
|
|||||||
swap.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[userIdx]).Div(decimal.NewFromInt(1e9))
|
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, swap)
|
||||||
}
|
}
|
||||||
// tx.Swaps = append(tx.Swaps, swaps...)
|
// tx.Swaps = append(tx.Swaps, swaps...)
|
||||||
j = int(offset[1])
|
if ii == int(offset[0]) && j == int(offset[1]) {
|
||||||
ii = int(offset[0])
|
j = j + 1
|
||||||
|
} else {
|
||||||
|
j = int(offset[1])
|
||||||
|
ii = int(offset[0])
|
||||||
|
}
|
||||||
|
|
||||||
} else if p, exists := actionPrograms[innerProgramAccount]; exists {
|
} else if p, exists := actionPrograms[innerProgramAccount]; exists {
|
||||||
offset, err := p(tx, innerInstr, innersMap[i], [2]uint{uint(i), uint(j)})
|
offset, err := p(tx, innerInstr, innersMap[i], [2]uint{uint(i), uint(j)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
215
pump.go
215
pump.go
@@ -34,10 +34,19 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct
|
|||||||
|
|
||||||
switch discriminator {
|
switch discriminator {
|
||||||
case pumpBuyV2Discriminator, pumpBuyDiscriminator, pumpSellDiscriminator:
|
case pumpBuyV2Discriminator, pumpBuyDiscriminator, pumpSellDiscriminator:
|
||||||
|
if tx.Err != nil {
|
||||||
|
return failedTxBuyOrSellParser(tx, instruction, innerInstructions, offset)
|
||||||
|
}
|
||||||
return BuyOrSellParser(tx, instruction, innerInstructions, offset)
|
return BuyOrSellParser(tx, instruction, innerInstructions, offset)
|
||||||
case pumpCreateDiscriminator, pumpCreateV2Discriminator:
|
case pumpCreateDiscriminator, pumpCreateV2Discriminator:
|
||||||
|
if tx.Err != nil {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
return CreateParser(tx, instruction, innerInstructions, offset)
|
return CreateParser(tx, instruction, innerInstructions, offset)
|
||||||
case pumpMigrateDiscriminator:
|
case pumpMigrateDiscriminator:
|
||||||
|
if tx.Err != nil {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
return MigrateParser(tx, instruction, innerInstructions, offset)
|
return MigrateParser(tx, instruction, innerInstructions, offset)
|
||||||
default:
|
default:
|
||||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
@@ -79,6 +88,7 @@ type PumpCreateEvent struct {
|
|||||||
TokenTotalSupply uint64
|
TokenTotalSupply uint64
|
||||||
TokenProgram solana.PublicKey
|
TokenProgram solana.PublicKey
|
||||||
IsMayhemMode bool
|
IsMayhemMode bool
|
||||||
|
IsCashbackEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
@@ -148,6 +158,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
|
|||||||
BaseReserve: decimal.NewFromUint64(createEvent.RealTokenReserves),
|
BaseReserve: decimal.NewFromUint64(createEvent.RealTokenReserves),
|
||||||
QuoteReserve: decimal.Zero,
|
QuoteReserve: decimal.Zero,
|
||||||
Mayhem: createEvent.IsMayhemMode,
|
Mayhem: createEvent.IsMayhemMode,
|
||||||
|
Cashback: createEvent.IsCashbackEnabled,
|
||||||
UserBaseBalance: userBase,
|
UserBaseBalance: userBase,
|
||||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
@@ -176,6 +187,16 @@ type PumpTradeEvent struct {
|
|||||||
|
|
||||||
CreatorFeeBasisPoints uint64
|
CreatorFeeBasisPoints uint64
|
||||||
CreatorFee uint64
|
CreatorFee uint64
|
||||||
|
|
||||||
|
TrackVolume bool
|
||||||
|
TotalUnclaimedTokens uint64
|
||||||
|
TotalClaimedTokens uint64
|
||||||
|
CurrentSolVolume uint64
|
||||||
|
LastUpdateTimestamp int64
|
||||||
|
IxName string
|
||||||
|
MayhemMode bool
|
||||||
|
CashbackFeeBasisPoints uint64
|
||||||
|
Cashback uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
type PumpTradeFeeArg struct {
|
type PumpTradeFeeArg struct {
|
||||||
@@ -191,6 +212,154 @@ type CompleteEvent struct {
|
|||||||
Timestamp int64
|
Timestamp int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PumpTradeArgs struct {
|
||||||
|
Discriminator [8]byte
|
||||||
|
Amount1 uint64
|
||||||
|
Amount2 uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func pumpTradeAmountInfoFromArgs(args PumpTradeArgs) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]):
|
||||||
|
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
|
||||||
|
case bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]):
|
||||||
|
return SwapModeExactOut, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
|
||||||
|
case bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]):
|
||||||
|
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
|
||||||
|
default:
|
||||||
|
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pumpCompleteMatchesTradeEvent(completeEvent CompleteEvent, tradeEvent PumpTradeEvent, bondingCurve solana.PublicKey) bool {
|
||||||
|
if completeEvent.Mint != tradeEvent.Mint {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if completeEvent.User != tradeEvent.User {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if completeEvent.BondingCurve != bondingCurve {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePumpQuoteSideMint(s *Swap) {
|
||||||
|
if s.FixedAmountSide == SwapAmountSideQuote && s.FixedMint.IsZero() {
|
||||||
|
s.FixedMint = wSolMint
|
||||||
|
}
|
||||||
|
if s.LimitAmountSide == SwapAmountSideQuote && s.LimitMint.IsZero() {
|
||||||
|
s.LimitMint = wSolMint
|
||||||
|
}
|
||||||
|
if s.ActualLimitAmountSide == SwapAmountSideQuote && s.LimitMint.IsZero() {
|
||||||
|
s.LimitMint = wSolMint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
|
if tx.Err == nil || tx.Err.UnKnown != "" {
|
||||||
|
return nil, increaseOffset(offset), fmt.Errorf("tx pump failed but error is nil, offset, %d, %d", offset[0], offset[1])
|
||||||
|
}
|
||||||
|
if tx.Err.Variant != InstructionError {
|
||||||
|
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but error variant is not instruction error, offset, %d, %d", offset[0], offset[1])
|
||||||
|
}
|
||||||
|
if tx.Err.Enum != Custom && tx.Err.Enum != ComputationalBudgetExceeded && tx.Err.Enum != ProgramFailedToComplete {
|
||||||
|
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but error is not custom or computational budget exceeded, offset, %d, %d", offset[0], offset[1])
|
||||||
|
}
|
||||||
|
if tx.Err.Enum == Custom {
|
||||||
|
if !(tx.Err.CustomCode == 1 ||
|
||||||
|
tx.Err.CustomCode == 6042 ||
|
||||||
|
tx.Err.CustomCode == 6041 ||
|
||||||
|
tx.Err.CustomCode == 6040 ||
|
||||||
|
tx.Err.CustomCode == 6023 || tx.Err.CustomCode == 6021 || tx.Err.CustomCode == 6003 || tx.Err.CustomCode == 6002) {
|
||||||
|
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but custom error code is unexpected, offset, %d, %d, code: %d", offset[0], offset[1], tx.Err.CustomCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := tx.rawTx
|
||||||
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
|
||||||
|
user := result.accountList[instruction.Accounts[6]]
|
||||||
|
ataUserIdx := instruction.Accounts[5]
|
||||||
|
userIndex := instruction.Accounts[6]
|
||||||
|
mint := result.accountList[instruction.Accounts[2]]
|
||||||
|
var args PumpTradeArgs
|
||||||
|
err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump buy/sell decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
|
}
|
||||||
|
var event string
|
||||||
|
var (
|
||||||
|
solAmount, tokenAmount uint64
|
||||||
|
)
|
||||||
|
if bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) {
|
||||||
|
event = "buy_failed"
|
||||||
|
solAmount = args.Amount1
|
||||||
|
tokenAmount = args.Amount2
|
||||||
|
} else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) {
|
||||||
|
event = "buy_failed"
|
||||||
|
solAmount = args.Amount2
|
||||||
|
tokenAmount = args.Amount1
|
||||||
|
} else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) {
|
||||||
|
event = "sell_failed"
|
||||||
|
solAmount = args.Amount2
|
||||||
|
tokenAmount = args.Amount1
|
||||||
|
} else {
|
||||||
|
return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction discriminator, offset, %d, %d", offset[0], offset[1])
|
||||||
|
}
|
||||||
|
var baseTokenProgram solana.PublicKey
|
||||||
|
|
||||||
|
if event == "buy_failed" {
|
||||||
|
baseTokenProgram = result.accountList[instruction.Accounts[8]]
|
||||||
|
} else {
|
||||||
|
baseTokenProgram = result.accountList[instruction.Accounts[9]]
|
||||||
|
}
|
||||||
|
if !user.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
|
||||||
|
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, mint)
|
||||||
|
//&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount
|
||||||
|
if !userBaseAmount.IsZero() {
|
||||||
|
user = result.accountList[0]
|
||||||
|
userIndex = 0
|
||||||
|
ataUserIdx = ataIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userBase := getAccountBalanceAfterTx(result, ataUserIdx)
|
||||||
|
userQuote, _ := GetSolAfterTx(result, userIndex)
|
||||||
|
|
||||||
|
bcIdx := instruction.Accounts[3]
|
||||||
|
bcAtaIndex := instruction.Accounts[4]
|
||||||
|
solReserves, _ := GetSolAfterTx(result, bcIdx)
|
||||||
|
tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex)
|
||||||
|
swaps := []Swap{
|
||||||
|
{
|
||||||
|
Program: SolProgramPump,
|
||||||
|
Event: event,
|
||||||
|
Pool: result.accountList[instruction.Accounts[3]],
|
||||||
|
BaseMint: mint,
|
||||||
|
QuoteMint: solana.PublicKey{},
|
||||||
|
BaseTokenProgram: baseTokenProgram,
|
||||||
|
QuoteTokenProgram: solana.PublicKey{},
|
||||||
|
BaseMintDecimals: 6,
|
||||||
|
QuoteMintDecimals: 9,
|
||||||
|
User: user,
|
||||||
|
BaseAmount: decimal.NewFromUint64(tokenAmount),
|
||||||
|
QuoteAmount: decimal.NewFromUint64(solAmount),
|
||||||
|
BaseReserve: tokenReserves,
|
||||||
|
QuoteReserve: decimal.NewFromUint64(solReserves),
|
||||||
|
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
|
||||||
|
UserBaseBalance: userBase,
|
||||||
|
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
||||||
|
EntryContract: entryContract,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
|
||||||
|
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||||
|
normalizePumpQuoteSideMint(&swaps[0])
|
||||||
|
}
|
||||||
|
return swaps, offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
@@ -210,6 +379,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
completeEvent CompleteEvent
|
completeEvent CompleteEvent
|
||||||
completed bool
|
completed bool
|
||||||
newoffset [2]uint
|
newoffset [2]uint
|
||||||
|
tradeFound bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var prefixLen = offset[1]
|
var prefixLen = offset[1]
|
||||||
@@ -217,10 +387,13 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("pump create get inner instructions error: %v,offset, %d, %d", err, offset[0], offset[1])
|
return nil, increaseOffset(offset), fmt.Errorf("pump create get inner instructions error: %v,offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
if instruction.StackHeight != nil && *instruction.StackHeight > 2 {
|
if !entryContract.Equals(axiomOuterContract) {
|
||||||
for _, innerInstr := range innerInstructions.Instructions {
|
if instruction.StackHeight != nil && *instruction.StackHeight > 2 {
|
||||||
if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 {
|
for _, innerInstr := range innerInstructions.Instructions {
|
||||||
entryContract = result.accountList[innerInstr.ProgramIDIndex]
|
if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 {
|
||||||
|
entryContract = result.accountList[innerInstr.ProgramIDIndex]
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,6 +408,9 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
}
|
}
|
||||||
if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) {
|
if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) {
|
||||||
if bytes.Equal(innerInstr.Data[8:16], pumpTradeEventDiscriminator[8:16]) {
|
if bytes.Equal(innerInstr.Data[8:16], pumpTradeEventDiscriminator[8:16]) {
|
||||||
|
if tradeFound {
|
||||||
|
break
|
||||||
|
}
|
||||||
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&tradeEvent)
|
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&tradeEvent)
|
||||||
if offset[1] == 0 {
|
if offset[1] == 0 {
|
||||||
newoffset = [2]uint{offset[0] + 1, offset[1]}
|
newoffset = [2]uint{offset[0] + 1, offset[1]}
|
||||||
@@ -244,19 +420,31 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
}
|
}
|
||||||
|
expectedIsBuy := !bytes.Equal(instruction.Data[:8], pumpSellDiscriminator[:])
|
||||||
|
if tradeEvent.IsBuy != expectedIsBuy {
|
||||||
|
tradeEvent = PumpTradeEvent{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tradeFound = true
|
||||||
if !tradeEvent.IsBuy {
|
if !tradeEvent.IsBuy {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else if bytes.Equal(innerInstr.Data[8:16], pumpCompleteEventDiscriminator[:]) {
|
} else if bytes.Equal(innerInstr.Data[8:16], pumpCompleteEventDiscriminator[:]) {
|
||||||
|
if !tradeFound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&completeEvent)
|
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&completeEvent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, increaseOffset(offset), fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
|
}
|
||||||
|
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, result.accountList[instruction.Accounts[3]]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
if offset[1] == 0 {
|
if offset[1] == 0 {
|
||||||
newoffset = [2]uint{offset[0] + 1, offset[1]}
|
newoffset = [2]uint{offset[0] + 1, offset[1]}
|
||||||
} else {
|
} else {
|
||||||
newoffset = [2]uint{offset[0], prefixLen + uint(innerIndex) + 1}
|
newoffset = [2]uint{offset[0], prefixLen + uint(innerIndex) + 1}
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, newoffset, fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
||||||
}
|
|
||||||
completed = true
|
completed = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -269,6 +457,11 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
|
|
||||||
offset = [2]uint{newoffset[0], newoffset[1]}
|
offset = [2]uint{newoffset[0], newoffset[1]}
|
||||||
|
|
||||||
|
var args PumpTradeArgs
|
||||||
|
if err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args); err != nil {
|
||||||
|
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump buy/sell decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||||
|
}
|
||||||
|
|
||||||
event := ""
|
event := ""
|
||||||
baseTokenProgram := solana.TokenProgramID
|
baseTokenProgram := solana.TokenProgramID
|
||||||
if tradeEvent.IsBuy {
|
if tradeEvent.IsBuy {
|
||||||
@@ -311,6 +504,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
solAmount = solAmount - fee
|
solAmount = solAmount - fee
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isCashbackCoin := tradeEvent.CashbackFeeBasisPoints > 0 || tradeEvent.Cashback > 0
|
||||||
swaps := []Swap{
|
swaps := []Swap{
|
||||||
{
|
{
|
||||||
Program: SolProgramPump,
|
Program: SolProgramPump,
|
||||||
@@ -332,8 +526,13 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
UserBaseBalance: userBase,
|
UserBaseBalance: userBase,
|
||||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
|
Cashback: isCashbackCoin,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
|
||||||
|
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||||
|
normalizePumpQuoteSideMint(&swaps[0])
|
||||||
|
}
|
||||||
if completed {
|
if completed {
|
||||||
swaps = append(swaps, Swap{
|
swaps = append(swaps, Swap{
|
||||||
Program: SolProgramPump,
|
Program: SolProgramPump,
|
||||||
@@ -347,6 +546,8 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
BaseMintDecimals: 6,
|
BaseMintDecimals: 6,
|
||||||
QuoteMintDecimals: 9,
|
QuoteMintDecimals: 9,
|
||||||
User: user,
|
User: user,
|
||||||
|
BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves),
|
||||||
|
QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves),
|
||||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
|
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
|
||||||
UserBaseBalance: userBase,
|
UserBaseBalance: userBase,
|
||||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
||||||
|
|||||||
63
pump_test.go
63
pump_test.go
@@ -11,6 +11,31 @@ import (
|
|||||||
"github.com/mr-tron/base58"
|
"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) {
|
func TestTradeEvent(t *testing.T) {
|
||||||
hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e"
|
hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e"
|
||||||
d, err := hex.DecodeString(hexData)
|
d, err := hex.DecodeString(hexData)
|
||||||
@@ -18,13 +43,21 @@ func TestTradeEvent(t *testing.T) {
|
|||||||
t.Errorf("Failed to decode base64 data: %v", err)
|
t.Errorf("Failed to decode base64 data: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var tradeEvent PumpTradeEvent
|
var tradeEvent legacyPumpTradeEvent
|
||||||
|
|
||||||
err = agbinary.NewBorshDecoder(d[16:]).Decode(&tradeEvent)
|
err = agbinary.NewBorshDecoder(d[16:]).Decode(&tradeEvent)
|
||||||
if err != nil {
|
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)
|
t.Logf("Trade Event: %+v", tradeEvent)
|
||||||
|
|
||||||
xx, err := base58.Decode("3Bxs48EzTZB4tzRd")
|
xx, err := base58.Decode("3Bxs48EzTZB4tzRd")
|
||||||
@@ -43,3 +76,27 @@ func TestCal(t *testing.T) {
|
|||||||
|
|
||||||
fmt.Println(solana.MustPublicKeyFromBase58("BM9CcyErJcu2mjrFvUsRRrD3snGeHDDVirJLvL6EjvMN").IsOnCurve())
|
fmt.Println(solana.MustPublicKeyFromBase58("BM9CcyErJcu2mjrFvUsRRrD3snGeHDDVirJLvL6EjvMN").IsOnCurve())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPumpCompleteMatchesTradeEvent(t *testing.T) {
|
||||||
|
mint := solana.MustPublicKeyFromBase58("8GNGkNnfBuoTP3QRnmdNzSYuuE15M8tvcNvxNsV4pump")
|
||||||
|
user := solana.MustPublicKeyFromBase58("DS95KxqUCCjwQaXhD7fhKatXbivwWDNrJdNV5ZcubGdz")
|
||||||
|
bondingCurve := solana.MustPublicKeyFromBase58("Gz5EX3X7kUDS48baijJKubQDKy3BBKpnMJQ3f3W1e9jA")
|
||||||
|
|
||||||
|
tradeEvent := PumpTradeEvent{
|
||||||
|
Mint: mint,
|
||||||
|
User: user,
|
||||||
|
}
|
||||||
|
completeEvent := CompleteEvent{
|
||||||
|
Mint: mint,
|
||||||
|
User: user,
|
||||||
|
BondingCurve: bondingCurve,
|
||||||
|
}
|
||||||
|
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, bondingCurve) {
|
||||||
|
t.Fatal("pumpCompleteMatchesTradeEvent() = false, want true")
|
||||||
|
}
|
||||||
|
|
||||||
|
completeEvent.User = solana.MustPublicKeyFromBase58("3g89wLRwJ5P22fkCdPJBAP7iiYAo6yY96geQvMYj6tYm")
|
||||||
|
if pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, bondingCurve) {
|
||||||
|
t.Fatal("pumpCompleteMatchesTradeEvent() = true for mismatched user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
418
pumpamm.go
418
pumpamm.go
@@ -41,6 +41,8 @@ type ammBuyEvent struct {
|
|||||||
LastUpdateTimestamp int64
|
LastUpdateTimestamp int64
|
||||||
MinBaseAmountOut uint64
|
MinBaseAmountOut uint64
|
||||||
IxName string
|
IxName string
|
||||||
|
CashbackFeeBasisPoints uint64
|
||||||
|
Cashback uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
type ammCreatePoolEvent struct {
|
type ammCreatePoolEvent struct {
|
||||||
@@ -113,6 +115,8 @@ type ammSellEvent struct {
|
|||||||
CoinCreator solana.PublicKey
|
CoinCreator solana.PublicKey
|
||||||
CoinCreatorFeeBasisPoints uint64
|
CoinCreatorFeeBasisPoints uint64
|
||||||
CoinCreatorFee uint64
|
CoinCreatorFee uint64
|
||||||
|
CashbackFeeBasisPoints uint64
|
||||||
|
Cashback uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
type ammWithdrawEvent struct {
|
type ammWithdrawEvent struct {
|
||||||
@@ -148,14 +152,29 @@ func pumpAmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
|||||||
discriminator := *(*[8]byte)(decode[:8])
|
discriminator := *(*[8]byte)(decode[:8])
|
||||||
switch discriminator {
|
switch discriminator {
|
||||||
case pumpAmmCreateDiscriminator:
|
case pumpAmmCreateDiscriminator:
|
||||||
|
if tx.Err != nil {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
return ammCreatePoolParser(tx, instruction, innerInstructions, offset)
|
return ammCreatePoolParser(tx, instruction, innerInstructions, offset)
|
||||||
case pumpAmmBuyDiscriminator, pumpAmmBuyV2Discriminator:
|
case pumpAmmBuyDiscriminator, pumpAmmBuyV2Discriminator:
|
||||||
|
if tx.Err != nil {
|
||||||
|
return failedTxAmmBuyParser(tx, instruction, innerInstructions, offset)
|
||||||
|
}
|
||||||
return ammBuyParser(tx, instruction, innerInstructions, offset)
|
return ammBuyParser(tx, instruction, innerInstructions, offset)
|
||||||
case pumpAmmSellDiscriminator:
|
case pumpAmmSellDiscriminator:
|
||||||
|
if tx.Err != nil {
|
||||||
|
return failedTxAmmSellParser(tx, instruction, innerInstructions, offset)
|
||||||
|
}
|
||||||
return ammSellParser(tx, instruction, innerInstructions, offset)
|
return ammSellParser(tx, instruction, innerInstructions, offset)
|
||||||
case pumpAmmDepositDiscriminator:
|
case pumpAmmDepositDiscriminator:
|
||||||
|
if tx.Err != nil {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
return depositParse(tx, instruction, innerInstructions, offset)
|
return depositParse(tx, instruction, innerInstructions, offset)
|
||||||
case pumpAmmWithdrawDiscriminator:
|
case pumpAmmWithdrawDiscriminator:
|
||||||
|
if tx.Err != nil {
|
||||||
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
|
}
|
||||||
return withdrawParse(tx, instruction, innerInstructions, offset)
|
return withdrawParse(tx, instruction, innerInstructions, offset)
|
||||||
default:
|
default:
|
||||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
@@ -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) {
|
func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
result := tx.rawTx
|
result := tx.rawTx
|
||||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
@@ -245,10 +529,13 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
|
return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
|
||||||
}
|
}
|
||||||
if instruction.StackHeight != nil && *instruction.StackHeight > 2 {
|
if !entryContract.Equals(axiomOuterContract) {
|
||||||
for _, innerInstr := range innerInstructions.Instructions {
|
if instruction.StackHeight != nil && *instruction.StackHeight > 2 {
|
||||||
if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 {
|
for _, innerInstr := range innerInstructions.Instructions {
|
||||||
entryContract = result.accountList[innerInstr.ProgramIDIndex]
|
if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 {
|
||||||
|
entryContract = result.accountList[innerInstr.ProgramIDIndex]
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,29 +615,43 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
|||||||
userBalance, _ := GetSolAfterTx(result, userIndex)
|
userBalance, _ := GetSolAfterTx(result, userIndex)
|
||||||
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
|
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
|
||||||
}
|
}
|
||||||
return []Swap{
|
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
|
||||||
{
|
swap := Swap{
|
||||||
Program: SolProgramPumpAMM,
|
Program: SolProgramPumpAMM,
|
||||||
Event: "buy",
|
Event: "buy",
|
||||||
Pool: event.Pool,
|
Pool: event.Pool,
|
||||||
BaseMint: baseMint,
|
BaseMint: baseMint,
|
||||||
QuoteMint: quoteMint,
|
QuoteMint: quoteMint,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
QuoteTokenProgram: quoteTokenProgram,
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
Creator: event.CoinCreator,
|
Creator: event.CoinCreator,
|
||||||
BaseMintDecimals: baseMintDecimals,
|
BaseMintDecimals: baseMintDecimals,
|
||||||
QuoteMintDecimals: quoteMintDecimals,
|
QuoteMintDecimals: quoteMintDecimals,
|
||||||
User: eventUser,
|
User: eventUser,
|
||||||
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
|
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
|
||||||
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountIn),
|
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountIn),
|
||||||
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
|
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
|
||||||
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
|
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
|
||||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
||||||
UserBaseBalance: userBase,
|
Cashback: isCashbackCoin,
|
||||||
UserQuoteBalance: userQuote,
|
UserBaseBalance: userBase,
|
||||||
EntryContract: entryContract,
|
UserQuoteBalance: userQuote,
|
||||||
},
|
EntryContract: entryContract,
|
||||||
}, offset, nil
|
}
|
||||||
|
if bytes.Equal(instruction.Data[:8], pumpAmmBuyV2Discriminator[:]) {
|
||||||
|
swap.SetSwapAmountInfo(
|
||||||
|
SwapModeExactIn,
|
||||||
|
decimal.NewFromUint64(event.UserQuoteAmountIn),
|
||||||
|
decimal.NewFromUint64(event.MinBaseAmountOut),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
swap.SetSwapAmountInfo(
|
||||||
|
SwapModeExactOut,
|
||||||
|
decimal.NewFromUint64(event.BaseAmountOut),
|
||||||
|
decimal.NewFromUint64(event.MaxQuoteAmountIn),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
@@ -363,10 +664,13 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
|||||||
return nil, increaseOffset(offset), fmt.Errorf("pumpamm sell get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
|
return nil, increaseOffset(offset), fmt.Errorf("pumpamm sell get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
|
||||||
}
|
}
|
||||||
|
|
||||||
if instruction.StackHeight != nil && *instruction.StackHeight > 2 {
|
if !entryContract.Equals(axiomOuterContract) {
|
||||||
for _, innerInstr := range innerInstructions.Instructions {
|
if instruction.StackHeight != nil && *instruction.StackHeight > 2 {
|
||||||
if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 {
|
for _, innerInstr := range innerInstructions.Instructions {
|
||||||
entryContract = result.accountList[innerInstr.ProgramIDIndex]
|
if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 {
|
||||||
|
entryContract = result.accountList[innerInstr.ProgramIDIndex]
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,29 +750,35 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
|
|||||||
userBalance, _ := GetSolAfterTx(result, userIndex)
|
userBalance, _ := GetSolAfterTx(result, userIndex)
|
||||||
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
|
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
|
||||||
}
|
}
|
||||||
return []Swap{
|
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
|
||||||
{
|
swap := Swap{
|
||||||
Program: SolProgramPumpAMM,
|
Program: SolProgramPumpAMM,
|
||||||
Event: "sell",
|
Event: "sell",
|
||||||
Pool: event.Pool,
|
Pool: event.Pool,
|
||||||
BaseMint: baseMint,
|
BaseMint: baseMint,
|
||||||
QuoteMint: quoteMint,
|
QuoteMint: quoteMint,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
QuoteTokenProgram: quoteTokenProgram,
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
Creator: event.CoinCreator,
|
Creator: event.CoinCreator,
|
||||||
BaseMintDecimals: baseMintDecimals,
|
BaseMintDecimals: baseMintDecimals,
|
||||||
QuoteMintDecimals: quoteMintDecimals,
|
QuoteMintDecimals: quoteMintDecimals,
|
||||||
User: eventUser,
|
User: eventUser,
|
||||||
BaseAmount: decimal.NewFromUint64(event.BaseAmountIn),
|
BaseAmount: decimal.NewFromUint64(event.BaseAmountIn),
|
||||||
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountOut),
|
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountOut),
|
||||||
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn),
|
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn),
|
||||||
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut),
|
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut),
|
||||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
||||||
UserBaseBalance: userBase,
|
Cashback: isCashbackCoin,
|
||||||
UserQuoteBalance: userQuote,
|
UserBaseBalance: userBase,
|
||||||
EntryContract: entryContract,
|
UserQuoteBalance: userQuote,
|
||||||
},
|
EntryContract: entryContract,
|
||||||
}, offset, nil
|
}
|
||||||
|
swap.SetSwapAmountInfo(
|
||||||
|
SwapModeExactIn,
|
||||||
|
decimal.NewFromUint64(event.BaseAmountIn),
|
||||||
|
decimal.NewFromUint64(event.MinQuoteAmountOut),
|
||||||
|
)
|
||||||
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
|
|||||||
109
rawtx.go
109
rawtx.go
@@ -73,6 +73,10 @@ func (tx *RawTx) GetAccountLust() []solana.PublicKey {
|
|||||||
return tx.getAccountList()
|
return tx.getAccountList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tx *RawTx) GetAccountList() []solana.PublicKey {
|
||||||
|
return tx.getAccountList()
|
||||||
|
}
|
||||||
|
|
||||||
func (tx *RawTx) TxHash() string {
|
func (tx *RawTx) TxHash() string {
|
||||||
if len(tx.Transaction.Signatures) > 0 {
|
if len(tx.Transaction.Signatures) > 0 {
|
||||||
return tx.Transaction.Signatures[0].String()
|
return tx.Transaction.Signatures[0].String()
|
||||||
@@ -146,16 +150,17 @@ func (tb *TokenBalance) ParseAccount() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Meta struct {
|
type Meta struct {
|
||||||
Err interface{} `json:"err"`
|
Err *TransactionParsedError `json:"err"`
|
||||||
Fee uint64 `json:"fee"`
|
Fee uint64 `json:"fee"`
|
||||||
InnerInstructions []InnerInstructions `json:"innerInstructions"`
|
InnerInstructions []InnerInstructions `json:"innerInstructions"`
|
||||||
LoadedAddresses LoadedAddresses `json:"loadedAddresses"`
|
LoadedAddresses LoadedAddresses `json:"loadedAddresses"`
|
||||||
LogMessages []string `json:"logMessages"`
|
LogMessages []string `json:"logMessages"`
|
||||||
PostBalances []uint64 `json:"postBalances"`
|
PostBalances []uint64 `json:"postBalances"`
|
||||||
PostTokenBalances []TokenBalance `json:"postTokenBalances"`
|
PostTokenBalances []TokenBalance `json:"postTokenBalances"`
|
||||||
PreBalances []uint64 `json:"preBalances"`
|
PreBalances []uint64 `json:"preBalances"`
|
||||||
PreTokenBalances []TokenBalance `json:"preTokenBalances"`
|
PreTokenBalances []TokenBalance `json:"preTokenBalances"`
|
||||||
Rewards []interface{} `json:"rewards"`
|
Rewards []interface{} `json:"rewards"`
|
||||||
|
ComputeUnitsConsumed uint64 `json:"computeUnitsConsumed"`
|
||||||
}
|
}
|
||||||
type Header struct {
|
type Header struct {
|
||||||
NumReadonlySignedAccounts int `json:"numReadonlySignedAccounts"`
|
NumReadonlySignedAccounts int `json:"numReadonlySignedAccounts"`
|
||||||
@@ -293,6 +298,16 @@ func InstructionsFromRpc(instructions []solana.CompiledInstruction) []Instructio
|
|||||||
return instrs
|
return instrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RpcTransactionErr []interface{}
|
||||||
|
|
||||||
|
func marshalRpcTransactionErr(err any) string {
|
||||||
|
e, _ := json.Marshal(err)
|
||||||
|
if len(e) == 0 {
|
||||||
|
return "UnKnown"
|
||||||
|
}
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, slot uint64, index int64) (*RawTx, error) {
|
func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, slot uint64, index int64) (*RawTx, error) {
|
||||||
created := int64(0)
|
created := int64(0)
|
||||||
if blockTime != nil {
|
if blockTime != nil {
|
||||||
@@ -320,12 +335,68 @@ func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, s
|
|||||||
yTx, _ := tx.GetTransaction()
|
yTx, _ := tx.GetTransaction()
|
||||||
|
|
||||||
if meta.Err != nil {
|
if meta.Err != nil {
|
||||||
e, _ := json.Marshal(meta.Err)
|
if iErr, ok := meta.Err.(map[string]any); ok {
|
||||||
sTx.Meta.Err = string(e)
|
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.Fee = meta.Fee
|
||||||
//sTx.Meta.InnerInstructions = meta.InnerInstructions
|
//sTx.Meta.InnerInstructions = meta.InnerInstructions
|
||||||
|
if meta.ComputeUnitsConsumed != nil {
|
||||||
|
sTx.Meta.ComputeUnitsConsumed = *meta.ComputeUnitsConsumed
|
||||||
|
}
|
||||||
for _, innerInstr := range meta.InnerInstructions {
|
for _, innerInstr := range meta.InnerInstructions {
|
||||||
var instrs []Instruction
|
var instrs []Instruction
|
||||||
for _, instr := range innerInstr.Instructions {
|
for _, instr := range innerInstr.Instructions {
|
||||||
@@ -513,6 +584,7 @@ func intSliceFromUint16Slice(in []uint16) []int {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAtaIdxByOwner(result *RawTx, owner solana.PublicKey, mint solana.PublicKey) (int, error) {
|
func getAtaIdxByOwner(result *RawTx, owner solana.PublicKey, mint solana.PublicKey) (int, error) {
|
||||||
var preBalance *TokenBalance
|
var preBalance *TokenBalance
|
||||||
for _, pre := range result.Meta.PreTokenBalances {
|
for _, pre := range result.Meta.PreTokenBalances {
|
||||||
@@ -795,17 +867,14 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT
|
|||||||
|
|
||||||
if meta.Err != nil && len(meta.Err.GetErr()) > 0 {
|
if meta.Err != nil && len(meta.Err.GetErr()) > 0 {
|
||||||
// If the transaction has an error, we set the error in the Meta
|
// If the transaction has an error, we set the error in the Meta
|
||||||
transError, err := DecodeTransactionError(meta.Err.GetErr())
|
sTx.Meta.Err = ParseTransactionErrorFromGeyser(meta.Err.GetErr())
|
||||||
if err != nil {
|
|
||||||
sTx.Meta.Err = err
|
|
||||||
} else {
|
|
||||||
sTx.Meta.Err = transError
|
|
||||||
}
|
|
||||||
// sTx.Meta.Err = meta.Err.GetErr()
|
// sTx.Meta.Err = meta.Err.GetErr()
|
||||||
}
|
}
|
||||||
sTx.Meta.Fee = meta.Fee
|
sTx.Meta.Fee = meta.Fee
|
||||||
//sTx.Meta.InnerInstructions = meta.InnerInstructions
|
//sTx.Meta.InnerInstructions = meta.InnerInstructions
|
||||||
|
if meta.ComputeUnitsConsumed != nil {
|
||||||
|
sTx.Meta.ComputeUnitsConsumed = *meta.ComputeUnitsConsumed
|
||||||
|
}
|
||||||
for _, innerInstr := range meta.InnerInstructions {
|
for _, innerInstr := range meta.InnerInstructions {
|
||||||
var instrs []Instruction
|
var instrs []Instruction
|
||||||
for _, instr := range innerInstr.Instructions {
|
for _, instr := range innerInstr.Instructions {
|
||||||
|
|||||||
@@ -1,12 +1,27 @@
|
|||||||
package pump_parser
|
package pump_parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gagliardetto/solana-go"
|
"github.com/gagliardetto/solana-go"
|
||||||
"github.com/shopspring/decimal"
|
"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) {
|
func raydiumClmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumClmmProgramID) {
|
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])
|
return nil, increaseOffset(offset), fmt.Errorf("raydiumClmm instruction not found, offset, %d, %d", offset[0], offset[1])
|
||||||
@@ -278,6 +293,10 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
userTokenOutAccount int
|
userTokenOutAccount int
|
||||||
)
|
)
|
||||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
amountSpecified, otherAmountThreshold, swapMode, err := decodeRaydiumClmmSwapArgs(instruction.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, increaseOffset(offset), err
|
||||||
|
}
|
||||||
if discriminator == raydiumClmmSwapDiscriminator {
|
if discriminator == raydiumClmmSwapDiscriminator {
|
||||||
accountMin = 9
|
accountMin = 9
|
||||||
pool = tx.rawTx.accountList[instruction.Accounts[2]]
|
pool = tx.rawTx.accountList[instruction.Accounts[2]]
|
||||||
@@ -350,26 +369,26 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
|
|
||||||
offset[1] += 2
|
offset[1] += 2
|
||||||
|
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramRaydiumCLMM,
|
||||||
Program: SolProgramRaydiumCLMM,
|
Event: "sell",
|
||||||
Event: "sell",
|
Pool: pool,
|
||||||
Pool: pool,
|
BaseMint: baseMint,
|
||||||
BaseMint: baseMint,
|
QuoteMint: quoteMint,
|
||||||
QuoteMint: quoteMint,
|
BaseTokenProgram: baseTokenProgram,
|
||||||
BaseTokenProgram: baseTokenProgram,
|
QuoteTokenProgram: quoteTokenProgram,
|
||||||
QuoteTokenProgram: quoteTokenProgram,
|
BaseMintDecimals: baseMintDecimals,
|
||||||
BaseMintDecimals: baseMintDecimals,
|
QuoteMintDecimals: quoteMintDecimals,
|
||||||
QuoteMintDecimals: quoteMintDecimals,
|
User: tx.rawTx.accountList[instruction.Accounts[0]],
|
||||||
User: tx.rawTx.accountList[instruction.Accounts[0]],
|
BaseAmount: baseAmount,
|
||||||
BaseAmount: baseAmount,
|
QuoteAmount: quoteAmount,
|
||||||
QuoteAmount: quoteAmount,
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
|
||||||
}, offset, nil
|
return []Swap{swap}, offset, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,20 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
agbinary "github.com/gagliardetto/binary"
|
||||||
"github.com/shopspring/decimal"
|
"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) {
|
func raydiumCPmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumCPmmProgramID) {
|
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])
|
return nil, increaseOffset(offset), fmt.Errorf("raydiumCPmm instruction not found, offset, %d, %d", offset[0], offset[1])
|
||||||
@@ -54,12 +65,12 @@ func raydiumCPmmCreatePoolParser(tx *Tx, instruction Instruction, innerInstructi
|
|||||||
vault1 = instruction.Accounts[12]
|
vault1 = instruction.Accounts[12]
|
||||||
}
|
}
|
||||||
|
|
||||||
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[vault0])
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[vault1])
|
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
|
||||||
}
|
}
|
||||||
@@ -327,6 +338,30 @@ func raydiumCPmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
}
|
}
|
||||||
|
|
||||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
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]]
|
market := tx.rawTx.accountList[instruction.Accounts[3]]
|
||||||
// Get token accounts from instruction
|
// Get token accounts from instruction
|
||||||
tokenIn := tx.rawTx.accountList[instruction.Accounts[4]]
|
tokenIn := tx.rawTx.accountList[instruction.Accounts[4]]
|
||||||
@@ -384,25 +419,26 @@ func raydiumCPmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
|||||||
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions")
|
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions")
|
||||||
}
|
}
|
||||||
offset[1] += 2
|
offset[1] += 2
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramRaydiumCPMM,
|
||||||
Program: SolProgramRaydiumCPMM,
|
Event: "sell",
|
||||||
Event: "sell",
|
Pool: market,
|
||||||
Pool: market,
|
BaseMint: inputTokenMint,
|
||||||
BaseMint: inputTokenMint,
|
QuoteMint: outputTokenMint,
|
||||||
QuoteMint: outputTokenMint,
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
||||||
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
||||||
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
||||||
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
||||||
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
BaseAmount: baseAmount,
|
||||||
BaseAmount: baseAmount,
|
QuoteAmount: quoteAmount,
|
||||||
QuoteAmount: quoteAmount,
|
User: user,
|
||||||
User: user,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||||
}, offset, nil
|
|
||||||
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ type RaydiumLaunchLabCreateEvent struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func raydiumLaunchLabInitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func raydiumLaunchLabInitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if len(instruction.Accounts) < 16 {
|
if len(instruction.Accounts) < 15 {
|
||||||
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction")
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction")
|
||||||
}
|
}
|
||||||
user := tx.rawTx.accountList[instruction.Accounts[0]]
|
user := tx.rawTx.accountList[instruction.Accounts[0]]
|
||||||
@@ -367,6 +367,11 @@ type RaydiumLaunchLabSwapEvent struct {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type raydiumLaunchLabSwapArgs struct {
|
||||||
|
Amount uint64
|
||||||
|
OtherAmountThreshold uint64
|
||||||
|
}
|
||||||
|
|
||||||
func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
|
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
|
||||||
var programName string
|
var programName string
|
||||||
@@ -375,6 +380,26 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
|
|||||||
} else {
|
} else {
|
||||||
programName = SolProgramRaydiumLaunchLab
|
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]
|
var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
user := tx.rawTx.accountList[instruction.Accounts[0]]
|
user := tx.rawTx.accountList[instruction.Accounts[0]]
|
||||||
pool := tx.rawTx.accountList[instruction.Accounts[4]]
|
pool := tx.rawTx.accountList[instruction.Accounts[4]]
|
||||||
@@ -447,7 +472,7 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
|
|||||||
userBase := getAccountBalanceAfterTx(tx.rawTx, userBaseIdx)
|
userBase := getAccountBalanceAfterTx(tx.rawTx, userBaseIdx)
|
||||||
userQuote := getAccountBalanceAfterTx(tx.rawTx, userQuoteIdx)
|
userQuote := getAccountBalanceAfterTx(tx.rawTx, userQuoteIdx)
|
||||||
|
|
||||||
return []Swap{{
|
swap := Swap{
|
||||||
Program: programName,
|
Program: programName,
|
||||||
Event: event,
|
Event: event,
|
||||||
Pool: pool,
|
Pool: pool,
|
||||||
@@ -466,5 +491,8 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
|
|||||||
UserBaseBalance: userBase,
|
UserBaseBalance: userBase,
|
||||||
UserQuoteBalance: userQuote,
|
UserQuoteBalance: userQuote,
|
||||||
EntryContract: entryContract,
|
EntryContract: entryContract,
|
||||||
}}, offset, nil
|
}
|
||||||
|
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||||
|
|
||||||
|
return []Swap{swap}, offset, nil
|
||||||
}
|
}
|
||||||
|
|||||||
168
raydiumv4.go
168
raydiumv4.go
@@ -1,11 +1,26 @@
|
|||||||
package pump_parser
|
package pump_parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
"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) {
|
func raydiumV4Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||||
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumV4Program) {
|
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])
|
return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 instruction not found, offset, %d, %d", offset[0], offset[1])
|
||||||
@@ -29,6 +44,8 @@ func raydiumV4Parser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
|||||||
return raydiumv4WithdrawPNLParser(tx, instruction, innerInstructions, offset)
|
return raydiumv4WithdrawPNLParser(tx, instruction, innerInstructions, offset)
|
||||||
case raydiumV4SwapBaseInDiscriminator, raydiumV4SwapBaseOutDiscriminator:
|
case raydiumV4SwapBaseInDiscriminator, raydiumV4SwapBaseOutDiscriminator:
|
||||||
return raydiumv4SwapParser(tx, instruction, innerInstructions, offset)
|
return raydiumv4SwapParser(tx, instruction, innerInstructions, offset)
|
||||||
|
case raydiumV4SwapBaseInV2Discriminator, raydiumV4SwapBaseOutV2Discriminator:
|
||||||
|
return raydiumv4SwapV2Parser(tx, instruction, innerInstructions, offset)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||||
@@ -312,6 +329,10 @@ func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions Inne
|
|||||||
vaultQuoteIdx = instruction.Accounts[6]
|
vaultQuoteIdx = instruction.Accounts[6]
|
||||||
}
|
}
|
||||||
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||||
|
amountSpecified, otherAmountThreshold, swapMode, err := decodeRaydiumV4SwapArgs(instruction.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, increaseOffset(offset), err
|
||||||
|
}
|
||||||
|
|
||||||
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
|
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
|
||||||
|
|
||||||
@@ -374,26 +395,129 @@ func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions Inne
|
|||||||
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
|
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
|
||||||
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
|
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
|
||||||
|
|
||||||
return []Swap{
|
swap := Swap{
|
||||||
{
|
Program: SolProgramRaydiumV4,
|
||||||
Program: SolProgramRaydiumV4,
|
Event: event,
|
||||||
Event: event,
|
Pool: ammAccount,
|
||||||
Pool: ammAccount,
|
BaseMint: baseTokenbalance.MintAccount,
|
||||||
BaseMint: baseTokenbalance.MintAccount,
|
QuoteMint: quoteTokenbalance.MintAccount,
|
||||||
QuoteMint: quoteTokenbalance.MintAccount,
|
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
|
||||||
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
|
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
|
||||||
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
|
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
|
||||||
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
|
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
|
||||||
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
|
User: user,
|
||||||
User: user,
|
BaseAmount: baseAmount,
|
||||||
BaseAmount: baseAmount,
|
QuoteAmount: quoteAmount,
|
||||||
QuoteAmount: quoteAmount,
|
BaseReserve: baseReserve,
|
||||||
BaseReserve: baseReserve,
|
QuoteReserve: quoteReserve,
|
||||||
QuoteReserve: quoteReserve,
|
Mayhem: false,
|
||||||
Mayhem: false,
|
UserBaseBalance: userBase,
|
||||||
UserBaseBalance: userBase,
|
UserQuoteBalance: userQuote,
|
||||||
UserQuoteBalance: userQuote,
|
EntryContract: entryContract,
|
||||||
EntryContract: entryContract,
|
}
|
||||||
},
|
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
|
||||||
}, offset, nil
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
14
system.go
14
system.go
@@ -34,12 +34,14 @@ func TransferParser(result *RawTx, instruction Instruction, offset [2]uint, tx *
|
|||||||
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]]
|
to := result.accountList[instruction.Accounts[1]]
|
||||||
|
|
||||||
if offset[1] == 0 {
|
if result.Meta.Err == nil {
|
||||||
tx.SolTransfer = append(tx.SolTransfer, SolTransfer{
|
if offset[1] == 0 {
|
||||||
From: from,
|
tx.SolTransfer = append(tx.SolTransfer, SolTransfer{
|
||||||
To: to,
|
From: from,
|
||||||
Amount: decimal.NewFromInt(int64(lamports)), // solana decimals
|
To: to,
|
||||||
})
|
Amount: decimal.NewFromInt(int64(lamports)), // solana decimals
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// load platform by to address
|
// load platform by to address
|
||||||
platform, ok := platformFeeAddresses[to]
|
platform, ok := platformFeeAddresses[to]
|
||||||
|
|||||||
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
60
tx.go
60
tx.go
@@ -12,6 +12,9 @@ type Swap struct {
|
|||||||
|
|
||||||
TxIndex int
|
TxIndex int
|
||||||
|
|
||||||
|
InstrIdx uint8
|
||||||
|
InnerIdx uint8
|
||||||
|
|
||||||
Pool solana.PublicKey
|
Pool solana.PublicKey
|
||||||
BaseMint solana.PublicKey
|
BaseMint solana.PublicKey
|
||||||
QuoteMint solana.PublicKey
|
QuoteMint solana.PublicKey
|
||||||
@@ -28,9 +31,22 @@ type Swap struct {
|
|||||||
BaseAmount decimal.Decimal
|
BaseAmount decimal.Decimal
|
||||||
QuoteAmount 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
|
BaseReserve decimal.Decimal
|
||||||
QuoteReserve decimal.Decimal
|
QuoteReserve decimal.Decimal
|
||||||
Mayhem bool
|
Mayhem bool
|
||||||
|
Cashback bool
|
||||||
|
|
||||||
UserBaseBalance decimal.Decimal
|
UserBaseBalance decimal.Decimal
|
||||||
UserQuoteBalance decimal.Decimal
|
UserQuoteBalance decimal.Decimal
|
||||||
@@ -44,16 +60,20 @@ type Swap struct {
|
|||||||
AfterSOLBalance decimal.Decimal
|
AfterSOLBalance decimal.Decimal
|
||||||
|
|
||||||
//For meteora dlmm
|
//For meteora dlmm
|
||||||
StartBinId int32
|
ActiveBinId int32
|
||||||
EndBinId int32
|
StartBinId int32
|
||||||
BinChanges []DlmmBinLiquidityChange
|
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
|
||||||
|
|
||||||
type DlmmBinLiquidityChange struct {
|
ConsumeUnit uint64
|
||||||
BinId int32
|
|
||||||
AmountX decimal.Decimal
|
|
||||||
AmountY decimal.Decimal
|
|
||||||
BpsToRemove uint16
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type platformInfo struct {
|
type platformInfo struct {
|
||||||
@@ -71,18 +91,23 @@ type SolTransfer struct {
|
|||||||
To solana.PublicKey
|
To solana.PublicKey
|
||||||
Amount decimal.Decimal
|
Amount decimal.Decimal
|
||||||
}
|
}
|
||||||
|
type ChainLink struct {
|
||||||
|
Timestamp int64
|
||||||
|
Price decimal.Decimal
|
||||||
|
}
|
||||||
|
|
||||||
type Tx struct {
|
type Tx struct {
|
||||||
rawTx *RawTx
|
rawTx *RawTx
|
||||||
Vote bool
|
Vote bool
|
||||||
Signer solana.PublicKey
|
Signer solana.PublicKey
|
||||||
Err interface{} `json:"err,omitempty"`
|
Err *TransactionParsedError `json:"err,omitempty"`
|
||||||
Swaps []Swap `json:"swaps,omitempty"`
|
Swaps []Swap `json:"swaps,omitempty"`
|
||||||
SolTransfer []SolTransfer `json:"sol_transfer,omitempty"`
|
SolTransfer []SolTransfer `json:"sol_transfer,omitempty"`
|
||||||
Block uint64 `json:"block"`
|
Block uint64 `json:"block"`
|
||||||
BlockIndex uint64 `json:"index"`
|
ChainLink ChainLink `json:"chain_link"`
|
||||||
TxHash *[64]byte `json:"-"`
|
BlockIndex uint64 `json:"index"`
|
||||||
BlockAt int64 `json:"block_at"`
|
TxHash *[64]byte `json:"-"`
|
||||||
|
BlockAt int64 `json:"block_at"`
|
||||||
|
|
||||||
CuFee decimal.Decimal `json:"cu_fee"`
|
CuFee decimal.Decimal `json:"cu_fee"`
|
||||||
|
|
||||||
@@ -98,6 +123,9 @@ type Tx struct {
|
|||||||
// update tokenInfo
|
// update tokenInfo
|
||||||
Token map[solana.PublicKey]TokenMeta `gorm:"-"`
|
Token map[solana.PublicKey]TokenMeta `gorm:"-"`
|
||||||
|
|
||||||
|
ComputeUnitsConsumed uint64 `json:"compute_units_consumed"`
|
||||||
|
CuLimit uint32 `json:"cu_limit"`
|
||||||
|
|
||||||
// todo pool info ??
|
// todo pool info ??
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2206
tx_binary.go
Normal file
2206
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)
|
||||||
|
}
|
||||||
903
tx_binary_test.go
Normal file
903
tx_binary_test.go
Normal file
@@ -0,0 +1,903 @@
|
|||||||
|
package pump_parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gagliardetto/solana-go"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTxBinaryRoundTrip(t *testing.T) {
|
||||||
|
txHash := [64]byte{}
|
||||||
|
for i := range txHash {
|
||||||
|
txHash[i] = byte(i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
original := &Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 123456789,
|
||||||
|
BlockIndex: 42,
|
||||||
|
TxHash: &txHash,
|
||||||
|
CuFee: decimal.NewFromInt(5000),
|
||||||
|
CUPrice: decimal.RequireFromString("0.123456"),
|
||||||
|
BeforeSolBalance: decimal.RequireFromString("1.500000000"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("1.234567890"),
|
||||||
|
ComputeUnitsConsumed: 345678,
|
||||||
|
CuLimit: 400000,
|
||||||
|
Platform: map[string]platformInfo{
|
||||||
|
PlatformGMGN: {
|
||||||
|
Platform: PlatformGMGN,
|
||||||
|
PlatformFee: decimal.RequireFromString("0.010000000"),
|
||||||
|
},
|
||||||
|
PlatformPhoton: {
|
||||||
|
Platform: PlatformPhoton,
|
||||||
|
PlatformFee: decimal.RequireFromString("0.020000000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MevAgent: map[string]mevInfo{
|
||||||
|
MevAgentJito: {
|
||||||
|
MevAgent: MevAgentJito,
|
||||||
|
MevAgentFee: decimal.RequireFromString("0.030000000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Swaps: []Swap{
|
||||||
|
{
|
||||||
|
Program: SolProgramPump,
|
||||||
|
Event: TxEventBuy,
|
||||||
|
TxIndex: 7,
|
||||||
|
InstrIdx: 2,
|
||||||
|
InnerIdx: 1,
|
||||||
|
Pool: mustPubKey("11111111111111111111111111111111"),
|
||||||
|
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
QuoteMint: solana.WrappedSol,
|
||||||
|
BaseTokenProgram: solana.TokenProgramID,
|
||||||
|
QuoteTokenProgram: solana.TokenProgramID,
|
||||||
|
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||||
|
BaseMintDecimals: 6,
|
||||||
|
QuoteMintDecimals: 9,
|
||||||
|
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
|
BaseAmount: decimal.NewFromInt(1200),
|
||||||
|
QuoteAmount: decimal.NewFromInt(3400),
|
||||||
|
SwapMode: SwapModeExactIn,
|
||||||
|
FixedAmount: decimal.NewFromInt(3400),
|
||||||
|
FixedAmountSide: SwapAmountSideQuote,
|
||||||
|
FixedMint: solana.WrappedSol,
|
||||||
|
LimitAmountType: SwapLimitTypeMinOut,
|
||||||
|
LimitAmount: decimal.NewFromInt(1000),
|
||||||
|
LimitAmountSide: SwapAmountSideBase,
|
||||||
|
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
ActualLimitAmount: decimal.NewFromInt(1200),
|
||||||
|
ActualLimitAmountSide: SwapAmountSideBase,
|
||||||
|
SlippageBps: decimal.RequireFromString("833.3333"),
|
||||||
|
BaseReserve: decimal.NewFromInt(5555),
|
||||||
|
QuoteReserve: decimal.NewFromInt(9999),
|
||||||
|
Mayhem: true,
|
||||||
|
Cashback: false,
|
||||||
|
UserBaseBalance: decimal.NewFromInt(777),
|
||||||
|
UserQuoteBalance: decimal.NewFromInt(888),
|
||||||
|
EntryContract: mustPubKey("ComputeBudget111111111111111111111111111111"),
|
||||||
|
MigrateToPool: mustPubKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
|
||||||
|
MigrateTopProgram: mustPubKey("AddressLookupTab1e1111111111111111111111111"),
|
||||||
|
LpMint: mustPubKey("4Nd1mJf8JQhRVTfJxW2YxXLNQKhPYo1JzN1u2KAPY1Hn"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.321000000"),
|
||||||
|
ActiveBinId: 11,
|
||||||
|
FeeAmount: decimal.NewFromInt(99),
|
||||||
|
FeeBps: "123",
|
||||||
|
FeeSide: "base",
|
||||||
|
ConsumeUnit: 9999,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := EncodeTxBinary(original)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := DecodeTxBinary(encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoded.Signer != original.Signer {
|
||||||
|
t.Fatalf("Signer = %s, want %s", decoded.Signer, original.Signer)
|
||||||
|
}
|
||||||
|
if decoded.Block != original.Block {
|
||||||
|
t.Fatalf("Block = %d, want %d", decoded.Block, original.Block)
|
||||||
|
}
|
||||||
|
if decoded.BlockIndex != original.BlockIndex {
|
||||||
|
t.Fatalf("BlockIndex = %d, want %d", decoded.BlockIndex, original.BlockIndex)
|
||||||
|
}
|
||||||
|
if decoded.TxHash == nil {
|
||||||
|
t.Fatal("TxHash = nil, want non-nil")
|
||||||
|
}
|
||||||
|
if *decoded.TxHash != *original.TxHash {
|
||||||
|
t.Fatalf("TxHash mismatch")
|
||||||
|
}
|
||||||
|
if !decoded.CuFee.Equal(original.CuFee) {
|
||||||
|
t.Fatalf("CuFee = %s, want %s", decoded.CuFee, original.CuFee)
|
||||||
|
}
|
||||||
|
if !decoded.CUPrice.Equal(original.CUPrice) {
|
||||||
|
t.Fatalf("CUPrice = %s, want %s", decoded.CUPrice, original.CUPrice)
|
||||||
|
}
|
||||||
|
if decoded.BeforeSolBalance.StringFixed(9) != original.BeforeSolBalance.StringFixed(9) {
|
||||||
|
t.Fatalf("BeforeSolBalance = %s, want %s", decoded.BeforeSolBalance, original.BeforeSolBalance)
|
||||||
|
}
|
||||||
|
if decoded.AfterSOLBalance.StringFixed(9) != original.AfterSOLBalance.StringFixed(9) {
|
||||||
|
t.Fatalf("AfterSOLBalance = %s, want %s", decoded.AfterSOLBalance, original.AfterSOLBalance)
|
||||||
|
}
|
||||||
|
if decoded.CuLimit != original.CuLimit {
|
||||||
|
t.Fatalf("CuLimit = %d, want %d", decoded.CuLimit, original.CuLimit)
|
||||||
|
}
|
||||||
|
if decoded.ComputeUnitsConsumed != original.ComputeUnitsConsumed {
|
||||||
|
t.Fatalf("ComputeUnitsConsumed = %d, want %d", decoded.ComputeUnitsConsumed, original.ComputeUnitsConsumed)
|
||||||
|
}
|
||||||
|
if len(decoded.Platform) != len(original.Platform) {
|
||||||
|
t.Fatalf("Platform len = %d, want %d", len(decoded.Platform), len(original.Platform))
|
||||||
|
}
|
||||||
|
if !decoded.Platform[PlatformGMGN].PlatformFee.Equal(original.Platform[PlatformGMGN].PlatformFee) {
|
||||||
|
t.Fatalf("Platform fee mismatch")
|
||||||
|
}
|
||||||
|
if len(decoded.MevAgent) != len(original.MevAgent) {
|
||||||
|
t.Fatalf("MevAgent len = %d, want %d", len(decoded.MevAgent), len(original.MevAgent))
|
||||||
|
}
|
||||||
|
if !decoded.MevAgent[MevAgentJito].MevAgentFee.Equal(original.MevAgent[MevAgentJito].MevAgentFee) {
|
||||||
|
t.Fatalf("MevAgent fee mismatch")
|
||||||
|
}
|
||||||
|
if len(decoded.Swaps) != 1 {
|
||||||
|
t.Fatalf("Swaps len = %d, want 1", len(decoded.Swaps))
|
||||||
|
}
|
||||||
|
|
||||||
|
swap := decoded.Swaps[0]
|
||||||
|
if swap.Program != original.Swaps[0].Program {
|
||||||
|
t.Fatalf("swap.Program = %s, want %s", swap.Program, original.Swaps[0].Program)
|
||||||
|
}
|
||||||
|
if swap.Event != original.Swaps[0].Event {
|
||||||
|
t.Fatalf("swap.Event = %s, want %s", swap.Event, original.Swaps[0].Event)
|
||||||
|
}
|
||||||
|
if swap.TxIndex != original.Swaps[0].TxIndex {
|
||||||
|
t.Fatalf("swap.TxIndex = %d, want %d", swap.TxIndex, original.Swaps[0].TxIndex)
|
||||||
|
}
|
||||||
|
if !swap.BaseAmount.Equal(original.Swaps[0].BaseAmount) {
|
||||||
|
t.Fatalf("swap.BaseAmount = %s, want %s", swap.BaseAmount, original.Swaps[0].BaseAmount)
|
||||||
|
}
|
||||||
|
if !swap.QuoteAmount.Equal(original.Swaps[0].QuoteAmount) {
|
||||||
|
t.Fatalf("swap.QuoteAmount = %s, want %s", swap.QuoteAmount, original.Swaps[0].QuoteAmount)
|
||||||
|
}
|
||||||
|
if !swap.FixedAmount.Equal(original.Swaps[0].FixedAmount) {
|
||||||
|
t.Fatalf("swap.FixedAmount = %s, want %s", swap.FixedAmount, original.Swaps[0].FixedAmount)
|
||||||
|
}
|
||||||
|
if !swap.LimitAmount.Equal(original.Swaps[0].LimitAmount) {
|
||||||
|
t.Fatalf("swap.LimitAmount = %s, want %s", swap.LimitAmount, original.Swaps[0].LimitAmount)
|
||||||
|
}
|
||||||
|
if !swap.ActualLimitAmount.Equal(original.Swaps[0].ActualLimitAmount) {
|
||||||
|
t.Fatalf("swap.ActualLimitAmount = %s, want %s", swap.ActualLimitAmount, original.Swaps[0].ActualLimitAmount)
|
||||||
|
}
|
||||||
|
if swap.SlippageBps.String() != "833" {
|
||||||
|
t.Fatalf("swap.SlippageBps = %s, want 833", swap.SlippageBps)
|
||||||
|
}
|
||||||
|
if !swap.BaseReserve.Equal(original.Swaps[0].BaseReserve) {
|
||||||
|
t.Fatalf("swap.BaseReserve = %s, want %s", swap.BaseReserve, original.Swaps[0].BaseReserve)
|
||||||
|
}
|
||||||
|
if !swap.QuoteReserve.Equal(original.Swaps[0].QuoteReserve) {
|
||||||
|
t.Fatalf("swap.QuoteReserve = %s, want %s", swap.QuoteReserve, original.Swaps[0].QuoteReserve)
|
||||||
|
}
|
||||||
|
if !swap.UserBaseBalance.Equal(original.Swaps[0].UserBaseBalance) {
|
||||||
|
t.Fatalf("swap.UserBaseBalance = %s, want %s", swap.UserBaseBalance, original.Swaps[0].UserBaseBalance)
|
||||||
|
}
|
||||||
|
if !swap.UserQuoteBalance.Equal(original.Swaps[0].UserQuoteBalance) {
|
||||||
|
t.Fatalf("swap.UserQuoteBalance = %s, want %s", swap.UserQuoteBalance, original.Swaps[0].UserQuoteBalance)
|
||||||
|
}
|
||||||
|
if swap.AfterSOLBalance.StringFixed(9) != original.Swaps[0].AfterSOLBalance.StringFixed(9) {
|
||||||
|
t.Fatalf("swap.AfterSOLBalance = %s, want %s", swap.AfterSOLBalance, original.Swaps[0].AfterSOLBalance)
|
||||||
|
}
|
||||||
|
|
||||||
|
if swap.ActiveBinId != 0 {
|
||||||
|
t.Fatalf("swap.ActiveBinId = %d, want 0", swap.ActiveBinId)
|
||||||
|
}
|
||||||
|
if !swap.FeeAmount.IsZero() {
|
||||||
|
t.Fatalf("swap.FeeAmount = %s, want 0", swap.FeeAmount)
|
||||||
|
}
|
||||||
|
if swap.FeeBps != "" {
|
||||||
|
t.Fatalf("swap.FeeBps = %q, want empty", swap.FeeBps)
|
||||||
|
}
|
||||||
|
if swap.FeeSide != "" {
|
||||||
|
t.Fatalf("swap.FeeSide = %q, want empty", swap.FeeSide)
|
||||||
|
}
|
||||||
|
if swap.ConsumeUnit != 0 {
|
||||||
|
t.Fatalf("swap.ConsumeUnit = %d, want 0", swap.ConsumeUnit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTxBinaryRejectsUnknownProgramEnum(t *testing.T) {
|
||||||
|
txBinary := &TxBinary{
|
||||||
|
SchemaVersion: txBinarySchemaVersionCurrent,
|
||||||
|
EnumVersion: txBinaryEnumVersionV1,
|
||||||
|
Swaps: []SwapBinary{
|
||||||
|
{Program: "unknown_program"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := txBinary.MarshalBinary(); err == nil {
|
||||||
|
t.Fatal("MarshalBinary() error = nil, want error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTxBinaryAcceptsKnownEventEnums(t *testing.T) {
|
||||||
|
events := []string{
|
||||||
|
TxEventAddLP,
|
||||||
|
TxEventRemoveLP,
|
||||||
|
TxEventBuy,
|
||||||
|
TxEventSell,
|
||||||
|
TxEventBuyFailed,
|
||||||
|
TxEventSellFailed,
|
||||||
|
TxEventBurn,
|
||||||
|
TxEventCreate,
|
||||||
|
TxEventComplete,
|
||||||
|
TxEventMigrate,
|
||||||
|
TxEventDeposit,
|
||||||
|
TxEventWithdraw,
|
||||||
|
TxEventOpen,
|
||||||
|
TxEventClose,
|
||||||
|
TxEventClaimFee,
|
||||||
|
TxEventAddLiquidity,
|
||||||
|
TxEventAddLiquidityOneSide,
|
||||||
|
TxEventRemoveLiquidity,
|
||||||
|
TxEventRemoveLiquidityOneSide,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, event := range events {
|
||||||
|
t.Run(event, func(t *testing.T) {
|
||||||
|
txBinary := &TxBinary{
|
||||||
|
SchemaVersion: txBinarySchemaVersionCurrent,
|
||||||
|
EnumVersion: txBinaryEnumVersionV1,
|
||||||
|
AddressTable: []solana.PublicKey{
|
||||||
|
mustPubKey("11111111111111111111111111111111"),
|
||||||
|
mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
solana.TokenProgramID,
|
||||||
|
mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||||
|
mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
|
},
|
||||||
|
Swaps: []SwapBinary{
|
||||||
|
{
|
||||||
|
Program: SolProgramPump,
|
||||||
|
Event: event,
|
||||||
|
Pool: 0,
|
||||||
|
BaseMint: 1,
|
||||||
|
QuoteMint: 1,
|
||||||
|
BaseTokenProgram: 2,
|
||||||
|
QuoteTokenProgram: 2,
|
||||||
|
Creator: 3,
|
||||||
|
User: 4,
|
||||||
|
FixedMint: 1,
|
||||||
|
LimitMint: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := txBinary.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MarshalBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded TxBinary
|
||||||
|
if err := decoded.UnmarshalBinary(encoded); err != nil {
|
||||||
|
t.Fatalf("UnmarshalBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
if got := decoded.Swaps[0].Event; got != event {
|
||||||
|
t.Fatalf("decoded event = %q, want %q", got, event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTxBinaryPreservesFractionalReserves(t *testing.T) {
|
||||||
|
tx := &Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 1,
|
||||||
|
BlockIndex: 1,
|
||||||
|
CuFee: decimal.NewFromInt(1),
|
||||||
|
CUPrice: decimal.RequireFromString("0.000001"),
|
||||||
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
|
||||||
|
ComputeUnitsConsumed: 1,
|
||||||
|
CuLimit: 1,
|
||||||
|
Swaps: []Swap{
|
||||||
|
{
|
||||||
|
Program: SolProgramMeteoraPools,
|
||||||
|
Event: TxEventAddLiquidity,
|
||||||
|
Pool: mustPubKey("11111111111111111111111111111111"),
|
||||||
|
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
QuoteMint: solana.WrappedSol,
|
||||||
|
BaseTokenProgram: solana.TokenProgramID,
|
||||||
|
QuoteTokenProgram: solana.TokenProgramID,
|
||||||
|
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||||
|
BaseMintDecimals: 6,
|
||||||
|
QuoteMintDecimals: 9,
|
||||||
|
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
|
BaseAmount: decimal.NewFromInt(10),
|
||||||
|
QuoteAmount: decimal.NewFromInt(20),
|
||||||
|
SwapMode: SwapModeExactIn,
|
||||||
|
FixedAmount: decimal.NewFromInt(20),
|
||||||
|
FixedAmountSide: SwapAmountSideQuote,
|
||||||
|
FixedMint: solana.WrappedSol,
|
||||||
|
LimitAmountType: SwapLimitTypeMinOut,
|
||||||
|
LimitAmount: decimal.NewFromInt(9),
|
||||||
|
LimitAmountSide: SwapAmountSideBase,
|
||||||
|
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
ActualLimitAmount: decimal.NewFromInt(10),
|
||||||
|
ActualLimitAmountSide: SwapAmountSideBase,
|
||||||
|
BaseReserve: decimal.RequireFromString("123.4"),
|
||||||
|
QuoteReserve: decimal.RequireFromString("710079483.625409498"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := EncodeTxBinary(tx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := DecodeTxBinary(encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
if got := decoded.Swaps[0].BaseReserve.String(); got != "123.4" {
|
||||||
|
t.Fatalf("BaseReserve = %s, want 123.4", got)
|
||||||
|
}
|
||||||
|
diff := decoded.Swaps[0].QuoteReserve.Sub(decimal.RequireFromString("710079483.625409498")).Abs()
|
||||||
|
if diff.GreaterThan(decimal.RequireFromString("0.0000001")) {
|
||||||
|
t.Fatalf("QuoteReserve diff = %s, want <= 0.0000001", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTxBinaryCanonicalizesOnSideEventAlias(t *testing.T) {
|
||||||
|
tx := &Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 1,
|
||||||
|
BlockIndex: 1,
|
||||||
|
CuFee: decimal.NewFromInt(1),
|
||||||
|
CUPrice: decimal.RequireFromString("0.000001"),
|
||||||
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
|
||||||
|
ComputeUnitsConsumed: 1,
|
||||||
|
CuLimit: 1,
|
||||||
|
Swaps: []Swap{
|
||||||
|
{
|
||||||
|
Program: SolProgramOrcaWhirPool,
|
||||||
|
Event: "remove_liquidity_on_side",
|
||||||
|
Pool: mustPubKey("11111111111111111111111111111111"),
|
||||||
|
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
QuoteMint: solana.WrappedSol,
|
||||||
|
BaseTokenProgram: solana.TokenProgramID,
|
||||||
|
QuoteTokenProgram: solana.TokenProgramID,
|
||||||
|
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||||
|
BaseMintDecimals: 6,
|
||||||
|
QuoteMintDecimals: 9,
|
||||||
|
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
|
BaseAmount: decimal.NewFromInt(10),
|
||||||
|
QuoteAmount: decimal.Zero,
|
||||||
|
SwapMode: SwapModeExactIn,
|
||||||
|
FixedAmount: decimal.NewFromInt(10),
|
||||||
|
FixedAmountSide: SwapAmountSideBase,
|
||||||
|
FixedMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
LimitAmountType: SwapLimitTypeMinOut,
|
||||||
|
LimitAmount: decimal.Zero,
|
||||||
|
LimitAmountSide: SwapAmountSideQuote,
|
||||||
|
ActualLimitAmount: decimal.Zero,
|
||||||
|
ActualLimitAmountSide: SwapAmountSideQuote,
|
||||||
|
BaseReserve: decimal.RequireFromString("123.4"),
|
||||||
|
QuoteReserve: decimal.RequireFromString("456.7"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := EncodeTxBinary(tx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := DecodeTxBinary(encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
if got := decoded.Swaps[0].Event; got != TxEventRemoveLiquidityOneSide {
|
||||||
|
t.Fatalf("Event = %q, want %q", got, TxEventRemoveLiquidityOneSide)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
|
||||||
|
tx1 := Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 1,
|
||||||
|
BlockIndex: 1,
|
||||||
|
CuFee: decimal.NewFromInt(1000),
|
||||||
|
CUPrice: decimal.RequireFromString("0.123456"),
|
||||||
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
|
||||||
|
ComputeUnitsConsumed: 100,
|
||||||
|
CuLimit: 200000,
|
||||||
|
Swaps: []Swap{
|
||||||
|
{
|
||||||
|
Program: SolProgramPump,
|
||||||
|
Event: TxEventBuy,
|
||||||
|
TxIndex: 1,
|
||||||
|
InstrIdx: 0,
|
||||||
|
InnerIdx: 0,
|
||||||
|
Pool: mustPubKey("11111111111111111111111111111111"),
|
||||||
|
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
QuoteMint: solana.WrappedSol,
|
||||||
|
BaseTokenProgram: solana.TokenProgramID,
|
||||||
|
QuoteTokenProgram: solana.TokenProgramID,
|
||||||
|
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||||
|
BaseMintDecimals: 6,
|
||||||
|
QuoteMintDecimals: 9,
|
||||||
|
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
|
BaseAmount: decimal.NewFromInt(10),
|
||||||
|
QuoteAmount: decimal.NewFromInt(20),
|
||||||
|
SwapMode: SwapModeExactIn,
|
||||||
|
FixedAmount: decimal.NewFromInt(20),
|
||||||
|
FixedAmountSide: SwapAmountSideQuote,
|
||||||
|
FixedMint: solana.WrappedSol,
|
||||||
|
LimitAmountType: SwapLimitTypeMinOut,
|
||||||
|
LimitAmount: decimal.NewFromInt(9),
|
||||||
|
LimitAmountSide: SwapAmountSideBase,
|
||||||
|
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
ActualLimitAmount: decimal.NewFromInt(10),
|
||||||
|
ActualLimitAmountSide: SwapAmountSideBase,
|
||||||
|
SlippageBps: decimal.RequireFromString("100.2"),
|
||||||
|
BaseReserve: decimal.NewFromInt(100),
|
||||||
|
QuoteReserve: decimal.NewFromInt(200),
|
||||||
|
UserBaseBalance: decimal.NewFromInt(1),
|
||||||
|
UserQuoteBalance: decimal.NewFromInt(2),
|
||||||
|
EntryContract: solana.PublicKey{},
|
||||||
|
MigrateToPool: solana.PublicKey{},
|
||||||
|
MigrateTopProgram: solana.PublicKey{},
|
||||||
|
LpMint: solana.PublicKey{},
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tx2 := tx1
|
||||||
|
tx2.Block = 2
|
||||||
|
tx2.BlockIndex = 2
|
||||||
|
tx2.CuFee = decimal.NewFromInt(2000)
|
||||||
|
tx2.AfterSOLBalance = decimal.RequireFromString("0.700000000")
|
||||||
|
tx2.Swaps = []Swap{tx1.Swaps[0]}
|
||||||
|
tx2.Swaps[0].TxIndex = 2
|
||||||
|
tx2.Swaps[0].BaseAmount = decimal.NewFromInt(30)
|
||||||
|
tx2.Swaps[0].QuoteAmount = decimal.NewFromInt(40)
|
||||||
|
|
||||||
|
batchEncoded, err := EncodeTxsBinary([]Tx{tx1, tx2})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
decoded, err := DecodeTxsBinary(batchEncoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxsBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(decoded) != 2 {
|
||||||
|
t.Fatalf("decoded len = %d, want 2", len(decoded))
|
||||||
|
}
|
||||||
|
if decoded[0].Signer != tx1.Signer || decoded[1].Signer != tx2.Signer {
|
||||||
|
t.Fatalf("decoded signer mismatch")
|
||||||
|
}
|
||||||
|
if decoded[0].Swaps[0].Pool != tx1.Swaps[0].Pool || decoded[1].Swaps[0].Pool != tx2.Swaps[0].Pool {
|
||||||
|
t.Fatalf("decoded shared address mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
single1, err := EncodeTxBinary(&tx1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxBinary(tx1) error = %v", err)
|
||||||
|
}
|
||||||
|
single2, err := EncodeTxBinary(&tx2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxBinary(tx2) error = %v", err)
|
||||||
|
}
|
||||||
|
if len(batchEncoded) >= len(single1)+len(single2) {
|
||||||
|
t.Fatalf("batch encoded = %d, want smaller than singles sum %d", len(batchEncoded), len(single1)+len(single2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeTxsBinaryReader(t *testing.T) {
|
||||||
|
tx1 := Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 100,
|
||||||
|
BlockIndex: 7,
|
||||||
|
CuFee: decimal.NewFromInt(111),
|
||||||
|
CUPrice: decimal.RequireFromString("0.123456"),
|
||||||
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.500000000"),
|
||||||
|
ComputeUnitsConsumed: 1234,
|
||||||
|
CuLimit: 250000,
|
||||||
|
Swaps: []Swap{
|
||||||
|
{
|
||||||
|
Program: SolProgramPump,
|
||||||
|
Event: TxEventBuy,
|
||||||
|
TxIndex: 3,
|
||||||
|
InstrIdx: 1,
|
||||||
|
InnerIdx: 2,
|
||||||
|
Pool: mustPubKey("11111111111111111111111111111111"),
|
||||||
|
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
QuoteMint: solana.WrappedSol,
|
||||||
|
BaseTokenProgram: solana.TokenProgramID,
|
||||||
|
QuoteTokenProgram: solana.TokenProgramID,
|
||||||
|
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||||
|
BaseMintDecimals: 6,
|
||||||
|
QuoteMintDecimals: 9,
|
||||||
|
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
|
BaseAmount: decimal.NewFromInt(100),
|
||||||
|
QuoteAmount: decimal.NewFromInt(200),
|
||||||
|
SwapMode: SwapModeExactIn,
|
||||||
|
FixedAmount: decimal.NewFromInt(200),
|
||||||
|
FixedAmountSide: SwapAmountSideQuote,
|
||||||
|
FixedMint: solana.WrappedSol,
|
||||||
|
LimitAmountType: SwapLimitTypeMinOut,
|
||||||
|
LimitAmount: decimal.NewFromInt(90),
|
||||||
|
LimitAmountSide: SwapAmountSideBase,
|
||||||
|
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
ActualLimitAmount: decimal.NewFromInt(100),
|
||||||
|
ActualLimitAmountSide: SwapAmountSideBase,
|
||||||
|
SlippageBps: decimal.RequireFromString("99.6"),
|
||||||
|
BaseReserve: decimal.NewFromInt(1000),
|
||||||
|
QuoteReserve: decimal.NewFromInt(2000),
|
||||||
|
UserBaseBalance: decimal.NewFromInt(10),
|
||||||
|
UserQuoteBalance: decimal.NewFromInt(20),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.400000000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tx2 := tx1
|
||||||
|
tx2.Block = 101
|
||||||
|
tx2.BlockIndex = 8
|
||||||
|
tx2.CuFee = decimal.NewFromInt(222)
|
||||||
|
tx2.AfterSOLBalance = decimal.RequireFromString("0.300000000")
|
||||||
|
tx2.Swaps = []Swap{tx1.Swaps[0]}
|
||||||
|
tx2.Swaps[0].TxIndex = 4
|
||||||
|
tx2.Swaps[0].BaseAmount = decimal.NewFromInt(300)
|
||||||
|
|
||||||
|
encoded, err := EncodeTxsBinary([]Tx{tx1, tx2})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded []*Tx
|
||||||
|
for tx, err := range DecodeTxsBinaryReader(bytes.NewReader(encoded)) {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxsBinaryReader() error = %v", err)
|
||||||
|
}
|
||||||
|
decoded = append(decoded, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(decoded) != 2 {
|
||||||
|
t.Fatalf("decoded len = %d, want 2", len(decoded))
|
||||||
|
}
|
||||||
|
if decoded[0].Signer != tx1.Signer || decoded[1].Signer != tx2.Signer {
|
||||||
|
t.Fatalf("decoded signer mismatch")
|
||||||
|
}
|
||||||
|
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
|
||||||
|
t.Fatalf("decoded block mismatch")
|
||||||
|
}
|
||||||
|
if decoded[0].Swaps[0].BaseAmount.Cmp(tx1.Swaps[0].BaseAmount) != 0 {
|
||||||
|
t.Fatalf("decoded tx1 swap base amount = %s, want %s", decoded[0].Swaps[0].BaseAmount, tx1.Swaps[0].BaseAmount)
|
||||||
|
}
|
||||||
|
if decoded[1].Swaps[0].BaseAmount.Cmp(tx2.Swaps[0].BaseAmount) != 0 {
|
||||||
|
t.Fatalf("decoded tx2 swap base amount = %s, want %s", decoded[1].Swaps[0].BaseAmount, tx2.Swaps[0].BaseAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeTxsBinaryReaderEarlyStop(t *testing.T) {
|
||||||
|
tx := Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 1,
|
||||||
|
BlockIndex: 1,
|
||||||
|
CuFee: decimal.NewFromInt(1),
|
||||||
|
CUPrice: decimal.RequireFromString("0.000001"),
|
||||||
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.999999999"),
|
||||||
|
ComputeUnitsConsumed: 1,
|
||||||
|
CuLimit: 1,
|
||||||
|
}
|
||||||
|
encoded, err := EncodeTxsBinary([]Tx{tx, tx, tx})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for decodedTx, err := range DecodeTxsBinaryReader(bytes.NewReader(encoded)) {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxsBinaryReader() error = %v", err)
|
||||||
|
}
|
||||||
|
if decodedTx == nil {
|
||||||
|
t.Fatal("decoded tx is nil")
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if count != 1 {
|
||||||
|
t.Fatalf("count = %d, want 1", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeTxsBinaryBytes(t *testing.T) {
|
||||||
|
tx1 := Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 11,
|
||||||
|
BlockIndex: 1,
|
||||||
|
CuFee: decimal.NewFromInt(10),
|
||||||
|
CUPrice: decimal.RequireFromString("0.000123"),
|
||||||
|
BeforeSolBalance: decimal.RequireFromString("1.100000000"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("1.000000000"),
|
||||||
|
ComputeUnitsConsumed: 10,
|
||||||
|
CuLimit: 100,
|
||||||
|
Swaps: []Swap{
|
||||||
|
{
|
||||||
|
Program: SolProgramPump,
|
||||||
|
Event: TxEventBuy,
|
||||||
|
TxIndex: 1,
|
||||||
|
Pool: mustPubKey("11111111111111111111111111111111"),
|
||||||
|
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
QuoteMint: solana.WrappedSol,
|
||||||
|
BaseTokenProgram: solana.TokenProgramID,
|
||||||
|
QuoteTokenProgram: solana.TokenProgramID,
|
||||||
|
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||||
|
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
|
FixedMint: solana.WrappedSol,
|
||||||
|
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||||
|
EntryContract: solana.PublicKey{},
|
||||||
|
MigrateToPool: solana.PublicKey{},
|
||||||
|
MigrateTopProgram: solana.PublicKey{},
|
||||||
|
LpMint: solana.PublicKey{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tx2 := Tx{
|
||||||
|
Signer: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||||
|
Block: 12,
|
||||||
|
BlockIndex: 2,
|
||||||
|
CuFee: decimal.NewFromInt(20),
|
||||||
|
CUPrice: decimal.RequireFromString("0.000456"),
|
||||||
|
BeforeSolBalance: decimal.RequireFromString("2.200000000"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("2.000000000"),
|
||||||
|
ComputeUnitsConsumed: 20,
|
||||||
|
CuLimit: 200,
|
||||||
|
Swaps: []Swap{
|
||||||
|
{
|
||||||
|
Program: SolProgramPump,
|
||||||
|
Event: TxEventSell,
|
||||||
|
TxIndex: 2,
|
||||||
|
Pool: mustPubKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
|
||||||
|
BaseMint: mustPubKey("4Nd1mJf8JQhRVTfJxW2YxXLNQKhPYo1JzN1u2KAPY1Hn"),
|
||||||
|
QuoteMint: solana.WrappedSol,
|
||||||
|
BaseTokenProgram: solana.TokenProgramID,
|
||||||
|
QuoteTokenProgram: solana.TokenProgramID,
|
||||||
|
Creator: mustPubKey("ComputeBudget111111111111111111111111111111"),
|
||||||
|
User: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
FixedMint: solana.WrappedSol,
|
||||||
|
LimitMint: mustPubKey("4Nd1mJf8JQhRVTfJxW2YxXLNQKhPYo1JzN1u2KAPY1Hn"),
|
||||||
|
EntryContract: solana.PublicKey{},
|
||||||
|
MigrateToPool: solana.PublicKey{},
|
||||||
|
MigrateTopProgram: solana.PublicKey{},
|
||||||
|
LpMint: solana.PublicKey{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
batch1, err := EncodeTxsBinary([]Tx{tx1})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary(batch1) error = %v", err)
|
||||||
|
}
|
||||||
|
batch2, err := EncodeTxsBinary([]Tx{tx2})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary(batch2) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
merged, err := MergeTxsBinaryBytes([][]byte{batch1, batch2})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MergeTxsBinaryBytes() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mergedBinary TxsBinary
|
||||||
|
if err := mergedBinary.UnmarshalBinary(merged); err != nil {
|
||||||
|
t.Fatalf("UnmarshalBinary(merged) error = %v", err)
|
||||||
|
}
|
||||||
|
if len(mergedBinary.Txs) != 2 {
|
||||||
|
t.Fatalf("merged tx count = %d, want 2", len(mergedBinary.Txs))
|
||||||
|
}
|
||||||
|
if len(mergedBinary.AddressTable) >= len(mustTxBinary(t, batch1).AddressTable)+len(mustTxBinary(t, batch2).AddressTable) {
|
||||||
|
t.Fatalf("merged address table was not deduplicated")
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := DecodeTxsBinary(merged)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxsBinary(merged) error = %v", err)
|
||||||
|
}
|
||||||
|
if len(decoded) != 2 {
|
||||||
|
t.Fatalf("decoded len = %d, want 2", len(decoded))
|
||||||
|
}
|
||||||
|
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
|
||||||
|
t.Fatalf("decoded block mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
|
||||||
|
tx1 := Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 21,
|
||||||
|
BlockIndex: 1,
|
||||||
|
CuFee: decimal.NewFromInt(1),
|
||||||
|
CUPrice: decimal.RequireFromString("0.000001"),
|
||||||
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
|
||||||
|
ComputeUnitsConsumed: 11,
|
||||||
|
CuLimit: 111,
|
||||||
|
}
|
||||||
|
tx2 := tx1
|
||||||
|
tx2.Block = 22
|
||||||
|
tx2.BlockIndex = 2
|
||||||
|
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
|
||||||
|
tx3 := tx1
|
||||||
|
tx3.Block = 23
|
||||||
|
tx3.BlockIndex = 3
|
||||||
|
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
|
||||||
|
|
||||||
|
batch1, err := EncodeTxsBinary([]Tx{tx1})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary(batch1) error = %v", err)
|
||||||
|
}
|
||||||
|
batch2, err := EncodeTxsBinary([]Tx{tx2})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary(batch2) error = %v", err)
|
||||||
|
}
|
||||||
|
batch3, err := EncodeTxsBinary([]Tx{tx3})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary(batch3) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
source1 := &testTxsBinarySource{
|
||||||
|
data: append(append([]byte{}, batch1...), batch2...),
|
||||||
|
}
|
||||||
|
source2 := &testTxsBinarySource{
|
||||||
|
data: batch3,
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err := MergeTxsBinarySourcesToWriter([]TxsBinaryReaderSource{source1, source2}, &out); err != nil {
|
||||||
|
t.Fatalf("MergeTxsBinarySourcesToWriter() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if source1.opens != 2 || source2.opens != 2 {
|
||||||
|
t.Fatalf("source opens = (%d, %d), want (2, 2)", source1.opens, source2.opens)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := DecodeTxsBinary(out.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxsBinary(merged) error = %v", err)
|
||||||
|
}
|
||||||
|
if len(decoded) != 3 {
|
||||||
|
t.Fatalf("decoded len = %d, want 3", len(decoded))
|
||||||
|
}
|
||||||
|
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block || decoded[2].Block != tx3.Block {
|
||||||
|
t.Fatalf("decoded block order mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) {
|
||||||
|
tx1 := Tx{
|
||||||
|
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||||
|
Block: 31,
|
||||||
|
BlockIndex: 1,
|
||||||
|
CuFee: decimal.NewFromInt(1),
|
||||||
|
CUPrice: decimal.RequireFromString("0.000001"),
|
||||||
|
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||||
|
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
|
||||||
|
ComputeUnitsConsumed: 11,
|
||||||
|
CuLimit: 111,
|
||||||
|
}
|
||||||
|
tx2 := tx1
|
||||||
|
tx2.Block = 32
|
||||||
|
tx2.BlockIndex = 2
|
||||||
|
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
|
||||||
|
tx3 := tx1
|
||||||
|
tx3.Block = 33
|
||||||
|
tx3.BlockIndex = 3
|
||||||
|
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
|
||||||
|
|
||||||
|
batch1, err := EncodeTxsBinary([]Tx{tx1})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary(batch1) error = %v", err)
|
||||||
|
}
|
||||||
|
batch2, err := EncodeTxsBinary([]Tx{tx2})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary(batch2) error = %v", err)
|
||||||
|
}
|
||||||
|
batch3, err := EncodeTxsBinary([]Tx{tx3})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncodeTxsBinary(batch3) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
source := &testTxsBinarySource{
|
||||||
|
data: append(
|
||||||
|
append(
|
||||||
|
append([]byte{}, testBatchHeader(false)...),
|
||||||
|
batch1...,
|
||||||
|
),
|
||||||
|
append(
|
||||||
|
append(testBatchHeader(true), batch2...),
|
||||||
|
append(testBatchHeader(false), batch3...)...,
|
||||||
|
)...,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
err = MergeTxsBinarySourcesToWriterWithOptions(
|
||||||
|
[]TxsBinaryReaderSource{source},
|
||||||
|
&out,
|
||||||
|
TxsBinaryMergeOptions{
|
||||||
|
BatchHeaderFunc: func(ctx *TxsBinaryBatchHeaderContext) (bool, error) {
|
||||||
|
header := make([]byte, 5)
|
||||||
|
if _, err := io.ReadFull(ctx.Reader, header); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(header[:4], []byte("BHDR")) {
|
||||||
|
return false, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
return header[4] == 1, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MergeTxsBinarySourcesToWriterWithOptions() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := DecodeTxsBinary(out.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeTxsBinary(merged) error = %v", err)
|
||||||
|
}
|
||||||
|
if len(decoded) != 2 {
|
||||||
|
t.Fatalf("decoded len = %d, want 2", len(decoded))
|
||||||
|
}
|
||||||
|
if decoded[0].Block != tx1.Block || decoded[1].Block != tx3.Block {
|
||||||
|
t.Fatalf("decoded block order mismatch after skip")
|
||||||
|
}
|
||||||
|
if source.opens != 2 {
|
||||||
|
t.Fatalf("source.opens = %d, want 2", source.opens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustPubKey(value string) solana.PublicKey {
|
||||||
|
return solana.MustPublicKeyFromBase58(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustTxBinary(t *testing.T, data []byte) *TxsBinary {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var txsBinary TxsBinary
|
||||||
|
if err := txsBinary.UnmarshalBinary(data); err != nil {
|
||||||
|
t.Fatalf("UnmarshalBinary() error = %v", err)
|
||||||
|
}
|
||||||
|
return &txsBinary
|
||||||
|
}
|
||||||
|
|
||||||
|
type testTxsBinarySource struct {
|
||||||
|
data []byte
|
||||||
|
opens int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testTxsBinarySource) OpenTxsBinaryReader() (io.ReadCloser, error) {
|
||||||
|
s.opens++
|
||||||
|
return io.NopCloser(bytes.NewReader(s.data)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBatchHeader(skip bool) []byte {
|
||||||
|
header := []byte("BHDR\x00")
|
||||||
|
if skip {
|
||||||
|
header[4] = 1
|
||||||
|
}
|
||||||
|
return header
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user