Compare commits
5 Commits
273e87b8ad
...
v0.2.44
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cd3a97d81 | ||
|
|
0a4aabc67f | ||
|
|
d46e8b651c | ||
|
|
43659ea4e4 | ||
|
|
6414e6a25f |
594
cmd/analyze_rawtx_binary_size/main.go
Normal file
594
cmd/analyze_rawtx_binary_size/main.go
Normal file
@@ -0,0 +1,594 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
pump_parser "github.com/thloyi/pump-parser"
|
||||
)
|
||||
|
||||
type sizeStats struct {
|
||||
total uint64
|
||||
items map[string]uint64
|
||||
}
|
||||
|
||||
type txInnerDataStat struct {
|
||||
TxOrdinal int
|
||||
BlockIndex int
|
||||
IndexWithinBlock uint32
|
||||
Slot uint64
|
||||
Bytes uint64
|
||||
InstructionCount int
|
||||
}
|
||||
|
||||
func newSizeStats() *sizeStats {
|
||||
return &sizeStats{items: make(map[string]uint64)}
|
||||
}
|
||||
|
||||
func (s *sizeStats) add(name string, n uint64) {
|
||||
s.items[name] += n
|
||||
s.total += n
|
||||
}
|
||||
|
||||
func main() {
|
||||
filePath := flag.String("file", "testdata/rawtx-binary/rawtx-blocks-414696178-414696182.prbs", "path to RawTxBlocksBinary .prbs file")
|
||||
flag.Parse()
|
||||
|
||||
raw, err := os.ReadFile(*filePath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "read file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var blocks pump_parser.RawTxBlocksBinary
|
||||
if err := blocks.UnmarshalBinary(raw); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "decode rawtx blocks binary: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stats := analyzeRawTxBlocksBinary(&blocks)
|
||||
if stats.total != uint64(len(raw)) {
|
||||
fmt.Fprintf(os.Stderr, "size accounting mismatch: accounted=%d file=%d\n", stats.total, len(raw))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
printReport(*filePath, len(raw), &blocks, stats)
|
||||
fmt.Println()
|
||||
printInnerInstructionDataDistribution(&blocks)
|
||||
fmt.Println()
|
||||
printBalanceAnalysis(&blocks)
|
||||
}
|
||||
|
||||
func analyzeRawTxBlocksBinary(blocks *pump_parser.RawTxBlocksBinary) *sizeStats {
|
||||
stats := newSizeStats()
|
||||
stats.add("file.magic", 4)
|
||||
stats.add("file.schema_version", 2)
|
||||
stats.add("address_table.count", 4)
|
||||
stats.add("address_table.pubkeys", uint64(len(blocks.AddressTable))*32)
|
||||
stats.add("blocks.count", 4)
|
||||
stats.add("blocks.block_time", uint64(len(blocks.BlockTimes))*8)
|
||||
stats.add("blocks.tx_count", uint64(len(blocks.BlockTxCounts))*4)
|
||||
stats.add("txs.total_count", 4)
|
||||
|
||||
for i := range blocks.Txs {
|
||||
addTx(stats, &blocks.Txs[i])
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
func addTx(stats *sizeStats, tx *pump_parser.RawTxBinary) {
|
||||
stats.add("tx.index_within_block", 4)
|
||||
stats.add("tx.slot", 8)
|
||||
stats.add("tx.version", 1)
|
||||
stats.add("tx.account_key_count", 4)
|
||||
stats.add("tx.account_list.count", 4)
|
||||
stats.add("tx.account_list.refs", uint64(len(tx.AccountList))*4)
|
||||
|
||||
addMeta(stats, &tx.Meta)
|
||||
addTransaction(stats, &tx.Transaction)
|
||||
}
|
||||
|
||||
func addMeta(stats *sizeStats, meta *pump_parser.RawTxMetaBinary) {
|
||||
addErr(stats, meta.Err)
|
||||
stats.add("meta.fee", 8)
|
||||
addInnerInstructions(stats, meta.InnerInstructions)
|
||||
addLamportBalances(stats, meta.PreBalances, meta.PostBalances)
|
||||
addTokenBalances(stats, "meta.token_balances", meta.TokenBalances)
|
||||
stats.add("meta.compute_units_consumed", 8)
|
||||
}
|
||||
|
||||
func addErr(stats *sizeStats, errValue *pump_parser.TransactionParsedError) {
|
||||
stats.add("meta.err.present", 1)
|
||||
if errValue == nil {
|
||||
return
|
||||
}
|
||||
stats.add("meta.err.index", 1)
|
||||
stats.add("meta.err.variant", 4)
|
||||
stats.add("meta.err.enum", 4)
|
||||
stats.add("meta.err.custom_code", 4)
|
||||
}
|
||||
|
||||
func addTransaction(stats *sizeStats, tx *pump_parser.RawTxTransactionBinary) {
|
||||
stats.add("transaction.signature.present", 1)
|
||||
if tx.HasSignature {
|
||||
stats.add("transaction.signature.first", 64)
|
||||
}
|
||||
addHeader(stats)
|
||||
addInstructions(stats, "transaction.instructions", tx.Message.Instructions)
|
||||
addAddressTableLookups(stats, tx.Message.AddressTableLookups)
|
||||
}
|
||||
|
||||
func addHeader(stats *sizeStats) {
|
||||
stats.add("transaction.header.num_readonly_signed", 4)
|
||||
stats.add("transaction.header.num_readonly_unsigned", 4)
|
||||
stats.add("transaction.header.num_required_signatures", 4)
|
||||
}
|
||||
|
||||
func addInnerInstructions(stats *sizeStats, values []pump_parser.InnerInstructions) {
|
||||
stats.add("meta.inner_instructions.count", 4)
|
||||
for _, value := range values {
|
||||
stats.add("meta.inner_instructions.index", 4)
|
||||
addInstructions(stats, "meta.inner_instructions.instructions", value.Instructions)
|
||||
}
|
||||
}
|
||||
|
||||
func addInstructions(stats *sizeStats, prefix string, values []pump_parser.Instruction) {
|
||||
stats.add(prefix+".count", 4)
|
||||
for _, value := range values {
|
||||
stats.add(prefix+".program_id_index", 1)
|
||||
stats.add(prefix+".accounts.count", 4)
|
||||
stats.add(prefix+".accounts.refs", uint64(len(value.Accounts)))
|
||||
stats.add(prefix+".data.length", 4)
|
||||
stats.add(prefix+".data.bytes", uint64(len(value.Data)))
|
||||
stats.add(prefix+".stack_height.present", 1)
|
||||
if value.StackHeight != nil {
|
||||
stats.add(prefix+".stack_height.value", 4)
|
||||
}
|
||||
stats.add(prefix+".log_events.count", 4)
|
||||
for _, event := range value.LogEvents {
|
||||
stats.add(prefix+".log_events.length", 4)
|
||||
stats.add(prefix+".log_events.bytes", uint64(len(event)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addAddressTableLookups(stats *sizeStats, values []pump_parser.RawTxAddressTableLookupBinary) {
|
||||
stats.add("transaction.address_table_lookups.count", 4)
|
||||
for _, value := range values {
|
||||
stats.add("transaction.address_table_lookups.account_key", 4)
|
||||
stats.add("transaction.address_table_lookups.writable.count", 4)
|
||||
stats.add("transaction.address_table_lookups.writable.indexes", uint64(len(value.WritableIndexes)))
|
||||
stats.add("transaction.address_table_lookups.readonly.count", 4)
|
||||
stats.add("transaction.address_table_lookups.readonly.indexes", uint64(len(value.ReadonlyIndexes)))
|
||||
}
|
||||
}
|
||||
|
||||
func addUint64Slice(stats *sizeStats, prefix string, count int) {
|
||||
stats.add(prefix+".count", 4)
|
||||
stats.add(prefix+".values", uint64(count)*8)
|
||||
}
|
||||
|
||||
func addLamportBalances(stats *sizeStats, preBalances []uint64, postBalances []uint64) {
|
||||
stats.add("meta.pre_balances.count_uvarint", uint64(uvarintLen(uint64(len(preBalances)))))
|
||||
for _, value := range preBalances {
|
||||
stats.add("meta.pre_balances.value_uvarint", uint64(uvarintLen(value)))
|
||||
}
|
||||
n := len(preBalances)
|
||||
if len(postBalances) < n {
|
||||
n = len(postBalances)
|
||||
}
|
||||
changed := 0
|
||||
for i := 0; i < n; i++ {
|
||||
if preBalances[i] != postBalances[i] {
|
||||
changed++
|
||||
}
|
||||
}
|
||||
stats.add("meta.post_balance_changes.count_uvarint", uint64(uvarintLen(uint64(changed))))
|
||||
for i := 0; i < n; i++ {
|
||||
if preBalances[i] == postBalances[i] {
|
||||
continue
|
||||
}
|
||||
stats.add("meta.post_balance_changes.index_uvarint", uint64(uvarintLen(uint64(i))))
|
||||
stats.add("meta.post_balance_changes.delta_uvarint", uint64(zigzagDeltaUvarintLen(preBalances[i], postBalances[i])))
|
||||
}
|
||||
}
|
||||
|
||||
func addTokenBalances(stats *sizeStats, prefix string, values []pump_parser.RawTxTokenBalanceBinary) {
|
||||
stats.add(prefix+".count", 4)
|
||||
for _, value := range values {
|
||||
stats.add(prefix+".account_index", 1)
|
||||
stats.add(prefix+".mint_ref", 1)
|
||||
stats.add(prefix+".owner.present", 1)
|
||||
if value.HasOwnerAccount {
|
||||
stats.add(prefix+".owner_ref", 1)
|
||||
}
|
||||
stats.add(prefix+".program_id_ref", 1)
|
||||
stats.add(prefix+".decimals", 1)
|
||||
stats.add(prefix+".pre_amount.present", 1)
|
||||
if value.HasPreAmount {
|
||||
stats.add(prefix+".pre_amount.length", 1)
|
||||
stats.add(prefix+".pre_amount.bytes", uint64(uint256ByteLen(value.PreAmount)))
|
||||
}
|
||||
stats.add(prefix+".post_amount.present", 1)
|
||||
if value.HasPostAmount {
|
||||
stats.add(prefix+".post_amount.length", 1)
|
||||
stats.add(prefix+".post_amount.bytes", uint64(uint256ByteLen(value.PostAmount)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func uint256ByteLen(value string) int {
|
||||
if value == "" || value == "0" {
|
||||
return 0
|
||||
}
|
||||
amount, ok := new(big.Int).SetString(value, 10)
|
||||
if !ok || amount.Sign() <= 0 {
|
||||
return 0
|
||||
}
|
||||
return len(amount.Bytes())
|
||||
}
|
||||
|
||||
func printReport(filePath string, fileSize int, blocks *pump_parser.RawTxBlocksBinary, stats *sizeStats) {
|
||||
type row struct {
|
||||
name string
|
||||
bytes uint64
|
||||
}
|
||||
rows := make([]row, 0, len(stats.items))
|
||||
for name, bytes := range stats.items {
|
||||
rows = append(rows, row{name: name, bytes: bytes})
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
if rows[i].bytes == rows[j].bytes {
|
||||
return rows[i].name < rows[j].name
|
||||
}
|
||||
return rows[i].bytes > rows[j].bytes
|
||||
})
|
||||
|
||||
fmt.Printf("file=%s\n", filePath)
|
||||
fmt.Printf("bytes=%d\n", fileSize)
|
||||
fmt.Printf("schema_version=%d\n", blocks.SchemaVersion)
|
||||
fmt.Printf("blocks=%d\n", len(blocks.BlockTxCounts))
|
||||
fmt.Printf("txs=%d\n", len(blocks.Txs))
|
||||
fmt.Printf("address_table_entries=%d\n", len(blocks.AddressTable))
|
||||
fmt.Println()
|
||||
fmt.Printf("%-56s %12s %8s\n", "field", "bytes", "pct")
|
||||
fmt.Printf("%-56s %12s %8s\n", "-----", "-----", "---")
|
||||
for _, row := range rows {
|
||||
fmt.Printf("%-56s %12d %7.2f%%\n", row.name, row.bytes, float64(row.bytes)*100/float64(fileSize))
|
||||
}
|
||||
}
|
||||
|
||||
func printInnerInstructionDataDistribution(blocks *pump_parser.RawTxBlocksBinary) {
|
||||
stats := collectInnerInstructionDataStats(blocks)
|
||||
values := make([]uint64, 0, len(stats))
|
||||
var total uint64
|
||||
var nonZero int
|
||||
for _, stat := range stats {
|
||||
values = append(values, stat.Bytes)
|
||||
total += stat.Bytes
|
||||
if stat.Bytes > 0 {
|
||||
nonZero++
|
||||
}
|
||||
}
|
||||
sort.Slice(values, func(i, j int) bool { return values[i] < values[j] })
|
||||
|
||||
fmt.Println("inner_instruction_data_bytes_per_tx")
|
||||
fmt.Printf("txs=%d nonzero_txs=%d total_bytes=%d avg=%.2f\n", len(stats), nonZero, total, avg(total, len(stats)))
|
||||
if len(values) > 0 {
|
||||
fmt.Printf("min=%d p50=%d p75=%d p90=%d p95=%d p99=%d max=%d\n",
|
||||
values[0],
|
||||
percentile(values, 0.50),
|
||||
percentile(values, 0.75),
|
||||
percentile(values, 0.90),
|
||||
percentile(values, 0.95),
|
||||
percentile(values, 0.99),
|
||||
values[len(values)-1],
|
||||
)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("%-16s %8s %8s\n", "bucket", "txs", "bytes")
|
||||
fmt.Printf("%-16s %8s %8s\n", "------", "---", "-----")
|
||||
for _, bucket := range innerDataBuckets(stats) {
|
||||
fmt.Printf("%-16s %8d %8d\n", bucket.label, bucket.count, bucket.bytes)
|
||||
}
|
||||
|
||||
sort.Slice(stats, func(i, j int) bool {
|
||||
if stats[i].Bytes == stats[j].Bytes {
|
||||
return stats[i].TxOrdinal < stats[j].TxOrdinal
|
||||
}
|
||||
return stats[i].Bytes > stats[j].Bytes
|
||||
})
|
||||
fmt.Println()
|
||||
fmt.Printf("%-6s %-5s %-8s %-12s %-8s %-10s\n", "rank", "block", "tx_index", "slot", "bytes", "inner_ix")
|
||||
fmt.Printf("%-6s %-5s %-8s %-12s %-8s %-10s\n", "----", "-----", "--------", "----", "-----", "--------")
|
||||
limit := 20
|
||||
if len(stats) < limit {
|
||||
limit = len(stats)
|
||||
}
|
||||
for i := 0; i < limit; i++ {
|
||||
stat := stats[i]
|
||||
fmt.Printf("%-6d %-5d %-8d %-12d %-8d %-10d\n", i+1, stat.BlockIndex, stat.IndexWithinBlock, stat.Slot, stat.Bytes, stat.InstructionCount)
|
||||
}
|
||||
}
|
||||
|
||||
func collectInnerInstructionDataStats(blocks *pump_parser.RawTxBlocksBinary) []txInnerDataStat {
|
||||
out := make([]txInnerDataStat, 0, len(blocks.Txs))
|
||||
txOffset := 0
|
||||
for blockIndex, count := range blocks.BlockTxCounts {
|
||||
for i := uint32(0); i < count; i++ {
|
||||
tx := &blocks.Txs[txOffset]
|
||||
var bytes uint64
|
||||
var instructionCount int
|
||||
for _, inner := range tx.Meta.InnerInstructions {
|
||||
for _, instruction := range inner.Instructions {
|
||||
bytes += uint64(len(instruction.Data))
|
||||
instructionCount++
|
||||
}
|
||||
}
|
||||
out = append(out, txInnerDataStat{
|
||||
TxOrdinal: txOffset,
|
||||
BlockIndex: blockIndex,
|
||||
IndexWithinBlock: tx.IndexWithinBlock,
|
||||
Slot: tx.Slot,
|
||||
Bytes: bytes,
|
||||
InstructionCount: instructionCount,
|
||||
})
|
||||
txOffset++
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func percentile(values []uint64, p float64) uint64 {
|
||||
if len(values) == 0 {
|
||||
return 0
|
||||
}
|
||||
idx := int(float64(len(values)-1) * p)
|
||||
return values[idx]
|
||||
}
|
||||
|
||||
func avg(total uint64, count int) float64 {
|
||||
if count == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(total) / float64(count)
|
||||
}
|
||||
|
||||
type innerDataBucket struct {
|
||||
label string
|
||||
count int
|
||||
bytes uint64
|
||||
}
|
||||
|
||||
func innerDataBuckets(stats []txInnerDataStat) []innerDataBucket {
|
||||
buckets := []innerDataBucket{
|
||||
{label: "0"},
|
||||
{label: "1-63"},
|
||||
{label: "64-127"},
|
||||
{label: "128-255"},
|
||||
{label: "256-511"},
|
||||
{label: "512-1023"},
|
||||
{label: "1024-2047"},
|
||||
{label: "2048-4095"},
|
||||
{label: "4096+"},
|
||||
}
|
||||
for _, stat := range stats {
|
||||
index := 0
|
||||
switch {
|
||||
case stat.Bytes == 0:
|
||||
index = 0
|
||||
case stat.Bytes < 64:
|
||||
index = 1
|
||||
case stat.Bytes < 128:
|
||||
index = 2
|
||||
case stat.Bytes < 256:
|
||||
index = 3
|
||||
case stat.Bytes < 512:
|
||||
index = 4
|
||||
case stat.Bytes < 1024:
|
||||
index = 5
|
||||
case stat.Bytes < 2048:
|
||||
index = 6
|
||||
case stat.Bytes < 4096:
|
||||
index = 7
|
||||
default:
|
||||
index = 8
|
||||
}
|
||||
buckets[index].count++
|
||||
buckets[index].bytes += stat.Bytes
|
||||
}
|
||||
return buckets
|
||||
}
|
||||
|
||||
type balanceValueStats struct {
|
||||
name string
|
||||
count int
|
||||
unique int
|
||||
zeroCount int
|
||||
topValues []balanceTopValue
|
||||
fixedBytes uint64
|
||||
uvarintBytes uint64
|
||||
duplicateCount int
|
||||
}
|
||||
|
||||
type balanceTopValue struct {
|
||||
value uint64
|
||||
count int
|
||||
}
|
||||
|
||||
type balancePairStats struct {
|
||||
txCount int
|
||||
pairCount int
|
||||
lengthMismatchTxs int
|
||||
unchangedCount int
|
||||
changedCount int
|
||||
currentFixedValueBytes uint64
|
||||
bothUvarintBytes uint64
|
||||
preUvarintPostDelta uint64
|
||||
preUvarintChangedDeltas uint64
|
||||
}
|
||||
|
||||
func printBalanceAnalysis(blocks *pump_parser.RawTxBlocksBinary) {
|
||||
preValues := make([]uint64, 0)
|
||||
postValues := make([]uint64, 0)
|
||||
pairs := balancePairStats{}
|
||||
pairs.txCount = len(blocks.Txs)
|
||||
|
||||
for _, tx := range blocks.Txs {
|
||||
preValues = append(preValues, tx.Meta.PreBalances...)
|
||||
postValues = append(postValues, tx.Meta.PostBalances...)
|
||||
preLen := len(tx.Meta.PreBalances)
|
||||
postLen := len(tx.Meta.PostBalances)
|
||||
if preLen != postLen {
|
||||
pairs.lengthMismatchTxs++
|
||||
}
|
||||
n := preLen
|
||||
if postLen < n {
|
||||
n = postLen
|
||||
}
|
||||
pairs.currentFixedValueBytes += uint64(preLen+postLen) * 8
|
||||
pairs.preUvarintChangedDeltas += uint64(uvarintLen(uint64(n)))
|
||||
for i := 0; i < preLen; i++ {
|
||||
pairs.bothUvarintBytes += uint64(uvarintLen(tx.Meta.PreBalances[i]))
|
||||
pairs.preUvarintPostDelta += uint64(uvarintLen(tx.Meta.PreBalances[i]))
|
||||
pairs.preUvarintChangedDeltas += uint64(uvarintLen(tx.Meta.PreBalances[i]))
|
||||
}
|
||||
for i := 0; i < postLen; i++ {
|
||||
pairs.bothUvarintBytes += uint64(uvarintLen(tx.Meta.PostBalances[i]))
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
pre := tx.Meta.PreBalances[i]
|
||||
post := tx.Meta.PostBalances[i]
|
||||
pairs.pairCount++
|
||||
pairs.preUvarintPostDelta += uint64(zigzagDeltaUvarintLen(pre, post))
|
||||
if pre == post {
|
||||
pairs.unchangedCount++
|
||||
continue
|
||||
}
|
||||
pairs.changedCount++
|
||||
pairs.preUvarintChangedDeltas += uint64(uvarintLen(uint64(i)))
|
||||
pairs.preUvarintChangedDeltas += uint64(zigzagDeltaUvarintLen(pre, post))
|
||||
}
|
||||
}
|
||||
|
||||
preStats := collectBalanceValueStats("pre_balances", preValues)
|
||||
postStats := collectBalanceValueStats("post_balances", postValues)
|
||||
combined := append(append([]uint64(nil), preValues...), postValues...)
|
||||
combinedStats := collectBalanceValueStats("pre+post_balances", combined)
|
||||
|
||||
fmt.Println("balance_values_analysis")
|
||||
printBalanceValueStats(preStats)
|
||||
printBalanceValueStats(postStats)
|
||||
printBalanceValueStats(combinedStats)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("balance_encoding_estimates")
|
||||
fmt.Printf("txs=%d pairs=%d length_mismatch_txs=%d unchanged_pairs=%d changed_pairs=%d unchanged_pct=%.2f%%\n",
|
||||
pairs.txCount,
|
||||
pairs.pairCount,
|
||||
pairs.lengthMismatchTxs,
|
||||
pairs.unchangedCount,
|
||||
pairs.changedCount,
|
||||
float64(pairs.unchangedCount)*100/float64(maxInt(pairs.pairCount, 1)),
|
||||
)
|
||||
printEstimate("current_fixed_uint64_values", pairs.currentFixedValueBytes, pairs.currentFixedValueBytes)
|
||||
printEstimate("both_values_uvarint", pairs.bothUvarintBytes, pairs.currentFixedValueBytes)
|
||||
printEstimate("pre_uvarint_post_delta_each_index", pairs.preUvarintPostDelta, pairs.currentFixedValueBytes)
|
||||
printEstimate("pre_uvarint_post_changed_delta_pairs", pairs.preUvarintChangedDeltas, pairs.currentFixedValueBytes)
|
||||
}
|
||||
|
||||
func collectBalanceValueStats(name string, values []uint64) balanceValueStats {
|
||||
freq := make(map[uint64]int)
|
||||
var zeroCount int
|
||||
var uvarintBytes uint64
|
||||
for _, value := range values {
|
||||
freq[value]++
|
||||
if value == 0 {
|
||||
zeroCount++
|
||||
}
|
||||
uvarintBytes += uint64(uvarintLen(value))
|
||||
}
|
||||
top := make([]balanceTopValue, 0, len(freq))
|
||||
var duplicateCount int
|
||||
for value, count := range freq {
|
||||
top = append(top, balanceTopValue{value: value, count: count})
|
||||
if count > 1 {
|
||||
duplicateCount += count - 1
|
||||
}
|
||||
}
|
||||
sort.Slice(top, func(i, j int) bool {
|
||||
if top[i].count == top[j].count {
|
||||
return top[i].value < top[j].value
|
||||
}
|
||||
return top[i].count > top[j].count
|
||||
})
|
||||
if len(top) > 10 {
|
||||
top = top[:10]
|
||||
}
|
||||
return balanceValueStats{
|
||||
name: name,
|
||||
count: len(values),
|
||||
unique: len(freq),
|
||||
zeroCount: zeroCount,
|
||||
topValues: top,
|
||||
fixedBytes: uint64(len(values)) * 8,
|
||||
uvarintBytes: uvarintBytes,
|
||||
duplicateCount: duplicateCount,
|
||||
}
|
||||
}
|
||||
|
||||
func printBalanceValueStats(stats balanceValueStats) {
|
||||
fmt.Printf("%s: count=%d unique=%d duplicate_values=%d zero=%d zero_pct=%.2f%% fixed_bytes=%d uvarint_bytes=%d uvarint_saved=%.2f%%\n",
|
||||
stats.name,
|
||||
stats.count,
|
||||
stats.unique,
|
||||
stats.duplicateCount,
|
||||
stats.zeroCount,
|
||||
float64(stats.zeroCount)*100/float64(maxInt(stats.count, 1)),
|
||||
stats.fixedBytes,
|
||||
stats.uvarintBytes,
|
||||
savedPct(stats.fixedBytes, stats.uvarintBytes),
|
||||
)
|
||||
fmt.Printf("%-22s %-8s %-8s\n", "value", "count", "pct")
|
||||
for _, item := range stats.topValues {
|
||||
fmt.Printf("%-22d %-8d %7.2f%%\n", item.value, item.count, float64(item.count)*100/float64(maxInt(stats.count, 1)))
|
||||
}
|
||||
}
|
||||
|
||||
func printEstimate(name string, bytes uint64, baseline uint64) {
|
||||
fmt.Printf("%-38s %10d saved=%7.2f%%\n", name, bytes, savedPct(baseline, bytes))
|
||||
}
|
||||
|
||||
func savedPct(baseline uint64, value uint64) float64 {
|
||||
if baseline == 0 {
|
||||
return 0
|
||||
}
|
||||
return (float64(baseline) - float64(value)) * 100 / float64(baseline)
|
||||
}
|
||||
|
||||
func uvarintLen(value uint64) int {
|
||||
n := 1
|
||||
for value >= 0x80 {
|
||||
value >>= 7
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func zigzagDeltaUvarintLen(pre uint64, post uint64) int {
|
||||
if post >= pre {
|
||||
return uvarintLen((post - pre) << 1)
|
||||
}
|
||||
return uvarintLen(((pre - post) << 1) - 1)
|
||||
}
|
||||
|
||||
func maxInt(a int, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
302
cmd/collect_yellowstone_rawtx_binary/main.go
Normal file
302
cmd/collect_yellowstone_rawtx_binary/main.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
pump_parser "github.com/thloyi/pump-parser"
|
||||
pb "go.onsig.ai/onsig/yellowstone-proto"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
type collector struct {
|
||||
endpoint string
|
||||
xToken string
|
||||
plaintext bool
|
||||
|
||||
blocks map[uint64][]pump_parser.RawTx
|
||||
seen map[string]struct{}
|
||||
|
||||
totalUpdates uint64
|
||||
txUpdates uint64
|
||||
savedNonVote uint64
|
||||
duplicates uint64
|
||||
voteFiltered uint64
|
||||
convertErrs uint64
|
||||
reconnects uint64
|
||||
|
||||
firstSlot uint64
|
||||
lastSlot uint64
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
endpoint = flag.String("endpoint", "ams.rpc.orbitflare.com:10000", "Yellowstone gRPC endpoint")
|
||||
xToken = flag.String("x-token", os.Getenv("YELLOWSTONE_X_TOKEN"), "Yellowstone x-token; defaults to YELLOWSTONE_X_TOKEN")
|
||||
duration = flag.Duration("duration", 5*time.Minute, "collection duration")
|
||||
output = flag.String("output", "", "output .prbs file path")
|
||||
plaintext = flag.Bool("plaintext", true, "use plaintext gRPC instead of TLS")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *xToken == "" {
|
||||
exitf("missing -x-token or YELLOWSTONE_X_TOKEN")
|
||||
}
|
||||
if *duration <= 0 {
|
||||
exitf("-duration must be positive")
|
||||
}
|
||||
if *output == "" {
|
||||
*output = filepath.Join("testdata", "rawtx-binary", fmt.Sprintf("rawtx-yellowstone-%s.prbs", time.Now().Format("20060102-150405")))
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
ctx, cancel := context.WithTimeout(ctx, *duration)
|
||||
defer cancel()
|
||||
|
||||
c := &collector{
|
||||
endpoint: *endpoint,
|
||||
xToken: *xToken,
|
||||
plaintext: *plaintext,
|
||||
blocks: make(map[uint64][]pump_parser.RawTx),
|
||||
seen: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
started := time.Now()
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.run(ctx)
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
exitf("%v", err)
|
||||
}
|
||||
break loop
|
||||
case <-ctx.Done():
|
||||
if err := <-done; err != nil {
|
||||
exitf("%v", err)
|
||||
}
|
||||
break loop
|
||||
case <-ticker.C:
|
||||
c.printProgress(started)
|
||||
}
|
||||
}
|
||||
|
||||
encoded, decodedCount, err := encodeAndVerify(c.blocks)
|
||||
if err != nil {
|
||||
exitf("raw tx binary encode/decode: %v", err)
|
||||
}
|
||||
if decodedCount != int(c.savedNonVote) {
|
||||
exitf("decoded tx count mismatch: got=%d want=%d", decodedCount, c.savedNonVote)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(*output), 0o755); err != nil {
|
||||
exitf("mkdir output dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(*output, encoded, 0o644); err != nil {
|
||||
exitf("write output: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("output=%s\n", *output)
|
||||
fmt.Printf("duration=%s elapsed=%s\n", *duration, time.Since(started).Truncate(time.Second))
|
||||
fmt.Printf("updates=%d tx_updates=%d converted_nonvote=%d duplicate=%d vote_filtered=%d convert_err=%d reconnects=%d\n",
|
||||
c.totalUpdates, c.txUpdates, c.savedNonVote, c.duplicates, c.voteFiltered, c.convertErrs, c.reconnects)
|
||||
fmt.Printf("slots=%d first_slot=%d last_slot=%d decoded=%d\n", len(c.blocks), c.firstSlot, c.lastSlot, decodedCount)
|
||||
fmt.Printf("bytes=%d bytes_per_tx=%.2f\n", len(encoded), float64(len(encoded))/float64(max(int(c.savedNonVote), 1)))
|
||||
}
|
||||
|
||||
func (c *collector) run(ctx context.Context) error {
|
||||
for ctx.Err() == nil {
|
||||
if err := c.recvOnce(ctx); err != nil && ctx.Err() == nil {
|
||||
c.reconnects++
|
||||
fmt.Fprintf(os.Stderr, "stream_err reconnect=%d err=%v\n", c.reconnects, err)
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
if ctx.Err() == nil {
|
||||
c.reconnects++
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *collector) recvOnce(ctx context.Context) error {
|
||||
conn, err := grpc.NewClient(
|
||||
c.endpoint,
|
||||
c.transportOption(),
|
||||
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: 10 * time.Second,
|
||||
Timeout: time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}),
|
||||
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(64*1024*1024)),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{"x-token": c.xToken}))
|
||||
stream, err := pb.NewGeyserClient(conn).Subscribe(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vote := false
|
||||
subscription := &pb.SubscribeRequest{
|
||||
Transactions: map[string]*pb.SubscribeRequestFilterTransactions{
|
||||
"nonvote": {Vote: &vote},
|
||||
},
|
||||
}
|
||||
if err := stream.Send(subscription); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF || ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
c.totalUpdates++
|
||||
txn := resp.GetTransaction()
|
||||
if txn == nil {
|
||||
continue
|
||||
}
|
||||
c.txUpdates++
|
||||
|
||||
created := time.Now().Unix()
|
||||
if resp.GetCreatedAt() != nil {
|
||||
created = resp.GetCreatedAt().Seconds
|
||||
}
|
||||
rawTx, err := pump_parser.ConvertYellowstoneGrpcTransactionToSolanaTransaction(txn, created)
|
||||
if err != nil {
|
||||
c.convertErrs++
|
||||
continue
|
||||
}
|
||||
txHash := rawTx.TxHash()
|
||||
if txHash != "" {
|
||||
if _, exists := c.seen[txHash]; exists {
|
||||
c.duplicates++
|
||||
continue
|
||||
}
|
||||
c.seen[txHash] = struct{}{}
|
||||
}
|
||||
if c.firstSlot == 0 || rawTx.Slot < c.firstSlot {
|
||||
c.firstSlot = rawTx.Slot
|
||||
}
|
||||
if rawTx.Slot > c.lastSlot {
|
||||
c.lastSlot = rawTx.Slot
|
||||
}
|
||||
if isVoteTx(rawTx) {
|
||||
c.voteFiltered++
|
||||
continue
|
||||
}
|
||||
c.blocks[rawTx.Slot] = append(c.blocks[rawTx.Slot], *rawTx)
|
||||
c.savedNonVote++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *collector) transportOption() grpc.DialOption {
|
||||
if c.plaintext {
|
||||
return grpc.WithTransportCredentials(insecure.NewCredentials())
|
||||
}
|
||||
return grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, ""))
|
||||
}
|
||||
|
||||
func (c *collector) printProgress(started time.Time) {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"progress elapsed=%s updates=%d tx_updates=%d saved_nonvote=%d slots=%d duplicate=%d vote_filtered=%d convert_err=%d reconnects=%d\n",
|
||||
time.Since(started).Truncate(time.Second),
|
||||
c.totalUpdates,
|
||||
c.txUpdates,
|
||||
c.savedNonVote,
|
||||
len(c.blocks),
|
||||
c.duplicates,
|
||||
c.voteFiltered,
|
||||
c.convertErrs,
|
||||
c.reconnects,
|
||||
)
|
||||
}
|
||||
|
||||
func encodeAndVerify(blocks map[uint64][]pump_parser.RawTx) ([]byte, int, error) {
|
||||
slots := make([]uint64, 0, len(blocks))
|
||||
for slot := range blocks {
|
||||
slots = append(slots, slot)
|
||||
}
|
||||
sort.Slice(slots, func(i, j int) bool { return slots[i] < slots[j] })
|
||||
|
||||
ordered := make([][]pump_parser.RawTx, 0, len(slots))
|
||||
for _, slot := range slots {
|
||||
txs := blocks[slot]
|
||||
blockTime := int64(0)
|
||||
if len(txs) > 0 {
|
||||
blockTime = txs[0].BlockTime
|
||||
}
|
||||
for i := range txs {
|
||||
txs[i].BlockTime = blockTime
|
||||
}
|
||||
ordered = append(ordered, txs)
|
||||
}
|
||||
|
||||
encoded, err := pump_parser.EncodeRawTxBlocksBinary(ordered)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
decoded, err := pump_parser.DecodeRawTxBlocksBinary(encoded)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
decodedCount := 0
|
||||
for _, block := range decoded {
|
||||
decodedCount += len(block)
|
||||
}
|
||||
return encoded, decodedCount, nil
|
||||
}
|
||||
|
||||
func isVoteTx(tx *pump_parser.RawTx) bool {
|
||||
if tx == nil {
|
||||
return false
|
||||
}
|
||||
accountList := tx.GetAccountList()
|
||||
for _, instr := range tx.Transaction.Message.Instructions {
|
||||
if instr.ProgramIDIndex >= 0 && instr.ProgramIDIndex < len(accountList) && accountList[instr.ProgramIDIndex] == solana.VoteProgramID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func exitf(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -14,7 +14,7 @@ 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"
|
||||
txHash = "29v7u2ewLr3Se6cWYC2xwN8jszqMWwvVgPz7MqkctTveMo1csWWYDBcUsjuJwb5ciugc5so1jc9QcmR7syJTjEns"
|
||||
}
|
||||
|
||||
if txHash == "" {
|
||||
|
||||
7
meta.go
7
meta.go
@@ -20,12 +20,16 @@ var mayhemFeeAccounts = []solana.PublicKey{
|
||||
|
||||
var pumpGetFeesDiscriminator = calculateDiscriminator("global:get_fees")
|
||||
var pumpBuyDiscriminator = calculateDiscriminator("global:buy")
|
||||
var pumpBuyV2Discriminator = calculateDiscriminator("global:buy_exact_sol_in")
|
||||
var pumpBuyExactSolInDiscriminator = calculateDiscriminator("global:buy_exact_sol_in")
|
||||
var pumpBuyV2Discriminator = calculateDiscriminator("global:buy_v2")
|
||||
var pumpBuyExactQuoteInV2Discriminator = calculateDiscriminator("global:buy_exact_quote_in_v2")
|
||||
var pumpSellDiscriminator = calculateDiscriminator("global:sell")
|
||||
var pumpSellV2Discriminator = calculateDiscriminator("global:sell_v2")
|
||||
var pumpCreateDiscriminator = calculateDiscriminator("global:create")
|
||||
var pumpCreateV2Discriminator = calculateDiscriminator("global:create_v2")
|
||||
var pumpAdminSetCreatorDiscriminator = calculateDiscriminator("global:admin_set_creator")
|
||||
var pumpMigrateDiscriminator = calculateDiscriminator("global:migrate")
|
||||
var pumpMigrateV2Discriminator = calculateDiscriminator("global:migrate_v2")
|
||||
|
||||
var pumpEventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29}
|
||||
var pumpTradeEventDiscriminator = [16]byte{228, 69, 165, 46, 81, 203, 154, 29, 189, 219, 127, 211, 78, 230, 97, 238}
|
||||
@@ -132,6 +136,7 @@ var (
|
||||
metaoraPoolRemoveBalanceLiquidityDiscriminator = calculateDiscriminator("global:remove_balance_liquidity")
|
||||
metaoraPoolClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee")
|
||||
metaoraPoolBootstrapLiquidityDiscriminator = calculateDiscriminator("global:bootstrap_liquidity")
|
||||
metaoraPoolSwapEventDiscriminator = calculateDiscriminator("event:Swap")
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -2006,16 +2006,12 @@ func resolveDlmmSwapAccounts(result *RawTx, accounts []int) (dlmmSwapAccounts, e
|
||||
if eventAuthorityPos < len(accounts) && accountList[accounts[eventAuthorityPos]].Equals(solana.MemoProgramID) {
|
||||
eventAuthorityPos++
|
||||
}
|
||||
programPos := eventAuthorityPos + 1
|
||||
if programPos >= len(accounts) {
|
||||
if eventAuthorityPos >= len(accounts) {
|
||||
continue
|
||||
}
|
||||
if !accountList[accounts[eventAuthorityPos]].Equals(meteoraDlmmEventAuthority) {
|
||||
continue
|
||||
}
|
||||
if !accountList[accounts[programPos]].Equals(meteoraDlmmProgram) {
|
||||
continue
|
||||
}
|
||||
|
||||
if hostFeePresent && oraclePos+1 < len(accounts) && dlmmIsSigner(result, accounts[oraclePos+1]) {
|
||||
continue
|
||||
|
||||
@@ -222,6 +222,50 @@ func TestDlmmDecodeLbPairCreateEvent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDlmmSwapAccountsAllowsRemainingAccountsAfterEventAuthority(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
accountList := make([]solana.PublicKey, 40)
|
||||
for i := range accountList {
|
||||
accountList[i] = testPublicKey(byte(i + 1))
|
||||
}
|
||||
accountList[0] = testPublicKey(200)
|
||||
accountList[26] = meteoraDlmmProgram
|
||||
accountList[27] = solana.MemoProgramID
|
||||
accountList[29] = solana.TokenProgramID
|
||||
accountList[33] = meteoraDlmmEventAuthority
|
||||
|
||||
rawTx := &RawTx{
|
||||
accountList: accountList,
|
||||
Transaction: Transaction{
|
||||
Message: Message{
|
||||
AccountKeys: accountList[:11],
|
||||
Header: Header{
|
||||
NumRequiredSignatures: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
accounts := []int{13, 26, 16, 14, 11, 4, 35, 28, 15, 26, 0, 29, 29, 27, 33, 29, 3, 7, 2}
|
||||
|
||||
resolved, err := resolveDlmmSwapAccounts(rawTx, accounts)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveDlmmSwapAccounts() error = %v", err)
|
||||
}
|
||||
if resolved.poolIdx != 13 {
|
||||
t.Fatalf("poolIdx = %d, want 13", resolved.poolIdx)
|
||||
}
|
||||
if resolved.reserveXIdx != 16 || resolved.reserveYIdx != 14 {
|
||||
t.Fatalf("reserve indexes = %d/%d, want 16/14", resolved.reserveXIdx, resolved.reserveYIdx)
|
||||
}
|
||||
if resolved.userIdx != 0 {
|
||||
t.Fatalf("userIdx = %d, want 0", resolved.userIdx)
|
||||
}
|
||||
if resolved.tokenXProgramIdx != 29 || resolved.tokenYProgramIdx != 29 {
|
||||
t.Fatalf("token program indexes = %d/%d, want 29/29", resolved.tokenXProgramIdx, resolved.tokenYProgramIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeteoraDlmmInitializeParserUsesLbPairCreateEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
196
metaorapool.go
196
metaorapool.go
@@ -2,8 +2,10 @@ package pump_parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
agbinary "github.com/gagliardetto/binary"
|
||||
"github.com/gagliardetto/solana-go"
|
||||
@@ -20,6 +22,14 @@ type metaoraPoolSwapArgs struct {
|
||||
MinimumOutAmount uint64
|
||||
}
|
||||
|
||||
type metaoraPoolSwapEvent struct {
|
||||
InAmount uint64
|
||||
OutAmount uint64
|
||||
TradeFee uint64
|
||||
ProtocolFee uint64
|
||||
HostFee uint64
|
||||
}
|
||||
|
||||
var (
|
||||
meteoraVaultProgram = solana.MustPublicKeyFromBase58("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi")
|
||||
meteoraVaultDepositDiscriminator = []byte{0xf2, 0x23, 0xc6, 0x89, 0x52, 0xe1, 0xf2, 0xb6}
|
||||
@@ -731,6 +741,7 @@ func metaoraPoolRemoveLiquidity(tx *Tx, instruction Instruction, innerInstructio
|
||||
}
|
||||
|
||||
func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||
swapOffset := offset
|
||||
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)
|
||||
@@ -886,10 +897,195 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
EntryContract: entryContract,
|
||||
},
|
||||
}
|
||||
fixedSide := fixedSwapAmountSide(event, SwapModeExactIn)
|
||||
limitSide := oppositeSwapAmountSide(fixedSide)
|
||||
if fixedSide == SwapAmountSideUnknown || limitSide == SwapAmountSideUnknown {
|
||||
swaps[0].SetSwapAmountInfo(
|
||||
SwapModeExactIn,
|
||||
decimal.NewFromUint64(args.InAmount),
|
||||
decimal.NewFromUint64(args.MinimumOutAmount),
|
||||
)
|
||||
return swaps, offset, nil
|
||||
}
|
||||
|
||||
actualLimitAmount := swapAmountForSide(baseAmount, quoteAmount, limitSide)
|
||||
if swapEvent, ok := metaoraPoolSwapEventFromInstruction(instruction); ok {
|
||||
actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount)
|
||||
} else if swapEvent, ok := metaoraPoolSwapEventForOffset(tx, swapOffset); ok {
|
||||
actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount)
|
||||
}
|
||||
swaps[0].SetSwapAmountInfoDetailed(
|
||||
SwapModeExactIn,
|
||||
decimal.NewFromUint64(args.InAmount),
|
||||
fixedSide,
|
||||
swapMintForSide(baseMint, quoteMint, fixedSide),
|
||||
SwapLimitTypeMinOut,
|
||||
decimal.NewFromUint64(args.MinimumOutAmount),
|
||||
limitSide,
|
||||
swapMintForSide(baseMint, quoteMint, limitSide),
|
||||
actualLimitAmount,
|
||||
)
|
||||
return swaps, offset, nil
|
||||
}
|
||||
|
||||
func metaoraPoolSwapEventFromInstruction(instruction Instruction) (metaoraPoolSwapEvent, bool) {
|
||||
for _, event := range instruction.LogEvents {
|
||||
if swapEvent, ok := metaoraPoolDecodeSwapEventData(event); ok {
|
||||
return swapEvent, true
|
||||
}
|
||||
}
|
||||
return metaoraPoolSwapEvent{}, false
|
||||
}
|
||||
|
||||
func metaoraPoolSwapEventForOffset(tx *Tx, offset [2]uint) (metaoraPoolSwapEvent, bool) {
|
||||
if tx == nil || tx.rawTx == nil {
|
||||
return metaoraPoolSwapEvent{}, false
|
||||
}
|
||||
occurrence := metaoraPoolSwapInstructionOccurrence(tx.rawTx, offset)
|
||||
if occurrence == 0 {
|
||||
return metaoraPoolSwapEvent{}, false
|
||||
}
|
||||
return metaoraPoolSwapEventFromLogs(tx.rawTx.Meta.LogMessages, occurrence)
|
||||
}
|
||||
|
||||
func metaoraPoolSwapInstructionOccurrence(rawTx *RawTx, offset [2]uint) int {
|
||||
if rawTx == nil {
|
||||
return 0
|
||||
}
|
||||
accountList := rawTx.getAccountList()
|
||||
innerByOuter := make(map[int]InnerInstructions, len(rawTx.Meta.InnerInstructions))
|
||||
for _, inner := range rawTx.Meta.InnerInstructions {
|
||||
innerByOuter[inner.Index] = inner
|
||||
}
|
||||
|
||||
occurrence := 0
|
||||
for i, instruction := range rawTx.Transaction.Message.Instructions {
|
||||
if uint(i) == offset[0] && offset[1] == 0 {
|
||||
if metaoraPoolIsSwapInstruction(accountList, instruction) {
|
||||
return occurrence + 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if metaoraPoolIsSwapInstruction(accountList, instruction) {
|
||||
occurrence++
|
||||
}
|
||||
|
||||
inner := innerByOuter[i]
|
||||
for j, instruction := range inner.Instructions {
|
||||
innerOffset := uint(j + 1)
|
||||
if uint(i) == offset[0] && offset[1] == innerOffset {
|
||||
if metaoraPoolIsSwapInstruction(accountList, instruction) {
|
||||
return occurrence + 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if metaoraPoolIsSwapInstruction(accountList, instruction) {
|
||||
occurrence++
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func metaoraPoolIsSwapInstruction(accountList []solana.PublicKey, instruction Instruction) bool {
|
||||
if instruction.ProgramIDIndex < 0 || instruction.ProgramIDIndex >= len(accountList) {
|
||||
return false
|
||||
}
|
||||
if !accountList[instruction.ProgramIDIndex].Equals(metaoraPoolProgramID) {
|
||||
return false
|
||||
}
|
||||
return len(instruction.Data) >= 8 && bytes.Equal(instruction.Data[:8], metaoraPoolSwapDiscriminator[:])
|
||||
}
|
||||
|
||||
func metaoraPoolSwapEventFromLogs(logMessages []string, occurrence int) (metaoraPoolSwapEvent, bool) {
|
||||
if occurrence <= 0 {
|
||||
return metaoraPoolSwapEvent{}, false
|
||||
}
|
||||
|
||||
type frame struct {
|
||||
program string
|
||||
sawSwap bool
|
||||
}
|
||||
|
||||
targetProgram := metaoraPoolProgramID.String()
|
||||
var stack []frame
|
||||
seen := 0
|
||||
for _, logMessage := range logMessages {
|
||||
if program, ok := metaoraPoolLogInvokeProgram(logMessage); ok {
|
||||
stack = append(stack, frame{program: program})
|
||||
continue
|
||||
}
|
||||
|
||||
if len(stack) > 0 && stack[len(stack)-1].program == targetProgram {
|
||||
if logMessage == "Program log: Instruction: Swap" {
|
||||
stack[len(stack)-1].sawSwap = true
|
||||
continue
|
||||
}
|
||||
if stack[len(stack)-1].sawSwap && strings.HasPrefix(logMessage, "Program data: ") {
|
||||
event, ok := metaoraPoolDecodeSwapEventLog(strings.TrimSpace(strings.TrimPrefix(logMessage, "Program data: ")))
|
||||
if ok {
|
||||
seen++
|
||||
if seen == occurrence {
|
||||
return event, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if program, ok := metaoraPoolLogFinishedProgram(logMessage); ok {
|
||||
for i := len(stack) - 1; i >= 0; i-- {
|
||||
if stack[i].program == program {
|
||||
stack = stack[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return metaoraPoolSwapEvent{}, false
|
||||
}
|
||||
|
||||
func metaoraPoolLogInvokeProgram(logMessage string) (string, bool) {
|
||||
if !strings.HasPrefix(logMessage, "Program ") || !strings.Contains(logMessage, " invoke [") {
|
||||
return "", false
|
||||
}
|
||||
rest := strings.TrimPrefix(logMessage, "Program ")
|
||||
program, _, ok := strings.Cut(rest, " ")
|
||||
return program, ok
|
||||
}
|
||||
|
||||
func metaoraPoolLogFinishedProgram(logMessage string) (string, bool) {
|
||||
if !strings.HasPrefix(logMessage, "Program ") {
|
||||
return "", false
|
||||
}
|
||||
if !strings.HasSuffix(logMessage, " success") && !strings.Contains(logMessage, " failed:") {
|
||||
return "", false
|
||||
}
|
||||
rest := strings.TrimPrefix(logMessage, "Program ")
|
||||
program, _, ok := strings.Cut(rest, " ")
|
||||
return program, ok
|
||||
}
|
||||
|
||||
func metaoraPoolDecodeSwapEventLog(encoded string) (metaoraPoolSwapEvent, bool) {
|
||||
data, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return metaoraPoolSwapEvent{}, false
|
||||
}
|
||||
return metaoraPoolDecodeSwapEventData(data)
|
||||
}
|
||||
|
||||
func metaoraPoolDecodeSwapEventData(data []byte) (metaoraPoolSwapEvent, bool) {
|
||||
if len(data) < 48 {
|
||||
return metaoraPoolSwapEvent{}, false
|
||||
}
|
||||
if !bytes.Equal(data[:8], metaoraPoolSwapEventDiscriminator[:]) {
|
||||
return metaoraPoolSwapEvent{}, false
|
||||
}
|
||||
body := data[8:]
|
||||
return metaoraPoolSwapEvent{
|
||||
InAmount: binary.LittleEndian.Uint64(body[0:8]),
|
||||
OutAmount: binary.LittleEndian.Uint64(body[8:16]),
|
||||
TradeFee: binary.LittleEndian.Uint64(body[16:24]),
|
||||
ProtocolFee: binary.LittleEndian.Uint64(body[24:32]),
|
||||
HostFee: binary.LittleEndian.Uint64(body[32:40]),
|
||||
}, true
|
||||
}
|
||||
|
||||
152
metaorapool_test.go
Normal file
152
metaorapool_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
)
|
||||
|
||||
func TestMetaoraPoolSwapEventFromLogsUsesMatchingInvocation(t *testing.T) {
|
||||
firstEvent := metaoraPoolSwapEventLogForTest(10, 9, 1, 0, 0)
|
||||
secondEvent := metaoraPoolSwapEventLogForTest(4013522650, 135, 8043041, 2010760, 0)
|
||||
|
||||
logs := []string{
|
||||
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
|
||||
"Program log: Instruction: Swap",
|
||||
"Program data: " + firstEvent,
|
||||
"Program " + metaoraPoolProgramID.String() + " success",
|
||||
"Program " + solana.TokenProgramID.String() + " invoke [1]",
|
||||
"Program data: " + secondEvent,
|
||||
"Program " + solana.TokenProgramID.String() + " success",
|
||||
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
|
||||
"Program log: Instruction: Swap",
|
||||
"Program data: " + secondEvent,
|
||||
"Program " + metaoraPoolProgramID.String() + " success",
|
||||
}
|
||||
|
||||
event, ok := metaoraPoolSwapEventFromLogs(logs, 2)
|
||||
if !ok {
|
||||
t.Fatal("expected second swap event")
|
||||
}
|
||||
if event.InAmount != 4013522650 {
|
||||
t.Fatalf("in amount = %d, want 4013522650", event.InAmount)
|
||||
}
|
||||
if event.OutAmount != 135 {
|
||||
t.Fatalf("out amount = %d, want 135", event.OutAmount)
|
||||
}
|
||||
if event.TradeFee != 8043041 {
|
||||
t.Fatalf("trade fee = %d, want 8043041", event.TradeFee)
|
||||
}
|
||||
if event.ProtocolFee != 2010760 {
|
||||
t.Fatalf("protocol fee = %d, want 2010760", event.ProtocolFee)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaoraPoolSwapInstructionOccurrenceIncludesInnerInstructions(t *testing.T) {
|
||||
rawTx := &RawTx{
|
||||
Transaction: Transaction{
|
||||
Message: Message{
|
||||
AccountKeys: solana.PublicKeySlice{
|
||||
metaoraPoolProgramID,
|
||||
solana.MustPublicKeyFromBase58("BASDaPs2cdVTsvgPRfESDLZgek8tKRTfqbR2ksdgptsn"),
|
||||
},
|
||||
Instructions: []Instruction{
|
||||
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
|
||||
{ProgramIDIndex: 1, Data: []byte{1}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Meta: Meta{
|
||||
InnerInstructions: []InnerInstructions{
|
||||
{
|
||||
Index: 1,
|
||||
Instructions: []Instruction{
|
||||
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if got := metaoraPoolSwapInstructionOccurrence(rawTx, [2]uint{1, 1}); got != 2 {
|
||||
t.Fatalf("occurrence = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachLogEventsToInstructions(t *testing.T) {
|
||||
swapEvent := metaoraPoolSwapEventLogForTest(4013522650, 135, 8043041, 2010760, 0)
|
||||
rawTx := &RawTx{
|
||||
Transaction: Transaction{
|
||||
Message: Message{
|
||||
AccountKeys: solana.PublicKeySlice{
|
||||
metaoraPoolProgramID,
|
||||
solana.TokenProgramID,
|
||||
},
|
||||
Instructions: []Instruction{
|
||||
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
|
||||
},
|
||||
},
|
||||
},
|
||||
Meta: Meta{
|
||||
InnerInstructions: []InnerInstructions{
|
||||
{
|
||||
Index: 0,
|
||||
Instructions: []Instruction{
|
||||
{ProgramIDIndex: 1, Data: []byte{3}, StackHeight: intPtrForTest(2)},
|
||||
},
|
||||
},
|
||||
},
|
||||
LogMessages: []string{
|
||||
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
|
||||
"Program log: Instruction: Swap",
|
||||
"Program " + solana.TokenProgramID.String() + " invoke [2]",
|
||||
"Program " + solana.TokenProgramID.String() + " success",
|
||||
"Program data: " + swapEvent,
|
||||
"Program " + metaoraPoolProgramID.String() + " success",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
applyRawTxConvertLogOptions(rawTx, RawTxConvertOptions{ParseLogEvents: true, IgnoreLogMessages: true})
|
||||
|
||||
if len(rawTx.Meta.LogMessages) != 0 {
|
||||
t.Fatalf("log messages length = %d, want 0", len(rawTx.Meta.LogMessages))
|
||||
}
|
||||
if len(rawTx.Transaction.Message.Instructions[0].LogEvents) != 1 {
|
||||
t.Fatalf("outer log events length = %d, want 1", len(rawTx.Transaction.Message.Instructions[0].LogEvents))
|
||||
}
|
||||
if len(rawTx.Meta.InnerInstructions[0].Instructions[0].LogEvents) != 0 {
|
||||
t.Fatalf("inner log events length = %d, want 0", len(rawTx.Meta.InnerInstructions[0].Instructions[0].LogEvents))
|
||||
}
|
||||
|
||||
event, ok := metaoraPoolSwapEventFromInstruction(rawTx.Transaction.Message.Instructions[0])
|
||||
if !ok {
|
||||
t.Fatal("expected swap event from outer instruction")
|
||||
}
|
||||
if event.OutAmount != 135 {
|
||||
t.Fatalf("out amount = %d, want 135", event.OutAmount)
|
||||
}
|
||||
}
|
||||
|
||||
func metaoraPoolSwapInstructionDataForTest() []byte {
|
||||
data := make([]byte, 8+16)
|
||||
copy(data, metaoraPoolSwapDiscriminator[:])
|
||||
return data
|
||||
}
|
||||
|
||||
func metaoraPoolSwapEventLogForTest(inAmount, outAmount, tradeFee, protocolFee, hostFee uint64) string {
|
||||
data := make([]byte, 8+40)
|
||||
copy(data, metaoraPoolSwapEventDiscriminator[:])
|
||||
binary.LittleEndian.PutUint64(data[8:16], inAmount)
|
||||
binary.LittleEndian.PutUint64(data[16:24], outAmount)
|
||||
binary.LittleEndian.PutUint64(data[24:32], tradeFee)
|
||||
binary.LittleEndian.PutUint64(data[32:40], protocolFee)
|
||||
binary.LittleEndian.PutUint64(data[40:48], hostFee)
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
func intPtrForTest(value int) *int {
|
||||
return &value
|
||||
}
|
||||
648
pump.go
648
pump.go
@@ -33,7 +33,7 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct
|
||||
discriminator := *(*[8]byte)(decode[:8])
|
||||
|
||||
switch discriminator {
|
||||
case pumpBuyV2Discriminator, pumpBuyDiscriminator, pumpSellDiscriminator:
|
||||
case pumpBuyExactSolInDiscriminator, pumpBuyDiscriminator, pumpBuyV2Discriminator, pumpBuyExactQuoteInV2Discriminator, pumpSellDiscriminator, pumpSellV2Discriminator:
|
||||
if tx.Err != nil {
|
||||
return failedTxBuyOrSellParser(tx, instruction, innerInstructions, offset)
|
||||
}
|
||||
@@ -43,7 +43,7 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
return CreateParser(tx, instruction, innerInstructions, offset)
|
||||
case pumpMigrateDiscriminator:
|
||||
case pumpMigrateDiscriminator, pumpMigrateV2Discriminator:
|
||||
if tx.Err != nil {
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
@@ -89,6 +89,56 @@ type PumpCreateEvent struct {
|
||||
TokenProgram solana.PublicKey
|
||||
IsMayhemMode bool
|
||||
IsCashbackEnabled bool
|
||||
QuoteMint solana.PublicKey
|
||||
VirtualQuoteReserves uint64
|
||||
}
|
||||
|
||||
type pumpCreateEventLegacy struct {
|
||||
Name string
|
||||
Symbol string
|
||||
Uri string
|
||||
|
||||
Mint solana.PublicKey
|
||||
BondingCurve solana.PublicKey
|
||||
User solana.PublicKey
|
||||
Creator solana.PublicKey
|
||||
|
||||
Timestamp int64
|
||||
VirtualTokenReserves uint64
|
||||
VirtualSolReserves uint64
|
||||
RealTokenReserves uint64
|
||||
TokenTotalSupply uint64
|
||||
TokenProgram solana.PublicKey
|
||||
IsMayhemMode bool
|
||||
IsCashbackEnabled bool
|
||||
}
|
||||
|
||||
func decodePumpCreateEvent(data []byte) (PumpCreateEvent, error) {
|
||||
var event PumpCreateEvent
|
||||
if err := agbinary.NewBorshDecoder(data).Decode(&event); err == nil {
|
||||
return event, nil
|
||||
}
|
||||
var legacy pumpCreateEventLegacy
|
||||
if err := agbinary.NewBorshDecoder(data).Decode(&legacy); err != nil {
|
||||
return PumpCreateEvent{}, err
|
||||
}
|
||||
return PumpCreateEvent{
|
||||
Name: legacy.Name,
|
||||
Symbol: legacy.Symbol,
|
||||
Uri: legacy.Uri,
|
||||
Mint: legacy.Mint,
|
||||
BondingCurve: legacy.BondingCurve,
|
||||
User: legacy.User,
|
||||
Creator: legacy.Creator,
|
||||
Timestamp: legacy.Timestamp,
|
||||
VirtualTokenReserves: legacy.VirtualTokenReserves,
|
||||
VirtualSolReserves: legacy.VirtualSolReserves,
|
||||
RealTokenReserves: legacy.RealTokenReserves,
|
||||
TokenTotalSupply: legacy.TokenTotalSupply,
|
||||
TokenProgram: legacy.TokenProgram,
|
||||
IsMayhemMode: legacy.IsMayhemMode,
|
||||
IsCashbackEnabled: legacy.IsCashbackEnabled,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||
@@ -106,7 +156,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
|
||||
}
|
||||
for innerIndex, innerInstr := range inners {
|
||||
if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], pumpCreateEventDiscriminator[:]) {
|
||||
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&createEvent)
|
||||
createEvent, err = decodePumpCreateEvent(innerInstr.Data[16:])
|
||||
if offset[1] == 0 {
|
||||
offset[0] += 1
|
||||
} else {
|
||||
@@ -129,6 +179,11 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
|
||||
}
|
||||
userBase := getAccountBalanceAfterTx(result, userIndex)
|
||||
userQuote, _ := GetSolAfterTx(result, userIndex)
|
||||
quoteMint, quoteTokenProgram, quoteDecimals := pumpCreateQuoteAccounts(result, instr, createEvent)
|
||||
userQuoteBalance := decimal.NewFromUint64(userQuote)
|
||||
if !quoteMint.IsZero() && !quoteMint.Equals(wSolMint) {
|
||||
userQuoteBalance = GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint)
|
||||
}
|
||||
|
||||
totalSupply := decimal.NewFromUint64(createEvent.TokenTotalSupply).Div(decimal.New(1, 6))
|
||||
tx.Token[createEvent.Mint] = TokenMeta{
|
||||
@@ -146,12 +201,12 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
|
||||
Event: "create",
|
||||
Pool: createEvent.BondingCurve,
|
||||
BaseMint: createEvent.Mint,
|
||||
QuoteMint: solana.PublicKey{},
|
||||
QuoteMint: quoteMint,
|
||||
BaseTokenProgram: createEvent.TokenProgram,
|
||||
QuoteTokenProgram: solana.PublicKey{},
|
||||
QuoteTokenProgram: quoteTokenProgram,
|
||||
Creator: createEvent.Creator,
|
||||
BaseMintDecimals: 6,
|
||||
QuoteMintDecimals: 9,
|
||||
QuoteMintDecimals: quoteDecimals,
|
||||
User: createEvent.User,
|
||||
BaseAmount: decimal.Zero,
|
||||
QuoteAmount: decimal.Zero,
|
||||
@@ -160,7 +215,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
|
||||
Mayhem: createEvent.IsMayhemMode,
|
||||
Cashback: createEvent.IsCashbackEnabled,
|
||||
UserBaseBalance: userBase,
|
||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
||||
UserQuoteBalance: userQuoteBalance,
|
||||
EntryContract: entryContract,
|
||||
},
|
||||
}, offset, nil
|
||||
@@ -197,6 +252,141 @@ type PumpTradeEvent struct {
|
||||
MayhemMode bool
|
||||
CashbackFeeBasisPoints uint64
|
||||
Cashback uint64
|
||||
BuybackFeeBasisPoints uint64
|
||||
BuybackFee uint64
|
||||
Shareholders []PumpShareholder
|
||||
QuoteMint solana.PublicKey
|
||||
QuoteAmount uint64
|
||||
VirtualQuoteReserves uint64
|
||||
RealQuoteReserves uint64
|
||||
}
|
||||
|
||||
type PumpShareholder struct {
|
||||
Address solana.PublicKey
|
||||
ShareBps uint16
|
||||
}
|
||||
|
||||
type pumpTradeEventLegacy struct {
|
||||
Mint solana.PublicKey
|
||||
SolAmount uint64
|
||||
TokenAmount uint64
|
||||
IsBuy bool
|
||||
User solana.PublicKey
|
||||
Timestamp int64
|
||||
VirtualSolReserves uint64
|
||||
VirtualTokenReserves uint64
|
||||
|
||||
RealSolReserves uint64
|
||||
RealTokenReserves uint64
|
||||
|
||||
FeeRecipient solana.PublicKey
|
||||
FeeBasisPoints uint64
|
||||
Fee uint64
|
||||
|
||||
Creator solana.PublicKey
|
||||
|
||||
CreatorFeeBasisPoints uint64
|
||||
CreatorFee uint64
|
||||
|
||||
TrackVolume bool
|
||||
TotalUnclaimedTokens uint64
|
||||
TotalClaimedTokens uint64
|
||||
CurrentSolVolume uint64
|
||||
LastUpdateTimestamp int64
|
||||
IxName string
|
||||
MayhemMode bool
|
||||
CashbackFeeBasisPoints uint64
|
||||
Cashback uint64
|
||||
}
|
||||
|
||||
type pumpTradeEventLegacyV0 struct {
|
||||
Mint solana.PublicKey
|
||||
SolAmount uint64
|
||||
TokenAmount uint64
|
||||
IsBuy bool
|
||||
User solana.PublicKey
|
||||
Timestamp int64
|
||||
VirtualSolReserves uint64
|
||||
VirtualTokenReserves uint64
|
||||
RealSolReserves uint64
|
||||
RealTokenReserves uint64
|
||||
FeeRecipient solana.PublicKey
|
||||
FeeBasisPoints uint64
|
||||
Fee uint64
|
||||
Creator solana.PublicKey
|
||||
CreatorFeeBasisPoints uint64
|
||||
CreatorFee uint64
|
||||
TrackVolume bool
|
||||
TotalUnclaimedTokens uint64
|
||||
TotalClaimedTokens uint64
|
||||
CurrentSolVolume uint64
|
||||
LastUpdateTimestamp int64
|
||||
IxName string
|
||||
}
|
||||
|
||||
func decodePumpTradeEvent(data []byte) (PumpTradeEvent, error) {
|
||||
var event PumpTradeEvent
|
||||
if err := agbinary.NewBorshDecoder(data).Decode(&event); err == nil {
|
||||
return event, nil
|
||||
}
|
||||
var legacy pumpTradeEventLegacy
|
||||
if err := agbinary.NewBorshDecoder(data).Decode(&legacy); err == nil {
|
||||
return PumpTradeEvent{
|
||||
Mint: legacy.Mint,
|
||||
SolAmount: legacy.SolAmount,
|
||||
TokenAmount: legacy.TokenAmount,
|
||||
IsBuy: legacy.IsBuy,
|
||||
User: legacy.User,
|
||||
Timestamp: legacy.Timestamp,
|
||||
VirtualSolReserves: legacy.VirtualSolReserves,
|
||||
VirtualTokenReserves: legacy.VirtualTokenReserves,
|
||||
RealSolReserves: legacy.RealSolReserves,
|
||||
RealTokenReserves: legacy.RealTokenReserves,
|
||||
FeeRecipient: legacy.FeeRecipient,
|
||||
FeeBasisPoints: legacy.FeeBasisPoints,
|
||||
Fee: legacy.Fee,
|
||||
Creator: legacy.Creator,
|
||||
CreatorFeeBasisPoints: legacy.CreatorFeeBasisPoints,
|
||||
CreatorFee: legacy.CreatorFee,
|
||||
TrackVolume: legacy.TrackVolume,
|
||||
TotalUnclaimedTokens: legacy.TotalUnclaimedTokens,
|
||||
TotalClaimedTokens: legacy.TotalClaimedTokens,
|
||||
CurrentSolVolume: legacy.CurrentSolVolume,
|
||||
LastUpdateTimestamp: legacy.LastUpdateTimestamp,
|
||||
IxName: legacy.IxName,
|
||||
MayhemMode: legacy.MayhemMode,
|
||||
CashbackFeeBasisPoints: legacy.CashbackFeeBasisPoints,
|
||||
Cashback: legacy.Cashback,
|
||||
}, nil
|
||||
}
|
||||
var legacyV0 pumpTradeEventLegacyV0
|
||||
if err := agbinary.NewBorshDecoder(data).Decode(&legacyV0); err != nil {
|
||||
return PumpTradeEvent{}, err
|
||||
}
|
||||
return PumpTradeEvent{
|
||||
Mint: legacyV0.Mint,
|
||||
SolAmount: legacyV0.SolAmount,
|
||||
TokenAmount: legacyV0.TokenAmount,
|
||||
IsBuy: legacyV0.IsBuy,
|
||||
User: legacyV0.User,
|
||||
Timestamp: legacyV0.Timestamp,
|
||||
VirtualSolReserves: legacyV0.VirtualSolReserves,
|
||||
VirtualTokenReserves: legacyV0.VirtualTokenReserves,
|
||||
RealSolReserves: legacyV0.RealSolReserves,
|
||||
RealTokenReserves: legacyV0.RealTokenReserves,
|
||||
FeeRecipient: legacyV0.FeeRecipient,
|
||||
FeeBasisPoints: legacyV0.FeeBasisPoints,
|
||||
Fee: legacyV0.Fee,
|
||||
Creator: legacyV0.Creator,
|
||||
CreatorFeeBasisPoints: legacyV0.CreatorFeeBasisPoints,
|
||||
CreatorFee: legacyV0.CreatorFee,
|
||||
TrackVolume: legacyV0.TrackVolume,
|
||||
TotalUnclaimedTokens: legacyV0.TotalUnclaimedTokens,
|
||||
TotalClaimedTokens: legacyV0.TotalClaimedTokens,
|
||||
CurrentSolVolume: legacyV0.CurrentSolVolume,
|
||||
LastUpdateTimestamp: legacyV0.LastUpdateTimestamp,
|
||||
IxName: legacyV0.IxName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type PumpTradeFeeArg struct {
|
||||
@@ -220,17 +410,185 @@ type PumpTradeArgs struct {
|
||||
|
||||
func pumpTradeAmountInfoFromArgs(args PumpTradeArgs) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
|
||||
switch {
|
||||
case bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]):
|
||||
case bytes.Equal(args.Discriminator[:], pumpBuyExactSolInDiscriminator[:]),
|
||||
bytes.Equal(args.Discriminator[:], pumpBuyExactQuoteInV2Discriminator[:]):
|
||||
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
|
||||
case bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]):
|
||||
case bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]),
|
||||
bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]):
|
||||
return SwapModeExactOut, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
|
||||
case bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]):
|
||||
case bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]),
|
||||
bytes.Equal(args.Discriminator[:], pumpSellV2Discriminator[:]):
|
||||
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
|
||||
default:
|
||||
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
|
||||
}
|
||||
}
|
||||
|
||||
type pumpTradeAccountLayout struct {
|
||||
IsV2 bool
|
||||
FeeRecipient int
|
||||
BaseMint int
|
||||
QuoteMint int
|
||||
BaseTokenProgram int
|
||||
QuoteTokenProgram int
|
||||
Pool int
|
||||
BasePoolToken int
|
||||
QuotePoolToken int
|
||||
User int
|
||||
BaseUserToken int
|
||||
QuoteUserToken int
|
||||
}
|
||||
|
||||
func pumpTradeLayout(instr Instruction) (pumpTradeAccountLayout, bool) {
|
||||
if len(instr.Data) < 8 {
|
||||
return pumpTradeAccountLayout{}, false
|
||||
}
|
||||
discriminator := instr.Data[:8]
|
||||
switch {
|
||||
case bytes.Equal(discriminator, pumpBuyDiscriminator[:]), bytes.Equal(discriminator, pumpBuyExactSolInDiscriminator[:]):
|
||||
if len(instr.Accounts) <= 8 {
|
||||
return pumpTradeAccountLayout{}, false
|
||||
}
|
||||
return pumpTradeAccountLayout{
|
||||
FeeRecipient: 1,
|
||||
BaseMint: 2,
|
||||
QuoteMint: -1,
|
||||
BaseTokenProgram: 8,
|
||||
QuoteTokenProgram: -1,
|
||||
Pool: 3,
|
||||
BasePoolToken: 4,
|
||||
QuotePoolToken: -1,
|
||||
User: 6,
|
||||
BaseUserToken: 5,
|
||||
QuoteUserToken: -1,
|
||||
}, true
|
||||
case bytes.Equal(discriminator, pumpSellDiscriminator[:]):
|
||||
if len(instr.Accounts) <= 9 {
|
||||
return pumpTradeAccountLayout{}, false
|
||||
}
|
||||
return pumpTradeAccountLayout{
|
||||
FeeRecipient: 1,
|
||||
BaseMint: 2,
|
||||
QuoteMint: -1,
|
||||
BaseTokenProgram: 9,
|
||||
QuoteTokenProgram: -1,
|
||||
Pool: 3,
|
||||
BasePoolToken: 4,
|
||||
QuotePoolToken: -1,
|
||||
User: 6,
|
||||
BaseUserToken: 5,
|
||||
QuoteUserToken: -1,
|
||||
}, true
|
||||
case bytes.Equal(discriminator, pumpBuyV2Discriminator[:]),
|
||||
bytes.Equal(discriminator, pumpBuyExactQuoteInV2Discriminator[:]),
|
||||
bytes.Equal(discriminator, pumpSellV2Discriminator[:]):
|
||||
if len(instr.Accounts) <= 15 {
|
||||
return pumpTradeAccountLayout{}, false
|
||||
}
|
||||
return pumpTradeAccountLayout{
|
||||
IsV2: true,
|
||||
FeeRecipient: 6,
|
||||
BaseMint: 1,
|
||||
QuoteMint: 2,
|
||||
BaseTokenProgram: 3,
|
||||
QuoteTokenProgram: 4,
|
||||
Pool: 10,
|
||||
BasePoolToken: 11,
|
||||
QuotePoolToken: 12,
|
||||
User: 13,
|
||||
BaseUserToken: 14,
|
||||
QuoteUserToken: 15,
|
||||
}, true
|
||||
default:
|
||||
return pumpTradeAccountLayout{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func pumpInstructionIsSell(data []byte) bool {
|
||||
return len(data) >= 8 && (bytes.Equal(data[:8], pumpSellDiscriminator[:]) || bytes.Equal(data[:8], pumpSellV2Discriminator[:]))
|
||||
}
|
||||
|
||||
func pumpInstructionIsExactQuoteIn(data []byte) bool {
|
||||
return len(data) >= 8 && (bytes.Equal(data[:8], pumpBuyExactSolInDiscriminator[:]) || bytes.Equal(data[:8], pumpBuyExactQuoteInV2Discriminator[:]))
|
||||
}
|
||||
|
||||
func pumpAccount(result *RawTx, instr Instruction, accountIndex int) solana.PublicKey {
|
||||
if accountIndex < 0 || accountIndex >= len(instr.Accounts) {
|
||||
return solana.PublicKey{}
|
||||
}
|
||||
listIndex := instr.Accounts[accountIndex]
|
||||
if listIndex < 0 || listIndex >= len(result.accountList) {
|
||||
return solana.PublicKey{}
|
||||
}
|
||||
return result.accountList[listIndex]
|
||||
}
|
||||
|
||||
func pumpCreateQuoteAccounts(result *RawTx, instr Instruction, createEvent PumpCreateEvent) (solana.PublicKey, solana.PublicKey, uint8) {
|
||||
quoteMint := createEvent.QuoteMint
|
||||
quoteTokenProgram := solana.PublicKey{}
|
||||
optionalStart := -1
|
||||
if len(instr.Data) >= 8 && bytes.Equal(instr.Data[:8], pumpCreateV2Discriminator[:]) {
|
||||
optionalStart = 16
|
||||
}
|
||||
if optionalStart >= 0 && len(instr.Accounts) > optionalStart {
|
||||
accountQuoteMint := pumpAccount(result, instr, optionalStart)
|
||||
if quoteMint.IsZero() && !accountQuoteMint.IsZero() && !accountQuoteMint.Equals(wSolMint) {
|
||||
quoteMint = accountQuoteMint
|
||||
}
|
||||
if len(instr.Accounts) > optionalStart+2 && !quoteMint.IsZero() {
|
||||
quoteTokenProgram = pumpAccount(result, instr, optionalStart+2)
|
||||
}
|
||||
}
|
||||
if quoteMint.Equals(wSolMint) {
|
||||
quoteTokenProgram = solana.TokenProgramID
|
||||
}
|
||||
if quoteTokenProgram.IsZero() && !quoteMint.IsZero() {
|
||||
quoteTokenProgram = solana.TokenProgramID
|
||||
}
|
||||
return quoteMint, quoteTokenProgram, pumpQuoteDecimals(result, quoteMint)
|
||||
}
|
||||
|
||||
func pumpMintDecimalsFromBalances(result *RawTx, mint solana.PublicKey, fallback uint8) uint8 {
|
||||
if mint.IsZero() {
|
||||
return fallback
|
||||
}
|
||||
for _, balance := range result.Meta.PostTokenBalances {
|
||||
balance.ParseAccount()
|
||||
if balance.MintAccount.Equals(mint) {
|
||||
return uint8(balance.UITokenAmount.Decimals)
|
||||
}
|
||||
}
|
||||
for _, balance := range result.Meta.PreTokenBalances {
|
||||
balance.ParseAccount()
|
||||
if balance.MintAccount.Equals(mint) {
|
||||
return uint8(balance.UITokenAmount.Decimals)
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func pumpQuoteDecimals(result *RawTx, quoteMint solana.PublicKey) uint8 {
|
||||
fallback := uint8(9)
|
||||
if quoteMint.Equals(usdcMint) || quoteMint.Equals(usd1Mint) {
|
||||
fallback = 6
|
||||
}
|
||||
return pumpMintDecimalsFromBalances(result, quoteMint, fallback)
|
||||
}
|
||||
|
||||
func pumpQuoteAmount(tradeEvent PumpTradeEvent) uint64 {
|
||||
if tradeEvent.QuoteAmount != 0 {
|
||||
return tradeEvent.QuoteAmount
|
||||
}
|
||||
return tradeEvent.SolAmount
|
||||
}
|
||||
|
||||
func pumpQuoteReserve(tradeEvent PumpTradeEvent) uint64 {
|
||||
if tradeEvent.RealQuoteReserves != 0 {
|
||||
return tradeEvent.RealQuoteReserves
|
||||
}
|
||||
return tradeEvent.RealSolReserves
|
||||
}
|
||||
|
||||
func pumpCompleteMatchesTradeEvent(completeEvent CompleteEvent, tradeEvent PumpTradeEvent, bondingCurve solana.PublicKey) bool {
|
||||
if completeEvent.Mint != tradeEvent.Mint {
|
||||
return false
|
||||
@@ -279,10 +637,16 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions
|
||||
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]]
|
||||
layout, ok := pumpTradeLayout(instruction)
|
||||
if !ok {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction account layout, offset, %d, %d", offset[0], offset[1])
|
||||
}
|
||||
user := pumpAccount(result, instruction, layout.User)
|
||||
ataUserIdx := instruction.Accounts[layout.BaseUserToken]
|
||||
userIndex := instruction.Accounts[layout.User]
|
||||
mint := pumpAccount(result, instruction, layout.BaseMint)
|
||||
quoteMint := pumpAccount(result, instruction, layout.QuoteMint)
|
||||
quoteTokenProgram := pumpAccount(result, instruction, layout.QuoteTokenProgram)
|
||||
var args PumpTradeArgs
|
||||
err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args)
|
||||
if err != nil {
|
||||
@@ -290,30 +654,27 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions
|
||||
}
|
||||
var event string
|
||||
var (
|
||||
solAmount, tokenAmount uint64
|
||||
quoteAmount, tokenAmount uint64
|
||||
)
|
||||
if bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) {
|
||||
if bytes.Equal(args.Discriminator[:], pumpBuyExactSolInDiscriminator[:]) ||
|
||||
bytes.Equal(args.Discriminator[:], pumpBuyExactQuoteInV2Discriminator[:]) {
|
||||
event = "buy_failed"
|
||||
solAmount = args.Amount1
|
||||
quoteAmount = args.Amount1
|
||||
tokenAmount = args.Amount2
|
||||
} else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) {
|
||||
} else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) ||
|
||||
bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) {
|
||||
event = "buy_failed"
|
||||
solAmount = args.Amount2
|
||||
quoteAmount = args.Amount2
|
||||
tokenAmount = args.Amount1
|
||||
} else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) {
|
||||
} else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) ||
|
||||
bytes.Equal(args.Discriminator[:], pumpSellV2Discriminator[:]) {
|
||||
event = "sell_failed"
|
||||
solAmount = args.Amount2
|
||||
quoteAmount = 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]]
|
||||
}
|
||||
baseTokenProgram := pumpAccount(result, instruction, layout.BaseTokenProgram)
|
||||
if !user.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
|
||||
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, mint)
|
||||
//&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount
|
||||
@@ -325,31 +686,43 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions
|
||||
}
|
||||
|
||||
userBase := getAccountBalanceAfterTx(result, ataUserIdx)
|
||||
userQuote, _ := GetSolAfterTx(result, userIndex)
|
||||
userQuote := decimal.Zero
|
||||
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
|
||||
userQuote = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuoteUserToken])
|
||||
} else {
|
||||
userQuoteLamports, _ := GetSolAfterTx(result, userIndex)
|
||||
userQuote = decimal.NewFromUint64(userQuoteLamports)
|
||||
}
|
||||
|
||||
bcIdx := instruction.Accounts[3]
|
||||
bcAtaIndex := instruction.Accounts[4]
|
||||
bcIdx := instruction.Accounts[layout.Pool]
|
||||
bcAtaIndex := instruction.Accounts[layout.BasePoolToken]
|
||||
quoteReserves := decimal.Zero
|
||||
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
|
||||
quoteReserves = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuotePoolToken])
|
||||
} else {
|
||||
solReserves, _ := GetSolAfterTx(result, bcIdx)
|
||||
quoteReserves = decimal.NewFromUint64(solReserves)
|
||||
}
|
||||
tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex)
|
||||
swaps := []Swap{
|
||||
{
|
||||
Program: SolProgramPump,
|
||||
Event: event,
|
||||
Pool: result.accountList[instruction.Accounts[3]],
|
||||
Pool: pumpAccount(result, instruction, layout.Pool),
|
||||
BaseMint: mint,
|
||||
QuoteMint: solana.PublicKey{},
|
||||
QuoteMint: quoteMint,
|
||||
BaseTokenProgram: baseTokenProgram,
|
||||
QuoteTokenProgram: solana.PublicKey{},
|
||||
QuoteTokenProgram: quoteTokenProgram,
|
||||
BaseMintDecimals: 6,
|
||||
QuoteMintDecimals: 9,
|
||||
QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
|
||||
User: user,
|
||||
BaseAmount: decimal.NewFromUint64(tokenAmount),
|
||||
QuoteAmount: decimal.NewFromUint64(solAmount),
|
||||
QuoteAmount: decimal.NewFromUint64(quoteAmount),
|
||||
BaseReserve: tokenReserves,
|
||||
QuoteReserve: decimal.NewFromUint64(solReserves),
|
||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
|
||||
QuoteReserve: quoteReserves,
|
||||
Mayhem: isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
|
||||
UserBaseBalance: userBase,
|
||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
||||
UserQuoteBalance: userQuote,
|
||||
EntryContract: entryContract,
|
||||
},
|
||||
}
|
||||
@@ -365,6 +738,10 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||
var err error
|
||||
var programIndex = instruction.ProgramIDIndex
|
||||
layout, ok := pumpTradeLayout(instruction)
|
||||
if !ok {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction account layout, offset, %d, %d", offset[0], offset[1])
|
||||
}
|
||||
|
||||
feeEventProgramIndex := 0
|
||||
for i, b := range result.accountList {
|
||||
@@ -411,7 +788,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
if tradeFound {
|
||||
break
|
||||
}
|
||||
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&tradeEvent)
|
||||
tradeEvent, err = decodePumpTradeEvent(innerInstr.Data[16:])
|
||||
if offset[1] == 0 {
|
||||
newoffset = [2]uint{offset[0] + 1, offset[1]}
|
||||
} else {
|
||||
@@ -420,7 +797,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
if err != nil {
|
||||
return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
expectedIsBuy := !bytes.Equal(instruction.Data[:8], pumpSellDiscriminator[:])
|
||||
expectedIsBuy := !pumpInstructionIsSell(instruction.Data)
|
||||
if tradeEvent.IsBuy != expectedIsBuy {
|
||||
tradeEvent = PumpTradeEvent{}
|
||||
continue
|
||||
@@ -437,7 +814,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
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]]) {
|
||||
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, pumpAccount(result, instruction, layout.Pool)) {
|
||||
break
|
||||
}
|
||||
if offset[1] == 0 {
|
||||
@@ -451,7 +828,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
|
||||
}
|
||||
}
|
||||
if tradeEvent == (PumpTradeEvent{}) {
|
||||
if !tradeFound {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("pmp buy/sell event not found, offset, %d, %d", offset[0], offset[1])
|
||||
}
|
||||
|
||||
@@ -463,13 +840,16 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
}
|
||||
|
||||
event := ""
|
||||
baseTokenProgram := solana.TokenProgramID
|
||||
baseTokenProgram := pumpAccount(result, instruction, layout.BaseTokenProgram)
|
||||
quoteMint := tradeEvent.QuoteMint
|
||||
if quoteMint.IsZero() {
|
||||
quoteMint = pumpAccount(result, instruction, layout.QuoteMint)
|
||||
}
|
||||
quoteTokenProgram := pumpAccount(result, instruction, layout.QuoteTokenProgram)
|
||||
if tradeEvent.IsBuy {
|
||||
event = "buy"
|
||||
baseTokenProgram = result.accountList[instruction.Accounts[8]]
|
||||
} else {
|
||||
event = "sell"
|
||||
baseTokenProgram = result.accountList[instruction.Accounts[9]]
|
||||
}
|
||||
if _, exists := tx.Token[tradeEvent.Mint]; !exists {
|
||||
tx.Token[tradeEvent.Mint] = TokenMeta{
|
||||
@@ -481,8 +861,8 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
|
||||
var user = tradeEvent.User
|
||||
|
||||
ataUserIdx := instruction.Accounts[5]
|
||||
userIndex := instruction.Accounts[6]
|
||||
ataUserIdx := instruction.Accounts[layout.BaseUserToken]
|
||||
userIndex := instruction.Accounts[layout.User]
|
||||
if !tradeEvent.User.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
|
||||
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, tradeEvent.Mint)
|
||||
//&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount
|
||||
@@ -494,14 +874,20 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
}
|
||||
|
||||
userBase := getAccountBalanceAfterTx(result, ataUserIdx)
|
||||
userQuote, _ := GetSolAfterTx(result, userIndex)
|
||||
userQuote := decimal.Zero
|
||||
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
|
||||
userQuote = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuoteUserToken])
|
||||
} else {
|
||||
userQuoteLamports, _ := GetSolAfterTx(result, userIndex)
|
||||
userQuote = decimal.NewFromUint64(userQuoteLamports)
|
||||
}
|
||||
|
||||
solAmount := tradeEvent.SolAmount
|
||||
if tradeEvent.IsBuy && bytes.Equal(instruction.Data[:8], pumpBuyV2Discriminator[:]) {
|
||||
quoteAmount := pumpQuoteAmount(tradeEvent)
|
||||
if tradeEvent.IsBuy && pumpInstructionIsExactQuoteIn(instruction.Data) && !layout.IsV2 {
|
||||
fee := tradeEvent.Fee + tradeEvent.CreatorFee
|
||||
solAmount = tradeFeeArg.TradeSize
|
||||
if solAmount > fee {
|
||||
solAmount = solAmount - fee
|
||||
quoteAmount = tradeFeeArg.TradeSize
|
||||
if quoteAmount > fee {
|
||||
quoteAmount = quoteAmount - fee
|
||||
}
|
||||
}
|
||||
isCashbackCoin := tradeEvent.CashbackFeeBasisPoints > 0 || tradeEvent.Cashback > 0
|
||||
@@ -509,22 +895,22 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
{
|
||||
Program: SolProgramPump,
|
||||
Event: event,
|
||||
Pool: result.accountList[instruction.Accounts[3]],
|
||||
Pool: pumpAccount(result, instruction, layout.Pool),
|
||||
BaseMint: tradeEvent.Mint,
|
||||
QuoteMint: solana.PublicKey{},
|
||||
QuoteMint: quoteMint,
|
||||
BaseTokenProgram: baseTokenProgram,
|
||||
QuoteTokenProgram: solana.PublicKey{},
|
||||
QuoteTokenProgram: quoteTokenProgram,
|
||||
Creator: tradeEvent.Creator,
|
||||
BaseMintDecimals: 6,
|
||||
QuoteMintDecimals: 9,
|
||||
QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
|
||||
User: user,
|
||||
BaseAmount: decimal.NewFromUint64(tradeEvent.TokenAmount),
|
||||
QuoteAmount: decimal.NewFromUint64(solAmount),
|
||||
QuoteAmount: decimal.NewFromUint64(quoteAmount),
|
||||
BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves),
|
||||
QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves),
|
||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
|
||||
QuoteReserve: decimal.NewFromUint64(pumpQuoteReserve(tradeEvent)),
|
||||
Mayhem: tradeEvent.MayhemMode || isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
|
||||
UserBaseBalance: userBase,
|
||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
||||
UserQuoteBalance: userQuote,
|
||||
EntryContract: entryContract,
|
||||
Cashback: isCashbackCoin,
|
||||
},
|
||||
@@ -537,20 +923,20 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
swaps = append(swaps, Swap{
|
||||
Program: SolProgramPump,
|
||||
Event: "complete",
|
||||
Pool: result.accountList[instruction.Accounts[3]],
|
||||
Pool: pumpAccount(result, instruction, layout.Pool),
|
||||
BaseMint: tradeEvent.Mint,
|
||||
QuoteMint: solana.PublicKey{},
|
||||
BaseTokenProgram: result.accountList[instruction.Accounts[8]],
|
||||
QuoteTokenProgram: solana.PublicKey{},
|
||||
QuoteMint: quoteMint,
|
||||
BaseTokenProgram: baseTokenProgram,
|
||||
QuoteTokenProgram: quoteTokenProgram,
|
||||
Creator: tradeEvent.Creator,
|
||||
BaseMintDecimals: 6,
|
||||
QuoteMintDecimals: 9,
|
||||
QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
|
||||
User: user,
|
||||
BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves),
|
||||
QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves),
|
||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
|
||||
QuoteReserve: decimal.NewFromUint64(pumpQuoteReserve(tradeEvent)),
|
||||
Mayhem: tradeEvent.MayhemMode || isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
|
||||
UserBaseBalance: userBase,
|
||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
||||
UserQuoteBalance: userQuote,
|
||||
EntryContract: entryContract,
|
||||
})
|
||||
}
|
||||
@@ -572,11 +958,74 @@ type MigrateEvent struct {
|
||||
Pool solana.PublicKey
|
||||
}
|
||||
|
||||
type pumpMigrateAccountLayout struct {
|
||||
IsV2 bool
|
||||
BaseMint int
|
||||
QuoteMint int
|
||||
Pool int
|
||||
BasePoolToken int
|
||||
QuotePoolToken int
|
||||
User int
|
||||
BaseTokenProgram int
|
||||
QuoteTokenProgram int
|
||||
}
|
||||
|
||||
func pumpMigrateLayout(instr Instruction) (pumpMigrateAccountLayout, bool) {
|
||||
if len(instr.Data) < 8 {
|
||||
return pumpMigrateAccountLayout{}, false
|
||||
}
|
||||
discriminator := instr.Data[:8]
|
||||
switch {
|
||||
case bytes.Equal(discriminator, pumpMigrateDiscriminator[:]):
|
||||
if len(instr.Accounts) <= 14 {
|
||||
return pumpMigrateAccountLayout{}, false
|
||||
}
|
||||
return pumpMigrateAccountLayout{
|
||||
BaseMint: 2,
|
||||
QuoteMint: 14,
|
||||
Pool: 3,
|
||||
BasePoolToken: 4,
|
||||
QuotePoolToken: -1,
|
||||
User: 5,
|
||||
BaseTokenProgram: 7,
|
||||
QuoteTokenProgram: -1,
|
||||
}, true
|
||||
case bytes.Equal(discriminator, pumpMigrateV2Discriminator[:]):
|
||||
if len(instr.Accounts) <= 20 {
|
||||
return pumpMigrateAccountLayout{}, false
|
||||
}
|
||||
return pumpMigrateAccountLayout{
|
||||
IsV2: true,
|
||||
BaseMint: 2,
|
||||
QuoteMint: 3,
|
||||
Pool: 4,
|
||||
BasePoolToken: 5,
|
||||
QuotePoolToken: 6,
|
||||
User: 7,
|
||||
BaseTokenProgram: 19,
|
||||
QuoteTokenProgram: 20,
|
||||
}, true
|
||||
default:
|
||||
return pumpMigrateAccountLayout{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func decimalFromUint64WithFallback(primary, fallback uint64) decimal.Decimal {
|
||||
if primary != 0 {
|
||||
return decimal.NewFromUint64(primary)
|
||||
}
|
||||
return decimal.NewFromUint64(fallback)
|
||||
}
|
||||
|
||||
func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||
result := tx.rawTx
|
||||
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||
var err error
|
||||
programIndex := instr.ProgramIDIndex
|
||||
layout, ok := pumpMigrateLayout(instr)
|
||||
if !ok {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("unknown pump migrate instruction account layout, offset, %d, %d", offset[0], offset[1])
|
||||
}
|
||||
ammprogramIdx := 0
|
||||
for i, b := range result.accountList {
|
||||
if b.Equals(pumpAmmProgram) {
|
||||
@@ -633,20 +1082,45 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction
|
||||
|
||||
offset = [2]uint{newoffset[0], newoffset[1]}
|
||||
// verify migrate by checking create pool and migrate event
|
||||
userIndex := instr.Accounts[5]
|
||||
ataBondingCurveAccountIndex := instr.Accounts[4]
|
||||
userIndex := instr.Accounts[layout.User]
|
||||
ataBondingCurveAccountIndex := instr.Accounts[layout.BasePoolToken]
|
||||
bc, err := getTokenBalanceAfterTx(result, ataBondingCurveAccountIndex)
|
||||
if err != nil || bc == nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("pump migrate get bonding curve balance error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
baseTokenProgram := bc.ProgramIDAccount
|
||||
if layout.IsV2 {
|
||||
baseTokenProgram = pumpAccount(result, instr, layout.BaseTokenProgram)
|
||||
}
|
||||
quoteMint := createEvent.QuoteMint
|
||||
if quoteMint.IsZero() {
|
||||
quoteMint = pumpAccount(result, instr, layout.QuoteMint)
|
||||
}
|
||||
quoteTokenProgram := pumpAccount(result, instr, layout.QuoteTokenProgram)
|
||||
if quoteTokenProgram.IsZero() && !quoteMint.IsZero() {
|
||||
quoteTokenProgram = solana.TokenProgramID
|
||||
}
|
||||
quoteDecimals := createEvent.QuoteMintDecimals
|
||||
if quoteDecimals == 0 {
|
||||
quoteDecimals = pumpQuoteDecimals(result, quoteMint)
|
||||
}
|
||||
var userBase decimal.Decimal
|
||||
if result.accountList[userIndex].Equals(pumpMigrationAccount) {
|
||||
userBase = decimal.Zero
|
||||
} else {
|
||||
userBase = GetTokenBalanceAfterTx(result, userIndex, baseTokenProgram, migrateEvent.Mint)
|
||||
}
|
||||
userQuote, _ := GetSolAfterTx(result, userIndex)
|
||||
userQuote := decimal.Zero
|
||||
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
|
||||
userQuote = GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint)
|
||||
} else {
|
||||
userQuoteLamports, _ := GetSolAfterTx(result, userIndex)
|
||||
userQuote = decimal.NewFromUint64(userQuoteLamports)
|
||||
}
|
||||
baseAmount := decimalFromUint64WithFallback(createEvent.BaseAmountIn, migrateEvent.MintAmount)
|
||||
quoteAmount := decimalFromUint64WithFallback(createEvent.QuoteAmountIn, migrateEvent.SolAmount)
|
||||
baseReserve := decimalFromUint64WithFallback(createEvent.PoolBaseAmount, migrateEvent.MintAmount)
|
||||
quoteReserve := decimalFromUint64WithFallback(createEvent.PoolQuoteAmount, migrateEvent.SolAmount)
|
||||
|
||||
if _, exists := tx.Token[migrateEvent.Mint]; !exists {
|
||||
tx.Token[migrateEvent.Mint] = TokenMeta{
|
||||
@@ -661,22 +1135,22 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction
|
||||
Event: "migrate",
|
||||
Pool: migrateEvent.BondingCurve,
|
||||
BaseMint: migrateEvent.Mint,
|
||||
QuoteMint: solana.PublicKey{},
|
||||
QuoteMint: quoteMint,
|
||||
BaseTokenProgram: baseTokenProgram,
|
||||
QuoteTokenProgram: solana.PublicKey{},
|
||||
QuoteTokenProgram: quoteTokenProgram,
|
||||
Creator: createEvent.Creator,
|
||||
BaseMintDecimals: 6,
|
||||
QuoteMintDecimals: 9,
|
||||
QuoteMintDecimals: quoteDecimals,
|
||||
User: migrateEvent.User,
|
||||
//BaseAmount: decimal.Decimal{},
|
||||
//QuoteAmount: decimal.Decimal{},
|
||||
BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount),
|
||||
QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount),
|
||||
BaseReserve: baseReserve,
|
||||
QuoteReserve: quoteReserve,
|
||||
Mayhem: createEvent.IsMayhemMode,
|
||||
MigrateTopProgram: pumpAmmProgram,
|
||||
MigrateToPool: migrateEvent.Pool,
|
||||
UserBaseBalance: userBase,
|
||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
||||
UserQuoteBalance: userQuote,
|
||||
EntryContract: entryContract,
|
||||
},
|
||||
}
|
||||
@@ -685,20 +1159,20 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction
|
||||
Event: "create",
|
||||
Pool: migrateEvent.Pool,
|
||||
BaseMint: migrateEvent.Mint,
|
||||
QuoteMint: wSolMint,
|
||||
QuoteMint: quoteMint,
|
||||
BaseTokenProgram: baseTokenProgram,
|
||||
QuoteTokenProgram: solana.TokenProgramID,
|
||||
QuoteTokenProgram: quoteTokenProgram,
|
||||
Creator: createEvent.Creator,
|
||||
BaseMintDecimals: 6,
|
||||
QuoteMintDecimals: 9,
|
||||
QuoteMintDecimals: quoteDecimals,
|
||||
User: migrateEvent.User,
|
||||
BaseAmount: decimal.NewFromUint64(migrateEvent.MintAmount),
|
||||
QuoteAmount: decimal.NewFromUint64(migrateEvent.SolAmount),
|
||||
BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount),
|
||||
QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount),
|
||||
BaseAmount: baseAmount,
|
||||
QuoteAmount: quoteAmount,
|
||||
BaseReserve: baseReserve,
|
||||
QuoteReserve: quoteReserve,
|
||||
Mayhem: createEvent.IsMayhemMode,
|
||||
UserBaseBalance: userBase,
|
||||
UserQuoteBalance: decimal.NewFromUint64(userQuote),
|
||||
UserQuoteBalance: userQuote,
|
||||
EntryContract: entryContract,
|
||||
})
|
||||
|
||||
|
||||
161
pump_test.go
161
pump_test.go
@@ -1,6 +1,7 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
@@ -100,3 +101,163 @@ func TestPumpCompleteMatchesTradeEvent(t *testing.T) {
|
||||
t.Fatal("pumpCompleteMatchesTradeEvent() = true for mismatched user")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPumpV2Discriminators(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
got [8]byte
|
||||
want [8]byte
|
||||
}{
|
||||
{name: "buy_exact_sol_in", got: pumpBuyExactSolInDiscriminator, want: [8]byte{56, 252, 116, 8, 158, 223, 205, 95}},
|
||||
{name: "buy_v2", got: pumpBuyV2Discriminator, want: [8]byte{184, 23, 238, 97, 103, 197, 211, 61}},
|
||||
{name: "buy_exact_quote_in_v2", got: pumpBuyExactQuoteInV2Discriminator, want: [8]byte{194, 171, 28, 70, 104, 77, 91, 47}},
|
||||
{name: "sell_v2", got: pumpSellV2Discriminator, want: [8]byte{93, 246, 130, 60, 231, 233, 64, 178}},
|
||||
{name: "create_v2", got: pumpCreateV2Discriminator, want: [8]byte{214, 144, 76, 236, 95, 139, 49, 180}},
|
||||
{name: "migrate_v2", got: pumpMigrateV2Discriminator, want: [8]byte{187, 203, 18, 31, 206, 237, 254, 41}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if tt.got != tt.want {
|
||||
t.Fatalf("%s discriminator = %v, want %v", tt.name, tt.got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPumpMigrateLayoutV2(t *testing.T) {
|
||||
accounts := make([]int, 27)
|
||||
for i := range accounts {
|
||||
accounts[i] = i
|
||||
}
|
||||
layout, ok := pumpMigrateLayout(Instruction{
|
||||
Data: pumpMigrateV2Discriminator[:],
|
||||
Accounts: accounts,
|
||||
})
|
||||
if !ok {
|
||||
t.Fatal("migrate_v2 layout not recognized")
|
||||
}
|
||||
if !layout.IsV2 ||
|
||||
layout.BaseMint != 2 ||
|
||||
layout.QuoteMint != 3 ||
|
||||
layout.Pool != 4 ||
|
||||
layout.BasePoolToken != 5 ||
|
||||
layout.QuotePoolToken != 6 ||
|
||||
layout.User != 7 ||
|
||||
layout.BaseTokenProgram != 19 ||
|
||||
layout.QuoteTokenProgram != 20 {
|
||||
t.Fatalf("migrate_v2 layout = %+v", layout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPumpTradeAmountInfoV2(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
disc [8]byte
|
||||
wantMode SwapMode
|
||||
}{
|
||||
{name: "legacy exact quote in", disc: pumpBuyExactSolInDiscriminator, wantMode: SwapModeExactIn},
|
||||
{name: "v2 exact quote in", disc: pumpBuyExactQuoteInV2Discriminator, wantMode: SwapModeExactIn},
|
||||
{name: "v2 buy exact out", disc: pumpBuyV2Discriminator, wantMode: SwapModeExactOut},
|
||||
{name: "v2 sell exact in", disc: pumpSellV2Discriminator, wantMode: SwapModeExactIn},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
mode, fixed, limit, ok := pumpTradeAmountInfoFromArgs(PumpTradeArgs{
|
||||
Discriminator: tt.disc,
|
||||
Amount1: 11,
|
||||
Amount2: 22,
|
||||
})
|
||||
if !ok {
|
||||
t.Fatalf("%s not recognized", tt.name)
|
||||
}
|
||||
if mode != tt.wantMode {
|
||||
t.Fatalf("%s mode = %s, want %s", tt.name, mode.String(), tt.wantMode.String())
|
||||
}
|
||||
if fixed.String() != "11" || limit.String() != "22" {
|
||||
t.Fatalf("%s fixed/limit = %s/%s, want 11/22", tt.name, fixed, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPumpCreateQuoteAccountsOptional(t *testing.T) {
|
||||
createAccounts := make([]int, 17)
|
||||
for i := range createAccounts {
|
||||
createAccounts[i] = i
|
||||
}
|
||||
createV2Accounts := make([]int, 19)
|
||||
for i := range createV2Accounts {
|
||||
createV2Accounts[i] = i
|
||||
}
|
||||
createV2Accounts[16] = 14
|
||||
createV2Accounts[18] = 16
|
||||
accountList := make([]solana.PublicKey, 19)
|
||||
accountList[14] = usdcMint
|
||||
accountList[16] = solana.TokenProgramID
|
||||
accountList[18] = solana.TokenProgramID
|
||||
result := &RawTx{accountList: accountList}
|
||||
|
||||
quoteMint, quoteTokenProgram, quoteDecimals := pumpCreateQuoteAccounts(result, Instruction{
|
||||
Data: pumpCreateDiscriminator[:],
|
||||
Accounts: createAccounts,
|
||||
}, PumpCreateEvent{})
|
||||
if !quoteMint.IsZero() || !quoteTokenProgram.IsZero() || quoteDecimals != 9 {
|
||||
t.Fatalf("create quote accounts = %s/%s/%d, want zero/zero/9", quoteMint, quoteTokenProgram, quoteDecimals)
|
||||
}
|
||||
|
||||
quoteMint, quoteTokenProgram, quoteDecimals = pumpCreateQuoteAccounts(result, Instruction{
|
||||
Data: pumpCreateV2Discriminator[:],
|
||||
Accounts: createV2Accounts,
|
||||
}, PumpCreateEvent{})
|
||||
if !quoteMint.Equals(usdcMint) || !quoteTokenProgram.Equals(solana.TokenProgramID) || quoteDecimals != 6 {
|
||||
t.Fatalf("create_v2 quote accounts = %s/%s/%d, want USDC/token/6", quoteMint, quoteTokenProgram, quoteDecimals)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePumpTradeEventV2QuoteFields(t *testing.T) {
|
||||
user := solana.MustPublicKeyFromBase58("DS95KxqUCCjwQaXhD7fhKatXbivwWDNrJdNV5ZcubGdz")
|
||||
mint := solana.MustPublicKeyFromBase58("8GNGkNnfBuoTP3QRnmdNzSYuuE15M8tvcNvxNsV4pump")
|
||||
want := PumpTradeEvent{
|
||||
Mint: mint,
|
||||
SolAmount: 1,
|
||||
TokenAmount: 2,
|
||||
IsBuy: true,
|
||||
User: user,
|
||||
VirtualTokenReserves: 3,
|
||||
RealTokenReserves: 4,
|
||||
IxName: "buy_v2",
|
||||
Shareholders: []PumpShareholder{{Address: user, ShareBps: 250}},
|
||||
QuoteMint: usdcMint,
|
||||
QuoteAmount: 5,
|
||||
VirtualQuoteReserves: 6,
|
||||
RealQuoteReserves: 7,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := agbinary.NewBorshEncoder(&buf).Encode(want); err != nil {
|
||||
t.Fatalf("encode v2 trade event: %v", err)
|
||||
}
|
||||
got, err := decodePumpTradeEvent(buf.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("decodePumpTradeEvent() error = %v", err)
|
||||
}
|
||||
if !got.QuoteMint.Equals(usdcMint) || got.QuoteAmount != 5 || got.VirtualQuoteReserves != 6 || got.RealQuoteReserves != 7 {
|
||||
t.Fatalf("decoded quote fields = %s/%d/%d/%d", got.QuoteMint, got.QuoteAmount, got.VirtualQuoteReserves, got.RealQuoteReserves)
|
||||
}
|
||||
if len(got.Shareholders) != 1 || got.Shareholders[0].ShareBps != 250 {
|
||||
t.Fatalf("decoded shareholders = %+v", got.Shareholders)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePumpTradeEventLegacyFallback(t *testing.T) {
|
||||
hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e"
|
||||
data, err := hex.DecodeString(hexData)
|
||||
if err != nil {
|
||||
t.Fatalf("decode hex: %v", err)
|
||||
}
|
||||
got, err := decodePumpTradeEvent(data[16:])
|
||||
if err != nil {
|
||||
t.Fatalf("decodePumpTradeEvent() legacy error = %v", err)
|
||||
}
|
||||
if got.IxName != "buy_exact_sol_in" || got.SolAmount != 11725956 || !got.IsBuy {
|
||||
t.Fatalf("legacy event = %+v", got)
|
||||
}
|
||||
if !got.QuoteMint.IsZero() || got.QuoteAmount != 0 || got.RealQuoteReserves != 0 {
|
||||
t.Fatalf("legacy quote fields = %s/%d/%d, want zero", got.QuoteMint, got.QuoteAmount, got.RealQuoteReserves)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,6 +616,10 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
||||
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
|
||||
}
|
||||
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
|
||||
quoteAmount := decimal.NewFromUint64(event.UserQuoteAmountIn)
|
||||
if event.IxName == "buy" {
|
||||
quoteAmount = decimal.NewFromUint64(event.QuoteAmountIn)
|
||||
}
|
||||
swap := Swap{
|
||||
Program: SolProgramPumpAMM,
|
||||
Event: "buy",
|
||||
@@ -629,7 +633,7 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
||||
QuoteMintDecimals: quoteMintDecimals,
|
||||
User: eventUser,
|
||||
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
|
||||
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountIn),
|
||||
QuoteAmount: quoteAmount,
|
||||
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
|
||||
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
|
||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
||||
|
||||
181
rawtx.go
181
rawtx.go
@@ -1,8 +1,11 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
bin "github.com/gagliardetto/binary"
|
||||
@@ -109,6 +112,7 @@ type Instruction struct {
|
||||
Data solana.Base58 `json:"data"`
|
||||
ProgramIDIndex int `json:"programIdIndex"`
|
||||
StackHeight *int `json:"stackHeight"`
|
||||
LogEvents []solana.Base64 `json:"logEvents,omitempty"`
|
||||
}
|
||||
type InnerInstructions struct {
|
||||
Index int `json:"index"`
|
||||
@@ -180,6 +184,11 @@ type Transaction struct {
|
||||
Signatures []solana.Signature `json:"signatures"`
|
||||
}
|
||||
|
||||
type RawTxConvertOptions struct {
|
||||
IgnoreLogMessages bool
|
||||
ParseLogEvents bool
|
||||
}
|
||||
|
||||
func (tx *Transaction) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 || (len(data) == 4 && string(data) == "null") {
|
||||
// TODO: is this an error?
|
||||
@@ -308,7 +317,8 @@ func marshalRpcTransactionErr(err any) string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, slot uint64, index int64) (*RawTx, error) {
|
||||
func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, slot uint64, index int64, options ...RawTxConvertOptions) (*RawTx, error) {
|
||||
option := rawTxConvertOption(options)
|
||||
created := int64(0)
|
||||
if blockTime != nil {
|
||||
created = int64(*blockTime)
|
||||
@@ -523,6 +533,8 @@ func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, s
|
||||
})
|
||||
}
|
||||
|
||||
applyRawTxConvertLogOptions(sTx, option)
|
||||
|
||||
return sTx, nil
|
||||
}
|
||||
|
||||
@@ -833,7 +845,8 @@ func isAccountOwner(account, owner, mint solana.PublicKey) (bool, error) {
|
||||
return account == ata, nil
|
||||
}
|
||||
|
||||
func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateTransaction, created int64) (*RawTx, error) {
|
||||
func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateTransaction, created int64, options ...RawTxConvertOptions) (*RawTx, error) {
|
||||
option := rawTxConvertOption(options)
|
||||
sTx := &RawTx{
|
||||
BlockTime: created,
|
||||
Slot: y.Slot,
|
||||
@@ -1002,6 +1015,8 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT
|
||||
})
|
||||
}
|
||||
|
||||
applyRawTxConvertLogOptions(sTx, option)
|
||||
|
||||
// resolve the lookups
|
||||
//{
|
||||
// if sTx.Transaction.Message.IsVersioned() {
|
||||
@@ -1021,6 +1036,168 @@ func newInt16(x uint16) *int {
|
||||
return &y
|
||||
}
|
||||
|
||||
func rawTxConvertOption(options []RawTxConvertOptions) RawTxConvertOptions {
|
||||
out := RawTxConvertOptions{ParseLogEvents: true}
|
||||
if len(options) > 0 {
|
||||
out = options[0]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func applyRawTxConvertLogOptions(tx *RawTx, option RawTxConvertOptions) {
|
||||
if tx == nil {
|
||||
return
|
||||
}
|
||||
if option.ParseLogEvents {
|
||||
attachLogEventsToInstructions(tx, tx.Meta.LogMessages)
|
||||
}
|
||||
if option.IgnoreLogMessages {
|
||||
tx.Meta.LogMessages = nil
|
||||
}
|
||||
}
|
||||
|
||||
type instructionLogFrame struct {
|
||||
program string
|
||||
instr *Instruction
|
||||
}
|
||||
|
||||
type instructionLogTarget struct {
|
||||
program string
|
||||
stackHeight int
|
||||
instr *Instruction
|
||||
}
|
||||
|
||||
func attachLogEventsToInstructions(tx *RawTx, logMessages []string) {
|
||||
if tx == nil || len(logMessages) == 0 {
|
||||
return
|
||||
}
|
||||
targets := rawTxInstructionLogTargets(tx)
|
||||
if len(targets) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
nextTarget := 0
|
||||
var stack []instructionLogFrame
|
||||
for _, logMessage := range logMessages {
|
||||
if program, stackHeight, ok := parseProgramInvokeLog(logMessage); ok {
|
||||
var instr *Instruction
|
||||
for nextTarget < len(targets) {
|
||||
target := targets[nextTarget]
|
||||
nextTarget++
|
||||
if target.program != program {
|
||||
continue
|
||||
}
|
||||
if target.stackHeight != 0 && target.stackHeight != stackHeight {
|
||||
continue
|
||||
}
|
||||
instr = target.instr
|
||||
break
|
||||
}
|
||||
stack = append(stack, instructionLogFrame{program: program, instr: instr})
|
||||
continue
|
||||
}
|
||||
|
||||
if data, ok := parseProgramDataLog(logMessage); ok {
|
||||
if len(stack) == 0 {
|
||||
continue
|
||||
}
|
||||
top := stack[len(stack)-1]
|
||||
if top.instr != nil {
|
||||
top.instr.LogEvents = append(top.instr.LogEvents, data)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if program, ok := parseProgramFinishedLog(logMessage); ok {
|
||||
for i := len(stack) - 1; i >= 0; i-- {
|
||||
if stack[i].program == program {
|
||||
stack = stack[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rawTxInstructionLogTargets(tx *RawTx) []instructionLogTarget {
|
||||
accountList := tx.getAccountList()
|
||||
innerByOuter := make(map[int]*InnerInstructions, len(tx.Meta.InnerInstructions))
|
||||
for i := range tx.Meta.InnerInstructions {
|
||||
inner := &tx.Meta.InnerInstructions[i]
|
||||
innerByOuter[inner.Index] = inner
|
||||
}
|
||||
|
||||
out := make([]instructionLogTarget, 0, len(tx.Transaction.Message.Instructions))
|
||||
for i := range tx.Transaction.Message.Instructions {
|
||||
instr := &tx.Transaction.Message.Instructions[i]
|
||||
if instr.ProgramIDIndex >= 0 && instr.ProgramIDIndex < len(accountList) {
|
||||
out = append(out, instructionLogTarget{
|
||||
program: accountList[instr.ProgramIDIndex].String(),
|
||||
stackHeight: 1,
|
||||
instr: instr,
|
||||
})
|
||||
}
|
||||
if inner := innerByOuter[i]; inner != nil {
|
||||
for j := range inner.Instructions {
|
||||
innerInstr := &inner.Instructions[j]
|
||||
if innerInstr.ProgramIDIndex < 0 || innerInstr.ProgramIDIndex >= len(accountList) {
|
||||
continue
|
||||
}
|
||||
stackHeight := 0
|
||||
if innerInstr.StackHeight != nil {
|
||||
stackHeight = *innerInstr.StackHeight
|
||||
}
|
||||
out = append(out, instructionLogTarget{
|
||||
program: accountList[innerInstr.ProgramIDIndex].String(),
|
||||
stackHeight: stackHeight,
|
||||
instr: innerInstr,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseProgramInvokeLog(logMessage string) (string, int, bool) {
|
||||
if !strings.HasPrefix(logMessage, "Program ") {
|
||||
return "", 0, false
|
||||
}
|
||||
rest := strings.TrimPrefix(logMessage, "Program ")
|
||||
program, suffix, ok := strings.Cut(rest, " invoke [")
|
||||
if !ok {
|
||||
return "", 0, false
|
||||
}
|
||||
suffix = strings.TrimSuffix(suffix, "]")
|
||||
stackHeight, err := strconv.Atoi(suffix)
|
||||
if err != nil {
|
||||
return "", 0, false
|
||||
}
|
||||
return program, stackHeight, true
|
||||
}
|
||||
|
||||
func parseProgramDataLog(logMessage string) (solana.Base64, bool) {
|
||||
if !strings.HasPrefix(logMessage, "Program data: ") {
|
||||
return nil, false
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(strings.TrimSpace(strings.TrimPrefix(logMessage, "Program data: ")))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return solana.Base64(data), true
|
||||
}
|
||||
|
||||
func parseProgramFinishedLog(logMessage string) (string, bool) {
|
||||
if !strings.HasPrefix(logMessage, "Program ") {
|
||||
return "", false
|
||||
}
|
||||
if !strings.HasSuffix(logMessage, " success") && !strings.Contains(logMessage, " failed:") {
|
||||
return "", false
|
||||
}
|
||||
rest := strings.TrimPrefix(logMessage, "Program ")
|
||||
program, _, ok := strings.Cut(rest, " ")
|
||||
return program, ok
|
||||
}
|
||||
|
||||
func newInt(x *uint32) *int {
|
||||
if x == nil {
|
||||
return nil
|
||||
|
||||
1812
rawtx_binary.go
Normal file
1812
rawtx_binary.go
Normal file
File diff suppressed because it is too large
Load Diff
434
rawtx_binary_test.go
Normal file
434
rawtx_binary_test.go
Normal file
@@ -0,0 +1,434 @@
|
||||
package pump_parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
"github.com/gagliardetto/solana-go/rpc"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func TestRawTxBinaryRoundTripRealFixture(t *testing.T) {
|
||||
original := mustLoadRawTxFixture(t, "testdata/rpc/4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri.json")
|
||||
|
||||
encoded, err := EncodeRawTxBinary(original)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeRawTxBinary() error = %v", err)
|
||||
}
|
||||
|
||||
decoded, err := DecodeRawTxBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxBinary() error = %v", err)
|
||||
}
|
||||
|
||||
assertRawTxAccountAccess(t, decoded)
|
||||
if decoded.TxHash() != original.TxHash() {
|
||||
t.Fatalf("TxHash = %s, want %s", decoded.TxHash(), original.TxHash())
|
||||
}
|
||||
if decoded.Slot != original.Slot {
|
||||
t.Fatalf("Slot = %d, want %d", decoded.Slot, original.Slot)
|
||||
}
|
||||
if decoded.IndexWithinBlock != original.IndexWithinBlock {
|
||||
t.Fatalf("IndexWithinBlock = %d, want %d", decoded.IndexWithinBlock, original.IndexWithinBlock)
|
||||
}
|
||||
if len(decoded.Meta.PostTokenBalances) != len(original.Meta.PostTokenBalances) {
|
||||
t.Fatalf("PostTokenBalances len = %d, want %d", len(decoded.Meta.PostTokenBalances), len(original.Meta.PostTokenBalances))
|
||||
}
|
||||
if len(decoded.Meta.PostTokenBalances) > 0 {
|
||||
got := decoded.Meta.PostTokenBalances[0]
|
||||
want := original.Meta.PostTokenBalances[0]
|
||||
if got.AccountIndex != want.AccountIndex {
|
||||
t.Fatalf("token balance account index = %d, want %d", got.AccountIndex, want.AccountIndex)
|
||||
}
|
||||
wantMint := want.MintAccount
|
||||
if wantMint.IsZero() && want.Mint != "" {
|
||||
var err error
|
||||
wantMint, err = solana.PublicKeyFromBase58(want.Mint)
|
||||
if err != nil {
|
||||
t.Fatalf("parse want mint: %v", err)
|
||||
}
|
||||
}
|
||||
if got.MintAccount != wantMint {
|
||||
t.Fatalf("token balance mint = %s, want %s", got.MintAccount, wantMint)
|
||||
}
|
||||
if got.UITokenAmount.Decimals != want.UITokenAmount.Decimals {
|
||||
t.Fatalf("token balance decimals = %d, want %d", got.UITokenAmount.Decimals, want.UITokenAmount.Decimals)
|
||||
}
|
||||
if got.UITokenAmount.Amount != want.UITokenAmount.Amount {
|
||||
t.Fatalf("token balance amount = %s, want %s", got.UITokenAmount.Amount, want.UITokenAmount.Amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawTxsBinaryBatchAndStreamRoundTrip(t *testing.T) {
|
||||
tx1 := mustLoadRawTxFixture(t, "testdata/rpc/4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri.json")
|
||||
tx2 := mustLoadRawTxFixture(t, "testdata/rpc/43EouSYkeVmLBZSKW1KptiQcAVvB6KX49wjztjgzkQ9iU38A6fF68k77bNy9Wn6fwjykqYsPorUKj8m6SFY7naf1.json")
|
||||
tx2.BlockTime = tx1.BlockTime
|
||||
original := []RawTx{*tx1, *tx2}
|
||||
|
||||
encoded, err := EncodeRawTxsBinary(original)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeRawTxsBinary() error = %v", err)
|
||||
}
|
||||
|
||||
decoded, err := DecodeRawTxsBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxsBinary() error = %v", err)
|
||||
}
|
||||
if len(decoded) != len(original) {
|
||||
t.Fatalf("DecodeRawTxsBinary len = %d, want %d", len(decoded), len(original))
|
||||
}
|
||||
for i := range decoded {
|
||||
assertRawTxAccountAccess(t, decoded[i])
|
||||
if decoded[i].TxHash() != original[i].TxHash() {
|
||||
t.Fatalf("decoded[%d].TxHash = %s, want %s", i, decoded[i].TxHash(), original[i].TxHash())
|
||||
}
|
||||
if decoded[i].BlockTime != original[i].BlockTime {
|
||||
t.Fatalf("decoded[%d].BlockTime = %d, want %d", i, decoded[i].BlockTime, original[i].BlockTime)
|
||||
}
|
||||
}
|
||||
|
||||
var streamed int
|
||||
for decodedTx, err := range DecodeRawTxsBinaryReader(bytes.NewReader(encoded)) {
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxsBinaryReader() error = %v", err)
|
||||
}
|
||||
assertRawTxAccountAccess(t, decodedTx)
|
||||
streamed++
|
||||
}
|
||||
if streamed != len(original) {
|
||||
t.Fatalf("streamed tx count = %d, want %d", streamed, len(original))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawTxBlocksBinaryRoundTrip(t *testing.T) {
|
||||
tx1 := mustLoadRawTxFixture(t, "testdata/rpc/4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri.json")
|
||||
tx2 := mustLoadRawTxFixture(t, "testdata/rpc/43EouSYkeVmLBZSKW1KptiQcAVvB6KX49wjztjgzkQ9iU38A6fF68k77bNy9Wn6fwjykqYsPorUKj8m6SFY7naf1.json")
|
||||
blocks := [][]RawTx{{*tx1}, {*tx2}}
|
||||
|
||||
encoded, err := EncodeRawTxBlocksBinary(blocks)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeRawTxBlocksBinary() error = %v", err)
|
||||
}
|
||||
|
||||
decoded, err := DecodeRawTxBlocksBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxBlocksBinary() error = %v", err)
|
||||
}
|
||||
if len(decoded) != len(blocks) {
|
||||
t.Fatalf("block count = %d, want %d", len(decoded), len(blocks))
|
||||
}
|
||||
for blockIndex := range decoded {
|
||||
if len(decoded[blockIndex]) != len(blocks[blockIndex]) {
|
||||
t.Fatalf("block[%d] tx count = %d, want %d", blockIndex, len(decoded[blockIndex]), len(blocks[blockIndex]))
|
||||
}
|
||||
for txIndex := range decoded[blockIndex] {
|
||||
assertRawTxAccountAccess(t, decoded[blockIndex][txIndex])
|
||||
if decoded[blockIndex][txIndex].TxHash() != blocks[blockIndex][txIndex].TxHash() {
|
||||
t.Fatalf("block[%d].tx[%d] hash mismatch", blockIndex, txIndex)
|
||||
}
|
||||
if decoded[blockIndex][txIndex].BlockTime != blocks[blockIndex][txIndex].BlockTime {
|
||||
t.Fatalf("block[%d].tx[%d] block time = %d, want %d", blockIndex, txIndex, decoded[blockIndex][txIndex].BlockTime, blocks[blockIndex][txIndex].BlockTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawTxBinaryPreservesAccountListHelperBehavior(t *testing.T) {
|
||||
owner := mustPubKey("SysvarRent111111111111111111111111111111111")
|
||||
mint := solana.WrappedSol
|
||||
tokenProgram := solana.TokenProgramID
|
||||
ata, _, err := solana.FindProgramAddress([][]byte{
|
||||
owner[:],
|
||||
tokenProgram[:],
|
||||
mint[:],
|
||||
}, solana.SPLAssociatedTokenAccountProgramID)
|
||||
if err != nil {
|
||||
t.Fatalf("find ata: %v", err)
|
||||
}
|
||||
|
||||
original := &RawTx{
|
||||
accountList: []solana.PublicKey{owner, ata, mint, tokenProgram},
|
||||
BlockTime: 1710000000,
|
||||
Slot: 123,
|
||||
IndexWithinBlock: 7,
|
||||
Transaction: Transaction{Signatures: []solana.Signature{{1, 2, 3}}},
|
||||
Meta: Meta{
|
||||
PreBalances: []uint64{2_000_000_000, 0, 0, 0},
|
||||
PostBalances: []uint64{1_500_000_000, 0, 0, 0},
|
||||
PreTokenBalances: []TokenBalance{{
|
||||
AccountIndex: 1,
|
||||
MintAccount: mint,
|
||||
OwnerAccount: &owner,
|
||||
ProgramIDAccount: tokenProgram,
|
||||
UITokenAmount: UITokenAmount{
|
||||
Amount: "100",
|
||||
Decimals: 9,
|
||||
UIAmount: 0.0000001,
|
||||
UIAmountString: "0.0000001",
|
||||
},
|
||||
}},
|
||||
PostTokenBalances: []TokenBalance{{
|
||||
AccountIndex: 1,
|
||||
MintAccount: mint,
|
||||
OwnerAccount: &owner,
|
||||
ProgramIDAccount: tokenProgram,
|
||||
UITokenAmount: UITokenAmount{
|
||||
Amount: "250",
|
||||
Decimals: 9,
|
||||
UIAmount: 0.00000025,
|
||||
UIAmountString: "0.00000025",
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
binaryForm, err := NewRawTxBinary(original)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRawTxBinary() error = %v", err)
|
||||
}
|
||||
if len(binaryForm.Meta.TokenBalances) != 1 {
|
||||
t.Fatalf("binary token balance count = %d, want 1", len(binaryForm.Meta.TokenBalances))
|
||||
}
|
||||
if got := binaryForm.Meta.TokenBalances[0]; !got.HasPreAmount || !got.HasPostAmount || got.PreAmount != "100" || got.PostAmount != "250" {
|
||||
t.Fatalf("merged binary token balance = %+v", got)
|
||||
}
|
||||
|
||||
encoded, err := EncodeRawTxBinary(original)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeRawTxBinary() error = %v", err)
|
||||
}
|
||||
decoded, err := DecodeRawTxBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxBinary() error = %v", err)
|
||||
}
|
||||
|
||||
for _, tx := range []*RawTx{original, decoded} {
|
||||
if got, err := GetSolAfterTx(tx, 0); err != nil || got != 1_500_000_000 {
|
||||
t.Fatalf("GetSolAfterTx() = %d, %v; want 1500000000, nil", got, err)
|
||||
}
|
||||
balance, err := getTokenBalanceAfterTx(tx, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("getTokenBalanceAfterTx() error = %v", err)
|
||||
}
|
||||
if balance.UITokenAmount.Amount != "250" {
|
||||
t.Fatalf("getTokenBalanceAfterTx amount = %s, want 250", balance.UITokenAmount.Amount)
|
||||
}
|
||||
if got := getAccountBalanceAfterTx(tx, 1); !got.Equal(decimal.NewFromInt(250)) {
|
||||
t.Fatalf("getAccountBalanceAfterTx() = %s, want 250", got)
|
||||
}
|
||||
if got := GetTokenBalanceAfterTx(tx, 0, tokenProgram, mint); !got.Equal(decimal.NewFromInt(250)) {
|
||||
t.Fatalf("GetTokenBalanceAfterTx() = %s, want 250", got)
|
||||
}
|
||||
ataBalance, err := getAtaByOwner(tx, owner, mint)
|
||||
if err != nil {
|
||||
t.Fatalf("getAtaByOwner() error = %v", err)
|
||||
}
|
||||
if ataBalance.AccountIndex != 1 {
|
||||
t.Fatalf("getAtaByOwner account index = %d, want 1", ataBalance.AccountIndex)
|
||||
}
|
||||
ataIndex, err := getAtaIdxByOwner(tx, owner, mint)
|
||||
if err != nil {
|
||||
t.Fatalf("getAtaIdxByOwner() error = %v", err)
|
||||
}
|
||||
if ataIndex != 1 {
|
||||
t.Fatalf("getAtaIdxByOwner() = %d, want 1", ataIndex)
|
||||
}
|
||||
change, changeAtaIndex := tokenBalanceChange(tx, 0, tokenProgram, mint)
|
||||
if !change.Equal(decimal.NewFromInt(150)) || changeAtaIndex != 1 {
|
||||
t.Fatalf("tokenBalanceChange() = (%s, %d), want (150, 1)", change, changeAtaIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawTxBinaryTokenBalanceAmountSupportsUint256(t *testing.T) {
|
||||
owner := mustPubKey("SysvarRent111111111111111111111111111111111")
|
||||
mint := solana.WrappedSol
|
||||
tokenProgram := solana.TokenProgramID
|
||||
amount := "340282366920938463463374607431768211455"
|
||||
original := &RawTx{
|
||||
accountList: []solana.PublicKey{owner, mint, tokenProgram},
|
||||
Transaction: Transaction{
|
||||
Signatures: []solana.Signature{{1, 2, 3}},
|
||||
},
|
||||
Meta: Meta{
|
||||
PreBalances: []uint64{1},
|
||||
PostBalances: []uint64{1},
|
||||
PostTokenBalances: []TokenBalance{{
|
||||
AccountIndex: 0,
|
||||
MintAccount: mint,
|
||||
OwnerAccount: &owner,
|
||||
ProgramIDAccount: tokenProgram,
|
||||
UITokenAmount: UITokenAmount{
|
||||
Amount: amount,
|
||||
Decimals: 9,
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
encoded, err := EncodeRawTxBinary(original)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeRawTxBinary() error = %v", err)
|
||||
}
|
||||
decoded, err := DecodeRawTxBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxBinary() error = %v", err)
|
||||
}
|
||||
got := decoded.Meta.PostTokenBalances[0].UITokenAmount
|
||||
if got.Amount != amount {
|
||||
t.Fatalf("Amount = %s, want %s", got.Amount, amount)
|
||||
}
|
||||
if got.UIAmountString != "340282366920938463463374607431.768211455" {
|
||||
t.Fatalf("UIAmountString = %s", got.UIAmountString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawTxBlocksBinarySaveSlots414696178To414696182(t *testing.T) {
|
||||
rpcURL := os.Getenv("RAWTX_BINARY_RPC_URL")
|
||||
if rpcURL == "" {
|
||||
t.Skip("set RAWTX_BINARY_RPC_URL to run RPC-backed rawtx-binary block save test")
|
||||
}
|
||||
|
||||
const startSlot uint64 = 414696178
|
||||
const endSlot uint64 = 414696182
|
||||
|
||||
client := rpc.New(rpcURL)
|
||||
rewards := false
|
||||
version := uint64(0)
|
||||
blocks := make([][]RawTx, 0, endSlot-startSlot+1)
|
||||
totalTx := 0
|
||||
filteredVote := 0
|
||||
for slot := startSlot; slot <= endSlot; slot++ {
|
||||
block, err := client.GetBlockWithOpts(context.Background(), slot, &rpc.GetBlockOpts{
|
||||
TransactionDetails: rpc.TransactionDetailsFull,
|
||||
Rewards: &rewards,
|
||||
Commitment: rpc.CommitmentFinalized,
|
||||
Encoding: solana.EncodingBase64,
|
||||
MaxSupportedTransactionVersion: &version,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get block %d: %v", slot, err)
|
||||
}
|
||||
var blockTime uint64
|
||||
if block.BlockTime != nil {
|
||||
blockTime = uint64(*block.BlockTime)
|
||||
}
|
||||
|
||||
rawTxs := make([]RawTx, 0, len(block.Transactions))
|
||||
for i, tx := range block.Transactions {
|
||||
totalTx++
|
||||
rawTx, err := FromRpcTransactionWithMeta(tx, &blockTime, slot, int64(i))
|
||||
if err != nil {
|
||||
t.Fatalf("slot %d tx[%d] convert: %v", slot, i, err)
|
||||
}
|
||||
if rawTxBinaryIsVoteTx(rawTx) {
|
||||
filteredVote++
|
||||
continue
|
||||
}
|
||||
rawTxs = append(rawTxs, *rawTx)
|
||||
}
|
||||
blocks = append(blocks, rawTxs)
|
||||
}
|
||||
|
||||
encoded, err := EncodeRawTxBlocksBinary(blocks)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeRawTxBlocksBinary() error = %v", err)
|
||||
}
|
||||
outputPath := os.Getenv("RAWTX_BINARY_OUT")
|
||||
if outputPath == "" {
|
||||
outputPath = filepath.Join("testdata", "rawtx-binary", "rawtx-blocks-414696178-414696182.prbs")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
||||
t.Fatalf("create rawtx binary output dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(outputPath, encoded, 0o644); err != nil {
|
||||
t.Fatalf("write rawtx binary: %v", err)
|
||||
}
|
||||
|
||||
decoded, err := DecodeRawTxBlocksBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeRawTxBlocksBinary() error = %v", err)
|
||||
}
|
||||
if len(decoded) != len(blocks) {
|
||||
t.Fatalf("decoded block count = %d, want %d", len(decoded), len(blocks))
|
||||
}
|
||||
savedTx := 0
|
||||
for blockIndex := range decoded {
|
||||
if len(decoded[blockIndex]) != len(blocks[blockIndex]) {
|
||||
t.Fatalf("decoded block[%d] tx count = %d, want %d", blockIndex, len(decoded[blockIndex]), len(blocks[blockIndex]))
|
||||
}
|
||||
for txIndex := range decoded[blockIndex] {
|
||||
assertRawTxAccountAccess(t, decoded[blockIndex][txIndex])
|
||||
if rawTxBinaryIsVoteTx(decoded[blockIndex][txIndex]) {
|
||||
t.Fatalf("decoded block[%d].tx[%d] is vote tx", blockIndex, txIndex)
|
||||
}
|
||||
savedTx++
|
||||
}
|
||||
}
|
||||
if savedTx == 0 {
|
||||
t.Fatal("saved tx count is zero")
|
||||
}
|
||||
t.Logf("saved rawtx binary: path=%s bytes=%d total_tx=%d saved_tx=%d filtered_vote=%d", outputPath, len(encoded), totalTx, savedTx, filteredVote)
|
||||
}
|
||||
|
||||
func mustLoadRawTxFixture(t *testing.T, path string) *RawTx {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read fixture: %v", err)
|
||||
}
|
||||
var response RPCResponse
|
||||
if err := json.Unmarshal(raw, &response); err != nil {
|
||||
t.Fatalf("unmarshal fixture: %v", err)
|
||||
}
|
||||
return &response.Result
|
||||
}
|
||||
|
||||
func assertRawTxAccountAccess(t *testing.T, tx *RawTx) {
|
||||
t.Helper()
|
||||
accounts := tx.GetAccountList()
|
||||
if len(accounts) == 0 {
|
||||
t.Fatal("decoded account list is empty")
|
||||
}
|
||||
for _, instr := range tx.Transaction.Message.Instructions {
|
||||
if instr.ProgramIDIndex < 0 || instr.ProgramIDIndex >= len(accounts) {
|
||||
t.Fatalf("instruction program id index %d out of range %d", instr.ProgramIDIndex, len(accounts))
|
||||
}
|
||||
for _, accountIndex := range instr.Accounts {
|
||||
if accountIndex < 0 || accountIndex >= len(accounts) {
|
||||
t.Fatalf("instruction account index %d out of range %d", accountIndex, len(accounts))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, inner := range tx.Meta.InnerInstructions {
|
||||
for _, instr := range inner.Instructions {
|
||||
if instr.ProgramIDIndex < 0 || instr.ProgramIDIndex >= len(accounts) {
|
||||
t.Fatalf("inner instruction program id index %d out of range %d", instr.ProgramIDIndex, len(accounts))
|
||||
}
|
||||
for _, accountIndex := range instr.Accounts {
|
||||
if accountIndex < 0 || accountIndex >= len(accounts) {
|
||||
t.Fatalf("inner instruction account index %d out of range %d", accountIndex, len(accounts))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rawTxBinaryIsVoteTx(tx *RawTx) bool {
|
||||
if tx == nil {
|
||||
return false
|
||||
}
|
||||
accountList := tx.GetAccountList()
|
||||
for _, instr := range tx.Transaction.Message.Instructions {
|
||||
if instr.ProgramIDIndex >= 0 && instr.ProgramIDIndex < len(accountList) && accountList[instr.ProgramIDIndex] == solana.VoteProgramID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user