Compare commits
8 Commits
v0.2.37
...
43659ea4e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43659ea4e4 | ||
|
|
6414e6a25f | ||
|
|
273e87b8ad | ||
|
|
bb858c643e | ||
|
|
a620df5837 | ||
|
|
36da96eeaf | ||
|
|
a765fafddd | ||
|
|
738e417167 |
589
cmd/analyze_rawtx_binary_size/main.go
Normal file
589
cmd/analyze_rawtx_binary_size/main.go
Normal file
@@ -0,0 +1,589 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
pump_parser "github.com/thloyi/pump-parser"
|
||||
)
|
||||
|
||||
type sizeStats struct {
|
||||
total uint64
|
||||
items map[string]uint64
|
||||
}
|
||||
|
||||
type txInnerDataStat struct {
|
||||
TxOrdinal int
|
||||
BlockIndex int
|
||||
IndexWithinBlock uint32
|
||||
Slot uint64
|
||||
Bytes uint64
|
||||
InstructionCount int
|
||||
}
|
||||
|
||||
func newSizeStats() *sizeStats {
|
||||
return &sizeStats{items: make(map[string]uint64)}
|
||||
}
|
||||
|
||||
func (s *sizeStats) add(name string, n uint64) {
|
||||
s.items[name] += n
|
||||
s.total += n
|
||||
}
|
||||
|
||||
func main() {
|
||||
filePath := flag.String("file", "testdata/rawtx-binary/rawtx-blocks-414696178-414696182.prbs", "path to RawTxBlocksBinary .prbs file")
|
||||
flag.Parse()
|
||||
|
||||
raw, err := os.ReadFile(*filePath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "read file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var blocks pump_parser.RawTxBlocksBinary
|
||||
if err := blocks.UnmarshalBinary(raw); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "decode rawtx blocks binary: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stats := analyzeRawTxBlocksBinary(&blocks)
|
||||
if stats.total != uint64(len(raw)) {
|
||||
fmt.Fprintf(os.Stderr, "size accounting mismatch: accounted=%d file=%d\n", stats.total, len(raw))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
printReport(*filePath, len(raw), &blocks, stats)
|
||||
fmt.Println()
|
||||
printInnerInstructionDataDistribution(&blocks)
|
||||
fmt.Println()
|
||||
printBalanceAnalysis(&blocks)
|
||||
}
|
||||
|
||||
func analyzeRawTxBlocksBinary(blocks *pump_parser.RawTxBlocksBinary) *sizeStats {
|
||||
stats := newSizeStats()
|
||||
stats.add("file.magic", 4)
|
||||
stats.add("file.schema_version", 2)
|
||||
stats.add("address_table.count", 4)
|
||||
stats.add("address_table.pubkeys", uint64(len(blocks.AddressTable))*32)
|
||||
stats.add("blocks.count", 4)
|
||||
stats.add("blocks.block_time", uint64(len(blocks.BlockTimes))*8)
|
||||
stats.add("blocks.tx_count", uint64(len(blocks.BlockTxCounts))*4)
|
||||
stats.add("txs.total_count", 4)
|
||||
|
||||
for i := range blocks.Txs {
|
||||
addTx(stats, &blocks.Txs[i])
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
func addTx(stats *sizeStats, tx *pump_parser.RawTxBinary) {
|
||||
stats.add("tx.index_within_block", 4)
|
||||
stats.add("tx.slot", 8)
|
||||
stats.add("tx.version", 1)
|
||||
stats.add("tx.account_key_count", 4)
|
||||
stats.add("tx.account_list.count", 4)
|
||||
stats.add("tx.account_list.refs", uint64(len(tx.AccountList))*4)
|
||||
|
||||
addMeta(stats, &tx.Meta)
|
||||
addTransaction(stats, &tx.Transaction)
|
||||
}
|
||||
|
||||
func addMeta(stats *sizeStats, meta *pump_parser.RawTxMetaBinary) {
|
||||
addErr(stats, meta.Err)
|
||||
stats.add("meta.fee", 8)
|
||||
addInnerInstructions(stats, meta.InnerInstructions)
|
||||
addLamportBalances(stats, meta.PreBalances, meta.PostBalances)
|
||||
addTokenBalances(stats, "meta.token_balances", meta.TokenBalances)
|
||||
stats.add("meta.compute_units_consumed", 8)
|
||||
}
|
||||
|
||||
func addErr(stats *sizeStats, errValue *pump_parser.TransactionParsedError) {
|
||||
stats.add("meta.err.present", 1)
|
||||
if errValue == nil {
|
||||
return
|
||||
}
|
||||
stats.add("meta.err.index", 1)
|
||||
stats.add("meta.err.variant", 4)
|
||||
stats.add("meta.err.enum", 4)
|
||||
stats.add("meta.err.custom_code", 4)
|
||||
}
|
||||
|
||||
func addTransaction(stats *sizeStats, tx *pump_parser.RawTxTransactionBinary) {
|
||||
stats.add("transaction.signature.present", 1)
|
||||
if tx.HasSignature {
|
||||
stats.add("transaction.signature.first", 64)
|
||||
}
|
||||
addHeader(stats)
|
||||
addInstructions(stats, "transaction.instructions", tx.Message.Instructions)
|
||||
addAddressTableLookups(stats, tx.Message.AddressTableLookups)
|
||||
}
|
||||
|
||||
func addHeader(stats *sizeStats) {
|
||||
stats.add("transaction.header.num_readonly_signed", 4)
|
||||
stats.add("transaction.header.num_readonly_unsigned", 4)
|
||||
stats.add("transaction.header.num_required_signatures", 4)
|
||||
}
|
||||
|
||||
func addInnerInstructions(stats *sizeStats, values []pump_parser.InnerInstructions) {
|
||||
stats.add("meta.inner_instructions.count", 4)
|
||||
for _, value := range values {
|
||||
stats.add("meta.inner_instructions.index", 4)
|
||||
addInstructions(stats, "meta.inner_instructions.instructions", value.Instructions)
|
||||
}
|
||||
}
|
||||
|
||||
func addInstructions(stats *sizeStats, prefix string, values []pump_parser.Instruction) {
|
||||
stats.add(prefix+".count", 4)
|
||||
for _, value := range values {
|
||||
stats.add(prefix+".program_id_index", 2)
|
||||
stats.add(prefix+".accounts.count", 4)
|
||||
stats.add(prefix+".accounts.refs", uint64(len(value.Accounts))*2)
|
||||
stats.add(prefix+".data.length", 4)
|
||||
stats.add(prefix+".data.bytes", uint64(len(value.Data)))
|
||||
stats.add(prefix+".stack_height.present", 1)
|
||||
if value.StackHeight != nil {
|
||||
stats.add(prefix+".stack_height.value", 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addAddressTableLookups(stats *sizeStats, values []pump_parser.RawTxAddressTableLookupBinary) {
|
||||
stats.add("transaction.address_table_lookups.count", 4)
|
||||
for _, value := range values {
|
||||
stats.add("transaction.address_table_lookups.account_key", 4)
|
||||
stats.add("transaction.address_table_lookups.writable.count", 4)
|
||||
stats.add("transaction.address_table_lookups.writable.indexes", uint64(len(value.WritableIndexes)))
|
||||
stats.add("transaction.address_table_lookups.readonly.count", 4)
|
||||
stats.add("transaction.address_table_lookups.readonly.indexes", uint64(len(value.ReadonlyIndexes)))
|
||||
}
|
||||
}
|
||||
|
||||
func addUint64Slice(stats *sizeStats, prefix string, count int) {
|
||||
stats.add(prefix+".count", 4)
|
||||
stats.add(prefix+".values", uint64(count)*8)
|
||||
}
|
||||
|
||||
func addLamportBalances(stats *sizeStats, preBalances []uint64, postBalances []uint64) {
|
||||
stats.add("meta.pre_balances.count_uvarint", uint64(uvarintLen(uint64(len(preBalances)))))
|
||||
for _, value := range preBalances {
|
||||
stats.add("meta.pre_balances.value_uvarint", uint64(uvarintLen(value)))
|
||||
}
|
||||
n := len(preBalances)
|
||||
if len(postBalances) < n {
|
||||
n = len(postBalances)
|
||||
}
|
||||
changed := 0
|
||||
for i := 0; i < n; i++ {
|
||||
if preBalances[i] != postBalances[i] {
|
||||
changed++
|
||||
}
|
||||
}
|
||||
stats.add("meta.post_balance_changes.count_uvarint", uint64(uvarintLen(uint64(changed))))
|
||||
for i := 0; i < n; i++ {
|
||||
if preBalances[i] == postBalances[i] {
|
||||
continue
|
||||
}
|
||||
stats.add("meta.post_balance_changes.index_uvarint", uint64(uvarintLen(uint64(i))))
|
||||
stats.add("meta.post_balance_changes.delta_uvarint", uint64(zigzagDeltaUvarintLen(preBalances[i], postBalances[i])))
|
||||
}
|
||||
}
|
||||
|
||||
func addTokenBalances(stats *sizeStats, prefix string, values []pump_parser.RawTxTokenBalanceBinary) {
|
||||
stats.add(prefix+".count", 4)
|
||||
for _, value := range values {
|
||||
stats.add(prefix+".account_index", 2)
|
||||
stats.add(prefix+".mint_ref", 2)
|
||||
stats.add(prefix+".owner.present", 1)
|
||||
if value.HasOwnerAccount {
|
||||
stats.add(prefix+".owner_ref", 2)
|
||||
}
|
||||
stats.add(prefix+".program_id_ref", 2)
|
||||
stats.add(prefix+".decimals", 1)
|
||||
stats.add(prefix+".pre_amount.present", 1)
|
||||
if value.HasPreAmount {
|
||||
stats.add(prefix+".pre_amount.length", 1)
|
||||
stats.add(prefix+".pre_amount.bytes", uint64(uint256ByteLen(value.PreAmount)))
|
||||
}
|
||||
stats.add(prefix+".post_amount.present", 1)
|
||||
if value.HasPostAmount {
|
||||
stats.add(prefix+".post_amount.length", 1)
|
||||
stats.add(prefix+".post_amount.bytes", uint64(uint256ByteLen(value.PostAmount)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func uint256ByteLen(value string) int {
|
||||
if value == "" || value == "0" {
|
||||
return 0
|
||||
}
|
||||
amount, ok := new(big.Int).SetString(value, 10)
|
||||
if !ok || amount.Sign() <= 0 {
|
||||
return 0
|
||||
}
|
||||
return len(amount.Bytes())
|
||||
}
|
||||
|
||||
func printReport(filePath string, fileSize int, blocks *pump_parser.RawTxBlocksBinary, stats *sizeStats) {
|
||||
type row struct {
|
||||
name string
|
||||
bytes uint64
|
||||
}
|
||||
rows := make([]row, 0, len(stats.items))
|
||||
for name, bytes := range stats.items {
|
||||
rows = append(rows, row{name: name, bytes: bytes})
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
if rows[i].bytes == rows[j].bytes {
|
||||
return rows[i].name < rows[j].name
|
||||
}
|
||||
return rows[i].bytes > rows[j].bytes
|
||||
})
|
||||
|
||||
fmt.Printf("file=%s\n", filePath)
|
||||
fmt.Printf("bytes=%d\n", fileSize)
|
||||
fmt.Printf("schema_version=%d\n", blocks.SchemaVersion)
|
||||
fmt.Printf("blocks=%d\n", len(blocks.BlockTxCounts))
|
||||
fmt.Printf("txs=%d\n", len(blocks.Txs))
|
||||
fmt.Printf("address_table_entries=%d\n", len(blocks.AddressTable))
|
||||
fmt.Println()
|
||||
fmt.Printf("%-56s %12s %8s\n", "field", "bytes", "pct")
|
||||
fmt.Printf("%-56s %12s %8s\n", "-----", "-----", "---")
|
||||
for _, row := range rows {
|
||||
fmt.Printf("%-56s %12d %7.2f%%\n", row.name, row.bytes, float64(row.bytes)*100/float64(fileSize))
|
||||
}
|
||||
}
|
||||
|
||||
func printInnerInstructionDataDistribution(blocks *pump_parser.RawTxBlocksBinary) {
|
||||
stats := collectInnerInstructionDataStats(blocks)
|
||||
values := make([]uint64, 0, len(stats))
|
||||
var total uint64
|
||||
var nonZero int
|
||||
for _, stat := range stats {
|
||||
values = append(values, stat.Bytes)
|
||||
total += stat.Bytes
|
||||
if stat.Bytes > 0 {
|
||||
nonZero++
|
||||
}
|
||||
}
|
||||
sort.Slice(values, func(i, j int) bool { return values[i] < values[j] })
|
||||
|
||||
fmt.Println("inner_instruction_data_bytes_per_tx")
|
||||
fmt.Printf("txs=%d nonzero_txs=%d total_bytes=%d avg=%.2f\n", len(stats), nonZero, total, avg(total, len(stats)))
|
||||
if len(values) > 0 {
|
||||
fmt.Printf("min=%d p50=%d p75=%d p90=%d p95=%d p99=%d max=%d\n",
|
||||
values[0],
|
||||
percentile(values, 0.50),
|
||||
percentile(values, 0.75),
|
||||
percentile(values, 0.90),
|
||||
percentile(values, 0.95),
|
||||
percentile(values, 0.99),
|
||||
values[len(values)-1],
|
||||
)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("%-16s %8s %8s\n", "bucket", "txs", "bytes")
|
||||
fmt.Printf("%-16s %8s %8s\n", "------", "---", "-----")
|
||||
for _, bucket := range innerDataBuckets(stats) {
|
||||
fmt.Printf("%-16s %8d %8d\n", bucket.label, bucket.count, bucket.bytes)
|
||||
}
|
||||
|
||||
sort.Slice(stats, func(i, j int) bool {
|
||||
if stats[i].Bytes == stats[j].Bytes {
|
||||
return stats[i].TxOrdinal < stats[j].TxOrdinal
|
||||
}
|
||||
return stats[i].Bytes > stats[j].Bytes
|
||||
})
|
||||
fmt.Println()
|
||||
fmt.Printf("%-6s %-5s %-8s %-12s %-8s %-10s\n", "rank", "block", "tx_index", "slot", "bytes", "inner_ix")
|
||||
fmt.Printf("%-6s %-5s %-8s %-12s %-8s %-10s\n", "----", "-----", "--------", "----", "-----", "--------")
|
||||
limit := 20
|
||||
if len(stats) < limit {
|
||||
limit = len(stats)
|
||||
}
|
||||
for i := 0; i < limit; i++ {
|
||||
stat := stats[i]
|
||||
fmt.Printf("%-6d %-5d %-8d %-12d %-8d %-10d\n", i+1, stat.BlockIndex, stat.IndexWithinBlock, stat.Slot, stat.Bytes, stat.InstructionCount)
|
||||
}
|
||||
}
|
||||
|
||||
func collectInnerInstructionDataStats(blocks *pump_parser.RawTxBlocksBinary) []txInnerDataStat {
|
||||
out := make([]txInnerDataStat, 0, len(blocks.Txs))
|
||||
txOffset := 0
|
||||
for blockIndex, count := range blocks.BlockTxCounts {
|
||||
for i := uint32(0); i < count; i++ {
|
||||
tx := &blocks.Txs[txOffset]
|
||||
var bytes uint64
|
||||
var instructionCount int
|
||||
for _, inner := range tx.Meta.InnerInstructions {
|
||||
for _, instruction := range inner.Instructions {
|
||||
bytes += uint64(len(instruction.Data))
|
||||
instructionCount++
|
||||
}
|
||||
}
|
||||
out = append(out, txInnerDataStat{
|
||||
TxOrdinal: txOffset,
|
||||
BlockIndex: blockIndex,
|
||||
IndexWithinBlock: tx.IndexWithinBlock,
|
||||
Slot: tx.Slot,
|
||||
Bytes: bytes,
|
||||
InstructionCount: instructionCount,
|
||||
})
|
||||
txOffset++
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func percentile(values []uint64, p float64) uint64 {
|
||||
if len(values) == 0 {
|
||||
return 0
|
||||
}
|
||||
idx := int(float64(len(values)-1) * p)
|
||||
return values[idx]
|
||||
}
|
||||
|
||||
func avg(total uint64, count int) float64 {
|
||||
if count == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(total) / float64(count)
|
||||
}
|
||||
|
||||
type innerDataBucket struct {
|
||||
label string
|
||||
count int
|
||||
bytes uint64
|
||||
}
|
||||
|
||||
func innerDataBuckets(stats []txInnerDataStat) []innerDataBucket {
|
||||
buckets := []innerDataBucket{
|
||||
{label: "0"},
|
||||
{label: "1-63"},
|
||||
{label: "64-127"},
|
||||
{label: "128-255"},
|
||||
{label: "256-511"},
|
||||
{label: "512-1023"},
|
||||
{label: "1024-2047"},
|
||||
{label: "2048-4095"},
|
||||
{label: "4096+"},
|
||||
}
|
||||
for _, stat := range stats {
|
||||
index := 0
|
||||
switch {
|
||||
case stat.Bytes == 0:
|
||||
index = 0
|
||||
case stat.Bytes < 64:
|
||||
index = 1
|
||||
case stat.Bytes < 128:
|
||||
index = 2
|
||||
case stat.Bytes < 256:
|
||||
index = 3
|
||||
case stat.Bytes < 512:
|
||||
index = 4
|
||||
case stat.Bytes < 1024:
|
||||
index = 5
|
||||
case stat.Bytes < 2048:
|
||||
index = 6
|
||||
case stat.Bytes < 4096:
|
||||
index = 7
|
||||
default:
|
||||
index = 8
|
||||
}
|
||||
buckets[index].count++
|
||||
buckets[index].bytes += stat.Bytes
|
||||
}
|
||||
return buckets
|
||||
}
|
||||
|
||||
type balanceValueStats struct {
|
||||
name string
|
||||
count int
|
||||
unique int
|
||||
zeroCount int
|
||||
topValues []balanceTopValue
|
||||
fixedBytes uint64
|
||||
uvarintBytes uint64
|
||||
duplicateCount int
|
||||
}
|
||||
|
||||
type balanceTopValue struct {
|
||||
value uint64
|
||||
count int
|
||||
}
|
||||
|
||||
type balancePairStats struct {
|
||||
txCount int
|
||||
pairCount int
|
||||
lengthMismatchTxs int
|
||||
unchangedCount int
|
||||
changedCount int
|
||||
currentFixedValueBytes uint64
|
||||
bothUvarintBytes uint64
|
||||
preUvarintPostDelta uint64
|
||||
preUvarintChangedDeltas uint64
|
||||
}
|
||||
|
||||
func printBalanceAnalysis(blocks *pump_parser.RawTxBlocksBinary) {
|
||||
preValues := make([]uint64, 0)
|
||||
postValues := make([]uint64, 0)
|
||||
pairs := balancePairStats{}
|
||||
pairs.txCount = len(blocks.Txs)
|
||||
|
||||
for _, tx := range blocks.Txs {
|
||||
preValues = append(preValues, tx.Meta.PreBalances...)
|
||||
postValues = append(postValues, tx.Meta.PostBalances...)
|
||||
preLen := len(tx.Meta.PreBalances)
|
||||
postLen := len(tx.Meta.PostBalances)
|
||||
if preLen != postLen {
|
||||
pairs.lengthMismatchTxs++
|
||||
}
|
||||
n := preLen
|
||||
if postLen < n {
|
||||
n = postLen
|
||||
}
|
||||
pairs.currentFixedValueBytes += uint64(preLen+postLen) * 8
|
||||
pairs.preUvarintChangedDeltas += uint64(uvarintLen(uint64(n)))
|
||||
for i := 0; i < preLen; i++ {
|
||||
pairs.bothUvarintBytes += uint64(uvarintLen(tx.Meta.PreBalances[i]))
|
||||
pairs.preUvarintPostDelta += uint64(uvarintLen(tx.Meta.PreBalances[i]))
|
||||
pairs.preUvarintChangedDeltas += uint64(uvarintLen(tx.Meta.PreBalances[i]))
|
||||
}
|
||||
for i := 0; i < postLen; i++ {
|
||||
pairs.bothUvarintBytes += uint64(uvarintLen(tx.Meta.PostBalances[i]))
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
pre := tx.Meta.PreBalances[i]
|
||||
post := tx.Meta.PostBalances[i]
|
||||
pairs.pairCount++
|
||||
pairs.preUvarintPostDelta += uint64(zigzagDeltaUvarintLen(pre, post))
|
||||
if pre == post {
|
||||
pairs.unchangedCount++
|
||||
continue
|
||||
}
|
||||
pairs.changedCount++
|
||||
pairs.preUvarintChangedDeltas += uint64(uvarintLen(uint64(i)))
|
||||
pairs.preUvarintChangedDeltas += uint64(zigzagDeltaUvarintLen(pre, post))
|
||||
}
|
||||
}
|
||||
|
||||
preStats := collectBalanceValueStats("pre_balances", preValues)
|
||||
postStats := collectBalanceValueStats("post_balances", postValues)
|
||||
combined := append(append([]uint64(nil), preValues...), postValues...)
|
||||
combinedStats := collectBalanceValueStats("pre+post_balances", combined)
|
||||
|
||||
fmt.Println("balance_values_analysis")
|
||||
printBalanceValueStats(preStats)
|
||||
printBalanceValueStats(postStats)
|
||||
printBalanceValueStats(combinedStats)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("balance_encoding_estimates")
|
||||
fmt.Printf("txs=%d pairs=%d length_mismatch_txs=%d unchanged_pairs=%d changed_pairs=%d unchanged_pct=%.2f%%\n",
|
||||
pairs.txCount,
|
||||
pairs.pairCount,
|
||||
pairs.lengthMismatchTxs,
|
||||
pairs.unchangedCount,
|
||||
pairs.changedCount,
|
||||
float64(pairs.unchangedCount)*100/float64(maxInt(pairs.pairCount, 1)),
|
||||
)
|
||||
printEstimate("current_fixed_uint64_values", pairs.currentFixedValueBytes, pairs.currentFixedValueBytes)
|
||||
printEstimate("both_values_uvarint", pairs.bothUvarintBytes, pairs.currentFixedValueBytes)
|
||||
printEstimate("pre_uvarint_post_delta_each_index", pairs.preUvarintPostDelta, pairs.currentFixedValueBytes)
|
||||
printEstimate("pre_uvarint_post_changed_delta_pairs", pairs.preUvarintChangedDeltas, pairs.currentFixedValueBytes)
|
||||
}
|
||||
|
||||
func collectBalanceValueStats(name string, values []uint64) balanceValueStats {
|
||||
freq := make(map[uint64]int)
|
||||
var zeroCount int
|
||||
var uvarintBytes uint64
|
||||
for _, value := range values {
|
||||
freq[value]++
|
||||
if value == 0 {
|
||||
zeroCount++
|
||||
}
|
||||
uvarintBytes += uint64(uvarintLen(value))
|
||||
}
|
||||
top := make([]balanceTopValue, 0, len(freq))
|
||||
var duplicateCount int
|
||||
for value, count := range freq {
|
||||
top = append(top, balanceTopValue{value: value, count: count})
|
||||
if count > 1 {
|
||||
duplicateCount += count - 1
|
||||
}
|
||||
}
|
||||
sort.Slice(top, func(i, j int) bool {
|
||||
if top[i].count == top[j].count {
|
||||
return top[i].value < top[j].value
|
||||
}
|
||||
return top[i].count > top[j].count
|
||||
})
|
||||
if len(top) > 10 {
|
||||
top = top[:10]
|
||||
}
|
||||
return balanceValueStats{
|
||||
name: name,
|
||||
count: len(values),
|
||||
unique: len(freq),
|
||||
zeroCount: zeroCount,
|
||||
topValues: top,
|
||||
fixedBytes: uint64(len(values)) * 8,
|
||||
uvarintBytes: uvarintBytes,
|
||||
duplicateCount: duplicateCount,
|
||||
}
|
||||
}
|
||||
|
||||
func printBalanceValueStats(stats balanceValueStats) {
|
||||
fmt.Printf("%s: count=%d unique=%d duplicate_values=%d zero=%d zero_pct=%.2f%% fixed_bytes=%d uvarint_bytes=%d uvarint_saved=%.2f%%\n",
|
||||
stats.name,
|
||||
stats.count,
|
||||
stats.unique,
|
||||
stats.duplicateCount,
|
||||
stats.zeroCount,
|
||||
float64(stats.zeroCount)*100/float64(maxInt(stats.count, 1)),
|
||||
stats.fixedBytes,
|
||||
stats.uvarintBytes,
|
||||
savedPct(stats.fixedBytes, stats.uvarintBytes),
|
||||
)
|
||||
fmt.Printf("%-22s %-8s %-8s\n", "value", "count", "pct")
|
||||
for _, item := range stats.topValues {
|
||||
fmt.Printf("%-22d %-8d %7.2f%%\n", item.value, item.count, float64(item.count)*100/float64(maxInt(stats.count, 1)))
|
||||
}
|
||||
}
|
||||
|
||||
func printEstimate(name string, bytes uint64, baseline uint64) {
|
||||
fmt.Printf("%-38s %10d saved=%7.2f%%\n", name, bytes, savedPct(baseline, bytes))
|
||||
}
|
||||
|
||||
func savedPct(baseline uint64, value uint64) float64 {
|
||||
if baseline == 0 {
|
||||
return 0
|
||||
}
|
||||
return (float64(baseline) - float64(value)) * 100 / float64(baseline)
|
||||
}
|
||||
|
||||
func uvarintLen(value uint64) int {
|
||||
n := 1
|
||||
for value >= 0x80 {
|
||||
value >>= 7
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func zigzagDeltaUvarintLen(pre uint64, post uint64) int {
|
||||
if post >= pre {
|
||||
return uvarintLen((post - pre) << 1)
|
||||
}
|
||||
return uvarintLen(((pre - post) << 1) - 1)
|
||||
}
|
||||
|
||||
func maxInt(a int, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -2,29 +2,18 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
"github.com/gagliardetto/solana-go/rpc"
|
||||
"github.com/jackc/pgtype"
|
||||
"github.com/shopspring/decimal"
|
||||
solana_parser "github.com/thloyi/pump-parser"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var ()
|
||||
|
||||
func main() {
|
||||
|
||||
var slot uint64 = 403021435
|
||||
var data = NewBlockData(decimal.NewFromFloat(100.0))
|
||||
var slot uint64 = 414696178
|
||||
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
|
||||
var rewards = false
|
||||
var version uint64 = 0
|
||||
@@ -42,7 +31,7 @@ func main() {
|
||||
}
|
||||
solana_parser.EnableAllParsers()
|
||||
|
||||
var txs []*solana_parser.Tx
|
||||
var txs []solana_parser.Tx
|
||||
for i, tx := range blocks.Transactions {
|
||||
var blockTime uint64
|
||||
if blocks.BlockTime != nil {
|
||||
@@ -61,766 +50,11 @@ func main() {
|
||||
fmt.Println("parse tx error:", i, rawTx.TxHash(), err)
|
||||
break
|
||||
}
|
||||
txs = append(txs, parsedTx)
|
||||
txs = append(txs, *parsedTx)
|
||||
}
|
||||
for _, result := range txs {
|
||||
swapsLen := len(result.Swaps)
|
||||
for i := 0; i < swapsLen; i++ {
|
||||
action := result.Swaps[i]
|
||||
var actions []solana_parser.Swap = make([]solana_parser.Swap, 0, 2)
|
||||
actions = append(actions, action)
|
||||
if i+1 < swapsLen {
|
||||
nextAction := result.Swaps[i+1]
|
||||
if action.Event == "buy" && nextAction.Event == "complete" &&
|
||||
action.Program == solana_parser.SolProgramPump &&
|
||||
nextAction.Program == solana_parser.SolProgramPump &&
|
||||
action.BaseMint == nextAction.BaseMint {
|
||||
actions = append(actions, nextAction)
|
||||
i++
|
||||
}
|
||||
if action.Event == "migrate" && nextAction.Event == "create" &&
|
||||
action.Program == solana_parser.SolProgramPump &&
|
||||
nextAction.Program == solana_parser.SolProgramPumpAMM &&
|
||||
action.BaseMint == nextAction.BaseMint {
|
||||
actions = append(actions, nextAction)
|
||||
i++
|
||||
}
|
||||
}
|
||||
if err = HandleAction(context.Background(), result, actions, data); err != nil {
|
||||
//h.logger.Errorf("handle action error: %s - %v", result.RawTx.Transaction.Signatures[0].String(), err)
|
||||
fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
fmt.Println("slot", slot, "tx count: ", len(data.Txs))
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
meteoraDammV2Program = solana.MustPublicKeyFromBase58("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG")
|
||||
raydiumCPmmProgramID = solana.MustPublicKeyFromBase58("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C")
|
||||
)
|
||||
|
||||
func HandleAction(ctx context.Context, tx *solana_parser.Tx, swaps []solana_parser.Swap, data *BlockData) error {
|
||||
swapLen := len(swaps)
|
||||
if len(swaps) == 0 {
|
||||
return nil
|
||||
}
|
||||
if swaps[0].BaseMint != solana_parser.WSOL && swaps[0].QuoteMint != solana_parser.WSOL && !swaps[0].QuoteMint.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(swaps) == 0 {
|
||||
return nil
|
||||
}
|
||||
event := swaps[0].Event
|
||||
swap := swaps[0]
|
||||
action := SwapGetter{swap}
|
||||
switch event {
|
||||
case "buy", "sell":
|
||||
|
||||
data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price))
|
||||
if swap.Program == solana_parser.SolProgramPump {
|
||||
if swapLen == 2 && swaps[1].Event == "complete" {
|
||||
t := pgtype.Timestamptz{}
|
||||
t.Set(time.Unix(tx.BlockAt, 0))
|
||||
data.AppendAction(Action{
|
||||
Maker: swaps[1].User.String(),
|
||||
Token: swaps[1].BaseMint.String(),
|
||||
Pair: swaps[1].Pool.String(),
|
||||
Action: "pump-migrate",
|
||||
Block: tx.Block,
|
||||
BlockAt: t,
|
||||
TxHash: tx.GetTxHash(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return data.SetPair(action, tx.Block, "")
|
||||
|
||||
case "create":
|
||||
pair, err := action.GetPair(tx.Block, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price))
|
||||
data.Pairs[pair.Address] = *pair
|
||||
case "add_liquidity", "remove_liquidity", "deposit", "withdraw", "add", "remove":
|
||||
liquidityTx, err := action.GetLiquidityTx(tx, uint64(swap.TxIndex))
|
||||
if liquidityTx == nil {
|
||||
return err
|
||||
}
|
||||
data.AppendTx(*liquidityTx)
|
||||
return data.SetPair(action, tx.Block, "")
|
||||
}
|
||||
|
||||
if event != "migrate" {
|
||||
return nil
|
||||
}
|
||||
if swap.Program == solana_parser.SolProgramPump {
|
||||
t := pgtype.Timestamptz{}
|
||||
t.Set(time.Unix(tx.BlockAt, 0))
|
||||
if swapLen == 2 && swaps[1].Event == "create" && swaps[1].Program == solana_parser.SolProgramPumpAMM && swaps[1].BaseMint == swap.BaseMint {
|
||||
tokenMint := swap.BaseMint.String()
|
||||
data.AppendAction(Action{
|
||||
Maker: swap.User.String(),
|
||||
Token: tokenMint,
|
||||
Pair: swaps[1].Pool.String(),
|
||||
Action: "on-pumpswap",
|
||||
Block: tx.Block,
|
||||
BlockAt: t,
|
||||
TxHash: tx.GetTxHash(),
|
||||
})
|
||||
data.NewRaydium = append(data.NewRaydium, tokenMint)
|
||||
}
|
||||
} else if swap.Program == solana_parser.SolProgramRaydiumLaunchLab || swap.Program == solana_parser.SolProgramRaydiumLaunchLabBonk {
|
||||
t := pgtype.Timestamptz{}
|
||||
t.Set(time.Unix(tx.BlockAt, 0))
|
||||
var actionType string
|
||||
if action.MigrateTopProgram == raydiumCPmmProgramID {
|
||||
actionType = "on-raydium-cpmm"
|
||||
} else {
|
||||
actionType = "on-raydium-amm"
|
||||
}
|
||||
data.AppendAction(Action{
|
||||
Maker: action.User.String(),
|
||||
Token: action.BaseMint.String(),
|
||||
Pair: action.MigrateToPool.String(),
|
||||
Action: actionType,
|
||||
Block: tx.Block,
|
||||
BlockAt: t,
|
||||
TxHash: tx.GetTxHash(),
|
||||
})
|
||||
} else if swap.Program == solana_parser.SolProgramMeteoraBondingCurve {
|
||||
t := pgtype.Timestamptz{}
|
||||
t.Set(time.Unix(tx.BlockAt, 0))
|
||||
var actionType string
|
||||
if swap.MigrateTopProgram == meteoraDammV2Program {
|
||||
actionType = "on-meteora-amm-v2"
|
||||
} else {
|
||||
actionType = "on-meteora-amm-v1"
|
||||
}
|
||||
data.AppendAction(Action{
|
||||
Maker: action.User.String(),
|
||||
Token: action.BaseMint.String(),
|
||||
Pair: action.MigrateToPool.String(),
|
||||
Action: actionType,
|
||||
Block: uint64(tx.Block),
|
||||
BlockAt: t,
|
||||
TxHash: tx.GetTxHash(),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Pair struct {
|
||||
Id string `gorm:"column:id;primaryKey;default:uuid_generate_v4()"`
|
||||
Address string
|
||||
Name string
|
||||
Token0 string
|
||||
Token1 string
|
||||
LpToken string
|
||||
ChainId int64
|
||||
Reserve0 decimal.Decimal
|
||||
Reserve1 decimal.Decimal
|
||||
Block uint64
|
||||
BlockAt *pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at,omitempty"`
|
||||
CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"created_at,omitempty"`
|
||||
SortId uint64
|
||||
Program string
|
||||
|
||||
IsCreate bool `gorm:"-"`
|
||||
//TokenObj *Token `gorm:"-" json:"token_obj,omitempty"`
|
||||
UpdateSlot uint64 `gorm:"-"`
|
||||
InDB bool `gorm:"-"`
|
||||
}
|
||||
|
||||
type Tx struct {
|
||||
Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"`
|
||||
PairAddress string `json:"pair_address"`
|
||||
Maker string `json:"maker"`
|
||||
Token0Address string `json:"token0_address"`
|
||||
Token1Address string `json:"token1_address"`
|
||||
Token0Amount decimal.Decimal `json:"token0Amount" gorm:"column:token0_amount;type:numeric"`
|
||||
Token1Amount decimal.Decimal `json:"token1Amount" gorm:"column:token1_amount;type:numeric"`
|
||||
PriceUsd decimal.Decimal `json:"price_usd" gorm:"column:price_usd;type:numeric"`
|
||||
AmountUsd decimal.Decimal `json:"amount_usd" gorm:"column:amount_usd;type:numeric"`
|
||||
Block uint64 `json:"block"`
|
||||
BlockIndex uint64 `json:"index"`
|
||||
Event string `json:"event"`
|
||||
TxHash string `json:"tx_hash"`
|
||||
TxIndex uint64 `json:"topic_index"`
|
||||
Program string `json:"program"`
|
||||
BlockAt pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at"`
|
||||
CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"`
|
||||
TotalSupply string `gorm:"total_supply"`
|
||||
AfterReserve0 string `gorm:"after_reserve0"`
|
||||
AfterReserve1 string `gorm:"after_reserve1"`
|
||||
PositionChange int64 `gorm:"position_change"`
|
||||
Platform string `gorm:"column:tx_platform;type:platform;default:'none'" json:"tx_platform"`
|
||||
PlatformFee decimal.Decimal `gorm:"-" json:"-"` // TODO: save to db
|
||||
CUPrice decimal.Decimal `gorm:"column:tx_cu_price;type:numeric" json:"tx_cu_price"`
|
||||
MevAgent string `gorm:"column:tx_mev_agent;type:mev_agent;default:'none'" json:"tx_mev_agent"`
|
||||
MevAgentFee decimal.Decimal `gorm:"column:tx_mev_agent_fee;type:numeric" json:"tx_mev_agent_fee"`
|
||||
AfterSOLBalance decimal.Decimal `gorm:"column:after_sol_balance;type:numeric" json:"after_sol_balance"`
|
||||
EntryContract string `gorm:"column:tx_entry_contract;type:entry_contract;default:'none'" json:"tx_entry_contract"`
|
||||
}
|
||||
|
||||
type Action struct {
|
||||
Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"`
|
||||
Maker string `json:"maker"`
|
||||
Token string `json:"token"`
|
||||
Pair string `json:"pair"`
|
||||
Action string `json:"action"`
|
||||
Block uint64 `json:"block"`
|
||||
BlockAt pgtype.Timestamptz `json:"block_at"`
|
||||
TxHash string `json:"tx_hash"`
|
||||
CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"`
|
||||
}
|
||||
|
||||
type BlockData struct {
|
||||
Pairs map[string]Pair
|
||||
Txs []Tx
|
||||
Actions []Action
|
||||
Price decimal.Decimal
|
||||
NewRaydium []string
|
||||
}
|
||||
|
||||
func NewBlockData(price decimal.Decimal) *BlockData {
|
||||
return &BlockData{
|
||||
Pairs: make(map[string]Pair),
|
||||
Txs: make([]Tx, 0),
|
||||
Actions: make([]Action, 0),
|
||||
Price: price,
|
||||
NewRaydium: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (bd *BlockData) AppendTx(tx Tx) {
|
||||
bd.Txs = append(bd.Txs, tx)
|
||||
}
|
||||
|
||||
func (bd *BlockData) AppendAction(action Action) {
|
||||
bd.Actions = append(bd.Actions, action)
|
||||
}
|
||||
|
||||
func (bd *BlockData) SetPair(action SwapGetter, block uint64, _ string) error {
|
||||
pair, err := action.GetPair(block, "")
|
||||
_, err = solana_parser.EncodeTxsBinary(txs)
|
||||
if err != nil {
|
||||
return err
|
||||
fmt.Println("EncodeTxsBinary err", err)
|
||||
}
|
||||
bd.Pairs[pair.Address] = *pair
|
||||
return nil
|
||||
}
|
||||
|
||||
type SwapGetter struct {
|
||||
solana_parser.Swap
|
||||
}
|
||||
|
||||
const (
|
||||
PositionChangeNone = int64(iota)
|
||||
PositionChangeNewBuy
|
||||
PositionChangeBuyMore
|
||||
PositionChangeSellPart
|
||||
PositionChangeSellAll
|
||||
)
|
||||
|
||||
func (spg SwapGetter) GetLiquidityTx(tx *solana_parser.Tx, index uint64) (*Tx, error) {
|
||||
if spg.BaseMint != solana.WrappedSol && spg.QuoteMint != solana.WrappedSol {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
token0 string
|
||||
amount0 decimal.Decimal
|
||||
amount1 decimal.Decimal
|
||||
pool0 decimal.Decimal
|
||||
pool1 decimal.Decimal
|
||||
|
||||
event string
|
||||
)
|
||||
|
||||
if spg.BaseMint == solana.WrappedSol {
|
||||
amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
||||
amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
||||
token0 = spg.QuoteMint.String()
|
||||
pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
||||
pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
||||
} else {
|
||||
amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
||||
amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
||||
token0 = spg.BaseMint.String()
|
||||
pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
||||
pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
||||
}
|
||||
if spg.Event == "deposit" || spg.Event == "add" || spg.Event == "add_liquidity" || spg.Event == "add_liquidity_one_side" {
|
||||
event = "add"
|
||||
} else if spg.Event == "withdraw" || spg.Event == "remove" || spg.Event == "remove_liquidity" || spg.Event == "remove_liquidity_one_side" {
|
||||
event = "remove"
|
||||
}
|
||||
if event == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
mevName, mevFee := tx.CheckMevAgent()
|
||||
platformName, platformFee := tx.CheckPlatform(spg.Swap)
|
||||
|
||||
pairString := ""
|
||||
if spg.Program == solana_parser.SolProgramPump {
|
||||
pairString = spg.BaseMint.String()
|
||||
} else {
|
||||
pairString = spg.Pool.String()
|
||||
}
|
||||
t := pgtype.Timestamptz{}
|
||||
_ = t.Set(time.Unix(tx.BlockAt, 0))
|
||||
return &Tx{
|
||||
PairAddress: pairString,
|
||||
Maker: spg.User.String(),
|
||||
Token0Address: token0,
|
||||
Token1Address: "So11111111111111111111111111111111111111112",
|
||||
Token0Amount: amount0,
|
||||
Token1Amount: amount1,
|
||||
Block: tx.Block,
|
||||
BlockIndex: tx.BlockIndex,
|
||||
Event: event,
|
||||
TxHash: tx.GetTxHash(),
|
||||
TxIndex: index,
|
||||
BlockAt: t,
|
||||
Program: spg.Program,
|
||||
AfterReserve0: pool0.String(),
|
||||
AfterReserve1: pool1.String(),
|
||||
Platform: platformName,
|
||||
PlatformFee: platformFee,
|
||||
CUPrice: tx.CUPrice,
|
||||
MevAgent: mevName,
|
||||
MevAgentFee: mevFee,
|
||||
AfterSOLBalance: spg.AfterSOLBalance,
|
||||
EntryContract: spg.CheckEntryContract(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (spg SwapGetter) GetTx(tx *solana_parser.Tx, index uint64, price decimal.Decimal) Tx {
|
||||
var (
|
||||
token0 string
|
||||
amount0 decimal.Decimal
|
||||
amount1 decimal.Decimal
|
||||
pool0 decimal.Decimal
|
||||
pool1 decimal.Decimal
|
||||
|
||||
event string
|
||||
)
|
||||
|
||||
if spg.BaseMint == solana.WrappedSol {
|
||||
amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
||||
amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
||||
token0 = spg.QuoteMint.String()
|
||||
pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
||||
pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
||||
if spg.Event == "buy" {
|
||||
event = "sell"
|
||||
} else if spg.Event == "sell" {
|
||||
event = "buy"
|
||||
}
|
||||
} else {
|
||||
amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
||||
amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
||||
token0 = spg.BaseMint.String()
|
||||
pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
||||
pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
||||
event = spg.Event
|
||||
}
|
||||
|
||||
priceUsd := decimal.Zero
|
||||
if amount0.GreaterThan(priceUsd) {
|
||||
priceUsd = amount1.Div(amount0).Mul(price)
|
||||
}
|
||||
pc := PositionChangeNone
|
||||
if event == "buy" {
|
||||
pc = PositionChangeNewBuy
|
||||
if spg.BaseMint == solana.WrappedSol {
|
||||
if spg.UserQuoteBalance.GreaterThan(spg.QuoteAmount) {
|
||||
pc = PositionChangeBuyMore
|
||||
}
|
||||
} else {
|
||||
if spg.UserBaseBalance.GreaterThan(spg.BaseAmount) {
|
||||
pc = PositionChangeBuyMore
|
||||
}
|
||||
}
|
||||
} else if event == "sell" {
|
||||
pc = PositionChangeSellPart
|
||||
if spg.BaseMint == solana.WrappedSol {
|
||||
if spg.UserQuoteBalance.Div(decimal.New(1, int32(spg.QuoteMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) {
|
||||
pc = PositionChangeSellAll
|
||||
}
|
||||
} else {
|
||||
if spg.UserBaseBalance.Div(decimal.New(1, int32(spg.BaseMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) {
|
||||
pc = PositionChangeSellAll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mevName, mevFee := tx.CheckMevAgent()
|
||||
platformName, platformFee := tx.CheckPlatform(spg.Swap)
|
||||
|
||||
if mevName == "" {
|
||||
mevName = "none"
|
||||
}
|
||||
if mevName == "unknown" {
|
||||
mevName = "none"
|
||||
mevFee = decimal.Zero
|
||||
}
|
||||
pairString := ""
|
||||
if spg.Program == solana_parser.SolProgramPump {
|
||||
pairString = spg.BaseMint.String()
|
||||
} else {
|
||||
pairString = spg.Pool.String()
|
||||
}
|
||||
t := pgtype.Timestamptz{}
|
||||
_ = t.Set(time.Unix(tx.BlockAt, 0))
|
||||
|
||||
return Tx{
|
||||
PairAddress: pairString,
|
||||
Maker: spg.User.String(),
|
||||
Token0Address: token0,
|
||||
Token1Address: "So11111111111111111111111111111111111111112",
|
||||
Token0Amount: amount0,
|
||||
Token1Amount: amount1,
|
||||
PriceUsd: priceUsd,
|
||||
AmountUsd: amount1.Mul(price),
|
||||
Block: tx.Block,
|
||||
BlockIndex: tx.BlockIndex,
|
||||
Event: event,
|
||||
TxHash: tx.GetTxHash(),
|
||||
TxIndex: index,
|
||||
BlockAt: t,
|
||||
Program: spg.Program,
|
||||
AfterReserve0: pool0.String(),
|
||||
AfterReserve1: pool1.String(),
|
||||
PositionChange: pc,
|
||||
Platform: platformName,
|
||||
PlatformFee: platformFee,
|
||||
CUPrice: tx.CUPrice,
|
||||
MevAgent: mevName,
|
||||
MevAgentFee: mevFee,
|
||||
AfterSOLBalance: spg.AfterSOLBalance,
|
||||
EntryContract: spg.CheckEntryContract(),
|
||||
}
|
||||
}
|
||||
|
||||
func (spg SwapGetter) GetPair(slot uint64, _ string) (*Pair, error) {
|
||||
//pump amm
|
||||
if spg.Program == solana_parser.SolProgramPump {
|
||||
tokenMint := spg.BaseMint.String()
|
||||
return &Pair{
|
||||
Address: tokenMint,
|
||||
Token0: tokenMint,
|
||||
Token1: "So11111111111111111111111111111111111111112",
|
||||
ChainId: 900,
|
||||
Reserve0: spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))),
|
||||
Reserve1: spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))),
|
||||
IsCreate: spg.Event == "create",
|
||||
Program: spg.Program,
|
||||
UpdateSlot: slot,
|
||||
}, nil
|
||||
} else {
|
||||
var (
|
||||
token0 string
|
||||
amount0 decimal.Decimal
|
||||
amount1 decimal.Decimal
|
||||
)
|
||||
if spg.BaseMint.IsZero() || spg.QuoteMint.IsZero() {
|
||||
return nil, errors.New("base mint or quote mint is empty")
|
||||
}
|
||||
|
||||
if spg.BaseMint == solana.WrappedSol {
|
||||
amount0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
||||
amount1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
||||
//decimal0 = spg.QuoteMintDecimals
|
||||
token0 = spg.QuoteMint.String()
|
||||
} else {
|
||||
amount0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
|
||||
amount1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
|
||||
//decimal0 = a.BaseDecimals
|
||||
token0 = spg.BaseMint.String()
|
||||
}
|
||||
|
||||
return &Pair{
|
||||
Address: spg.Pool.String(),
|
||||
LpToken: spg.LpMint.String(),
|
||||
Token0: token0,
|
||||
Token1: "So11111111111111111111111111111111111111112",
|
||||
ChainId: 900,
|
||||
Reserve0: amount0,
|
||||
Reserve1: amount1,
|
||||
IsCreate: false,
|
||||
Program: spg.Program,
|
||||
UpdateSlot: slot,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getBlockTxsFromDb(db *gorm.DB, block uint64) ([]Tx, error) {
|
||||
var txs []Tx
|
||||
result := db.Table("tx").Where("block = ?", block).Find(&txs)
|
||||
return txs, result.Error
|
||||
}
|
||||
|
||||
func getBlockActionsFromDb(db *gorm.DB, block uint64) ([]Action, error) {
|
||||
var txs []Action
|
||||
result := db.Table("action").Where("block = ?", block).Find(&txs)
|
||||
return txs, result.Error
|
||||
}
|
||||
|
||||
type dbLog struct {
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (l *dbLog) Printf(format string, args ...interface{}) {
|
||||
l.logger.Info(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func newDbLog() *dbLog {
|
||||
return &dbLog{logger: slog.Default()}
|
||||
}
|
||||
|
||||
func NewGorm(dsn string) *gorm.DB {
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.New(newDbLog(), logger.Config{
|
||||
Colorful: false,
|
||||
LogLevel: logger.Warn,
|
||||
SlowThreshold: time.Second * 10,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
}),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func compareTxs(dbTxs []Tx, dataTxs []Tx) (diff int, missing int) {
|
||||
dataByHash := make(map[string][]Tx, len(dataTxs))
|
||||
for _, tx := range dataTxs {
|
||||
dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)] = append(dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)], tx)
|
||||
}
|
||||
|
||||
for _, dbTx := range dbTxs {
|
||||
candidates := dataByHash[fmt.Sprintf("%s-%d", dbTx.TxHash, dbTx.TxIndex)]
|
||||
if len(candidates) == 0 {
|
||||
missing++
|
||||
log.Printf("missing tx: %s", txCompareString(dbTx))
|
||||
continue
|
||||
}
|
||||
matched := false
|
||||
for _, dataTx := range candidates {
|
||||
if txEqualWithoutHash(dbTx, dataTx) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
diff++
|
||||
log.Printf("tx diff hash=%s, program=%s, event:%s: %s, ", dbTx.TxHash, dbTx.Program, dbTx.Event, txCompareDiffString(dbTx, candidates[0]))
|
||||
}
|
||||
}
|
||||
log.Printf("compare txs done: db=%d parsed=%d missing=%d diff=%d", len(dbTxs), len(dataTxs), missing, diff)
|
||||
return diff, missing
|
||||
}
|
||||
|
||||
func withinOnePercentDecimal(a decimal.Decimal, b decimal.Decimal) bool {
|
||||
if a.IsZero() {
|
||||
return b.IsZero()
|
||||
}
|
||||
diff := a.Sub(b).Abs()
|
||||
threshold := a.Abs().Mul(decimal.NewFromFloat(0.03))
|
||||
return diff.LessThanOrEqual(threshold)
|
||||
}
|
||||
|
||||
func withinOnePercentStringDecimal(a string, b string) bool {
|
||||
ad, errA := decimal.NewFromString(a)
|
||||
bd, errB := decimal.NewFromString(b)
|
||||
if errA != nil || errB != nil {
|
||||
return a == b
|
||||
}
|
||||
return withinOnePercentDecimal(ad, bd)
|
||||
}
|
||||
|
||||
func txEqualWithoutHash(a Tx, b Tx) bool {
|
||||
//mevMatch := a.MevAgent == b.MevAgent || (a.MevAgent == "none" && b.MevAgent == "unknown") || (a.MevAgent == "unknown" && b.MevAgent == "none")
|
||||
//mevNone := a.MevAgent == "none" || a.MevAgent == "unknown"
|
||||
|
||||
return a.PairAddress == b.PairAddress &&
|
||||
a.Token1Address == b.Token1Address &&
|
||||
(a.Token0Address == "" || a.Token0Address == b.Token0Address) &&
|
||||
//a.Maker == b.Maker &&
|
||||
(a.Token0Address == "" || withinOnePercentDecimal(a.Token0Amount, b.Token0Amount)) &&
|
||||
withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) &&
|
||||
a.Block == b.Block &&
|
||||
a.BlockIndex == b.BlockIndex &&
|
||||
a.Event == b.Event &&
|
||||
a.TxIndex == b.TxIndex &&
|
||||
a.Program == b.Program &&
|
||||
(a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0)) &&
|
||||
(a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1)) &&
|
||||
// a.PositionChange == b.PositionChange &&
|
||||
(a.Platform == b.Platform || (a.Platform == "photon" && b.Platform == "fake") || (a.Platform == "trojan" && b.Platform == "fake")) &&
|
||||
a.CUPrice.String() == b.CUPrice.String() // &&
|
||||
//mevMatch &&
|
||||
//(mevNone || a.MevAgentFee.String() == b.MevAgentFee.String()) &&
|
||||
//(a.Program == solana_parser.SolProgramRaydiumV4 || a.AfterSOLBalance.String() == b.AfterSOLBalance.String())
|
||||
//&&
|
||||
// a.EntryContract == b.EntryContract
|
||||
}
|
||||
|
||||
func txCompareDiffString(a Tx, b Tx) string {
|
||||
var diffs []string
|
||||
if a.PairAddress != b.PairAddress {
|
||||
diffs = append(diffs, fmt.Sprintf("PairAddress db=%s data=%s", a.PairAddress, b.PairAddress))
|
||||
}
|
||||
//if a.Maker != b.Maker {
|
||||
// diffs = append(diffs, fmt.Sprintf("Maker db=%s, data=%s", a.Maker, b.Maker))
|
||||
//}
|
||||
if a.Token1Address != b.Token1Address {
|
||||
diffs = append(diffs, fmt.Sprintf("Token1Address db=%s data=%s", a.Token1Address, b.Token1Address))
|
||||
}
|
||||
if a.Token0Address != b.Token0Address {
|
||||
diffs = append(diffs, fmt.Sprintf("Token0Address db=%s data=%s", a.Token0Address, b.Token0Address))
|
||||
}
|
||||
if !withinOnePercentDecimal(a.Token0Amount, b.Token0Amount) {
|
||||
diffs = append(diffs, fmt.Sprintf("Token0Amount db=%s data=%s", a.Token0Amount.String(), b.Token0Amount.String()))
|
||||
}
|
||||
if !withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) {
|
||||
diffs = append(diffs, fmt.Sprintf("Token1Amount db=%s data=%s", a.Token1Amount.String(), b.Token1Amount.String()))
|
||||
}
|
||||
if a.Block != b.Block {
|
||||
diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block))
|
||||
}
|
||||
if a.BlockIndex != b.BlockIndex {
|
||||
diffs = append(diffs, fmt.Sprintf("BlockIndex db=%d data=%d", a.BlockIndex, b.BlockIndex))
|
||||
}
|
||||
if a.Event != b.Event {
|
||||
diffs = append(diffs, fmt.Sprintf("Event db=%s data=%s", a.Event, b.Event))
|
||||
}
|
||||
if a.TxIndex != b.TxIndex {
|
||||
diffs = append(diffs, fmt.Sprintf("TxIndex db=%d data=%d", a.TxIndex, b.TxIndex))
|
||||
}
|
||||
if a.Program != b.Program {
|
||||
diffs = append(diffs, fmt.Sprintf("Program db=%s data=%s", a.Program, b.Program))
|
||||
}
|
||||
if !withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0) {
|
||||
diffs = append(diffs, fmt.Sprintf("AfterReserve0 db=%s data=%s", a.AfterReserve0, b.AfterReserve0))
|
||||
}
|
||||
if !withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1) {
|
||||
diffs = append(diffs, fmt.Sprintf("AfterReserve1 db=%s data=%s", a.AfterReserve1, b.AfterReserve1))
|
||||
}
|
||||
//if a.PositionChange != b.PositionChange {
|
||||
// diffs = append(diffs, fmt.Sprintf("PositionChange db=%d data=%d", a.PositionChange, b.PositionChange))
|
||||
//}
|
||||
if a.Platform != b.Platform {
|
||||
diffs = append(diffs, fmt.Sprintf("Platform db=%s data=%s", a.Platform, b.Platform))
|
||||
}
|
||||
if a.CUPrice.String() != b.CUPrice.String() {
|
||||
diffs = append(diffs, fmt.Sprintf("CUPrice db=%s data=%s", a.CUPrice.String(), b.CUPrice.String()))
|
||||
}
|
||||
//if a.MevAgent != b.MevAgent {
|
||||
// diffs = append(diffs, fmt.Sprintf("MevAgent db=%s data=%s", a.MevAgent, b.MevAgent))
|
||||
//}
|
||||
//if a.MevAgentFee.String() != b.MevAgentFee.String() {
|
||||
// diffs = append(diffs, fmt.Sprintf("MevAgentFee db=%s data=%s", a.MevAgentFee.String(), b.MevAgentFee.String()))
|
||||
//}
|
||||
//if a.AfterSOLBalance.String() != b.AfterSOLBalance.String() {
|
||||
// diffs = append(diffs, fmt.Sprintf("AfterSOLBalance db=%s data=%s", a.AfterSOLBalance.String(), b.AfterSOLBalance.String()))
|
||||
//}
|
||||
//if a.EntryContract != b.EntryContract {
|
||||
// diffs = append(diffs, fmt.Sprintf("EntryContract db=%s data=%s", a.EntryContract, b.EntryContract))
|
||||
//}
|
||||
return strings.Join(diffs, "; ")
|
||||
}
|
||||
|
||||
func compareActions(dbActions []Action, dataActions []Action) (diff, missing int) {
|
||||
dataByHash := make(map[string][]Action, len(dataActions))
|
||||
for _, action := range dataActions {
|
||||
dataByHash[action.TxHash] = append(dataByHash[action.TxHash], action)
|
||||
}
|
||||
|
||||
for _, dbAction := range dbActions {
|
||||
candidates := dataByHash[dbAction.TxHash]
|
||||
if len(candidates) == 0 {
|
||||
missing++
|
||||
log.Printf("missing action: %s", actionCompareString(dbAction))
|
||||
continue
|
||||
}
|
||||
matched := false
|
||||
for _, dataAction := range candidates {
|
||||
if actionEqualWithoutHash(dbAction, dataAction) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
diff++
|
||||
log.Printf("action diff hash=%s: %s", dbAction.TxHash, actionCompareDiffString(dbAction, candidates[0]))
|
||||
}
|
||||
}
|
||||
log.Printf("compare actions done: db=%d parsed=%d missing=%d diff=%d", len(dbActions), len(dataActions), missing, diff)
|
||||
return diff, missing
|
||||
}
|
||||
|
||||
func actionEqualWithoutHash(a Action, b Action) bool {
|
||||
return a.Maker == b.Maker &&
|
||||
a.Token == b.Token &&
|
||||
a.Pair == b.Pair &&
|
||||
a.Action == b.Action &&
|
||||
a.Block == b.Block
|
||||
}
|
||||
|
||||
func actionCompareDiffString(a Action, b Action) string {
|
||||
var diffs []string
|
||||
if a.Maker != b.Maker {
|
||||
diffs = append(diffs, fmt.Sprintf("Maker db=%s data=%s", a.Maker, b.Maker))
|
||||
}
|
||||
if a.Token != b.Token {
|
||||
diffs = append(diffs, fmt.Sprintf("Token db=%s data=%s", a.Token, b.Token))
|
||||
}
|
||||
if a.Pair != b.Pair {
|
||||
diffs = append(diffs, fmt.Sprintf("Pair db=%s data=%s", a.Pair, b.Pair))
|
||||
}
|
||||
if a.Action != b.Action {
|
||||
diffs = append(diffs, fmt.Sprintf("Action db=%s data=%s", a.Action, b.Action))
|
||||
}
|
||||
if a.Block != b.Block {
|
||||
diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block))
|
||||
}
|
||||
return strings.Join(diffs, "; ")
|
||||
}
|
||||
|
||||
func actionCompareString(action Action) string {
|
||||
return fmt.Sprintf("Maker=%s Token=%s Pair=%s Action=%s Block=%d TxHash=%s", action.Maker, action.Token, action.Pair, action.Action, action.Block, action.TxHash)
|
||||
}
|
||||
|
||||
func txCompareString(tx Tx) string {
|
||||
return fmt.Sprintf(
|
||||
"tx.Program=%s TxHash=%s PairAddress=%s Token1Address=%s Token0Amount=%s Token1Amount=%s Block=%d BlockIndex=%d Event=%s TxIndex=%d AfterReserve0=%s AfterReserve1=%s PositionChange=%d Platform=%s CUPrice=%s MevAgent=%s MevAgentFee=%s AfterSOLBalance=%s EntryContract=%s",
|
||||
tx.Program,
|
||||
tx.TxHash,
|
||||
tx.PairAddress,
|
||||
tx.Token1Address,
|
||||
tx.Token0Amount.String(),
|
||||
tx.Token1Amount.String(),
|
||||
tx.Block,
|
||||
tx.BlockIndex,
|
||||
tx.Event,
|
||||
tx.TxIndex,
|
||||
tx.AfterReserve0,
|
||||
tx.AfterReserve1,
|
||||
tx.PositionChange,
|
||||
tx.Platform,
|
||||
tx.CUPrice.String(),
|
||||
tx.MevAgent,
|
||||
tx.MevAgentFee.String(),
|
||||
tx.AfterSOLBalance.String(),
|
||||
tx.EntryContract,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func main() {
|
||||
var data = NewBlockData(decimal.NewFromFloat(100.0))
|
||||
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
|
||||
var version uint64 = 0
|
||||
txSig, _ := solana.SignatureFromBase58("2LCw5yZy6sGTWKpJNxpFxR11M66cXPsrGmJXnQmWW9QVv6SDWRmu1aevc6yE9NeUz78mFb4T8TEx9w5781NHnz2T")
|
||||
txSig, _ := solana.SignatureFromBase58("4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri")
|
||||
tx, err := client.GetTransaction(context.Background(), txSig, &rpc.GetTransactionOpts{
|
||||
Commitment: rpc.CommitmentFinalized,
|
||||
Encoding: solana.EncodingBase64,
|
||||
@@ -78,6 +78,10 @@ func main() {
|
||||
i++
|
||||
}
|
||||
}
|
||||
fmt.Printf("swap: %d, program: %s, event: %s, base: %s quote: %s, base amount: %s, quote amount: %s, \n", i,
|
||||
action.Program, action.Event, action.BaseMint.String(), action.QuoteMint.String(),
|
||||
action.BaseAmount.String(),
|
||||
action.QuoteAmount.String())
|
||||
if err = HandleAction(context.Background(), result, actions, data); err != nil {
|
||||
//h.logger.Errorf("handle action error: %s - %v", result.RawTx.Transaction.Signatures[0].String(), err)
|
||||
fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ type metaoraPoolInitializePoolData struct {
|
||||
}
|
||||
|
||||
type metaoraPoolSwapArgs struct {
|
||||
InAmount uint64
|
||||
MinimumOutAmount uint64
|
||||
InAmount uint64
|
||||
MinimumOutAmount uint64
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -855,6 +855,9 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
}
|
||||
}
|
||||
if !baseFound || !quoteFound {
|
||||
if args.InAmount == 0 {
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
return nil, increaseOffset(offset), fmt.Errorf("failed to find meteora pool event in inner instructions")
|
||||
}
|
||||
|
||||
|
||||
124
orcawhirpool.go
124
orcawhirpool.go
@@ -263,10 +263,10 @@ func orcaWhirPoolLiquidityParser(tx *Tx, instruction Instruction, innerInstructi
|
||||
//return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
||||
baseAmount = decimal.NewFromInt(int64(amount))
|
||||
baseAmount = decimal.NewFromUint64(amount)
|
||||
baseFound = true
|
||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
||||
quoteAmount = decimal.NewFromUint64(amount)
|
||||
quoteFound = true
|
||||
}
|
||||
if baseFound && quoteFound {
|
||||
@@ -281,7 +281,7 @@ func orcaWhirPoolLiquidityParser(tx *Tx, instruction Instruction, innerInstructi
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
||||
instructionName += "_on_side"
|
||||
instructionName += "_one_side"
|
||||
}
|
||||
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
||||
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
||||
@@ -370,10 +370,10 @@ func orcaWhirPoolLiquidityV2Parser(tx *Tx, instruction Instruction, innerInstruc
|
||||
continue
|
||||
}
|
||||
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
||||
baseAmount = decimal.NewFromInt(int64(amount))
|
||||
baseAmount = decimal.NewFromUint64(amount)
|
||||
baseFound = true
|
||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
||||
quoteAmount = decimal.NewFromUint64(amount)
|
||||
quoteFound = true
|
||||
}
|
||||
if baseFound && quoteFound {
|
||||
@@ -388,7 +388,7 @@ func orcaWhirPoolLiquidityV2Parser(tx *Tx, instruction Instruction, innerInstruc
|
||||
return nil, offset, InstructionIgnoredError
|
||||
}
|
||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
||||
instructionName += "_on_side"
|
||||
instructionName += "_one_side"
|
||||
}
|
||||
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
||||
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
||||
@@ -475,10 +475,10 @@ func orcaWhirPoolCollectFeeParser(tx *Tx, instruction Instruction, innerInstruct
|
||||
//return nil, increaseOffset(offset), fmt.Errorf("orca whirpool parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
||||
baseAmount = decimal.NewFromInt(int64(amount))
|
||||
baseAmount = decimal.NewFromUint64(amount)
|
||||
baseFound = true
|
||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
||||
quoteAmount = decimal.NewFromUint64(amount)
|
||||
quoteFound = true
|
||||
}
|
||||
if (baseFound && quoteFound) || i >= 6 {
|
||||
@@ -493,7 +493,7 @@ func orcaWhirPoolCollectFeeParser(tx *Tx, instruction Instruction, innerInstruct
|
||||
return nil, offset, InstructionIgnoredError
|
||||
}
|
||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
||||
instructionName += "_on_side"
|
||||
instructionName += "_one_side"
|
||||
}
|
||||
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
||||
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
||||
@@ -577,10 +577,10 @@ func orcaWhirPoolCollectFeeV2Parser(tx *Tx, instruction Instruction, innerInstru
|
||||
//return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
||||
baseAmount = decimal.NewFromInt(int64(amount))
|
||||
baseAmount = decimal.NewFromUint64(amount)
|
||||
baseFound = true
|
||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
||||
quoteAmount = decimal.NewFromUint64(amount)
|
||||
quoteFound = true
|
||||
}
|
||||
if (baseFound && quoteFound) || i >= 6 {
|
||||
@@ -595,7 +595,7 @@ func orcaWhirPoolCollectFeeV2Parser(tx *Tx, instruction Instruction, innerInstru
|
||||
return nil, offset, InstructionIgnoredError
|
||||
}
|
||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
||||
instructionName += "_on_side"
|
||||
instructionName += "_one_side"
|
||||
}
|
||||
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
||||
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
||||
@@ -679,10 +679,10 @@ func orcaWhirPoolCollectProtocolFeeV2Parser(tx *Tx, instruction Instruction, inn
|
||||
// return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
||||
baseAmount = decimal.NewFromInt(int64(amount))
|
||||
baseAmount = decimal.NewFromUint64(amount)
|
||||
baseFound = true
|
||||
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
||||
quoteAmount = decimal.NewFromUint64(amount)
|
||||
quoteFound = true
|
||||
}
|
||||
if (baseFound && quoteFound) || i >= 6 {
|
||||
@@ -697,7 +697,7 @@ func orcaWhirPoolCollectProtocolFeeV2Parser(tx *Tx, instruction Instruction, inn
|
||||
return nil, offset, InstructionIgnoredError
|
||||
}
|
||||
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
||||
instructionName += "_on_side"
|
||||
instructionName += "_one_side"
|
||||
}
|
||||
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
||||
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
||||
@@ -784,7 +784,7 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
|
||||
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
|
||||
baseAmount = decimal.NewFromInt(int64(amount))
|
||||
baseAmount = decimal.NewFromUint64(amount)
|
||||
if from.Equals(vault0Account) && to.Equals(token0Account) {
|
||||
event = "buy"
|
||||
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
|
||||
@@ -792,7 +792,7 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
|
||||
}
|
||||
baseFound = true
|
||||
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
|
||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
||||
quoteAmount = decimal.NewFromUint64(amount)
|
||||
if from.Equals(vault1Account) && to.Equals(token1Account) {
|
||||
event = "sell"
|
||||
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
|
||||
@@ -811,23 +811,23 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
|
||||
}
|
||||
|
||||
swap := Swap{
|
||||
Program: SolProgramOrcaWhirPool,
|
||||
Event: event,
|
||||
Pool: pool,
|
||||
BaseMint: baseTokenBalance.MintAccount,
|
||||
QuoteMint: quoteTokenBalance.MintAccount,
|
||||
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
||||
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
||||
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
||||
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
||||
BaseAmount: baseAmount,
|
||||
QuoteAmount: quoteAmount,
|
||||
BaseReserve: baseReserve,
|
||||
QuoteReserve: quoteReserve,
|
||||
UserBaseBalance: userBase,
|
||||
UserQuoteBalance: userQuote,
|
||||
User: user,
|
||||
EntryContract: entryContract,
|
||||
Program: SolProgramOrcaWhirPool,
|
||||
Event: event,
|
||||
Pool: pool,
|
||||
BaseMint: baseTokenBalance.MintAccount,
|
||||
QuoteMint: quoteTokenBalance.MintAccount,
|
||||
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
||||
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
||||
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
||||
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
||||
BaseAmount: baseAmount,
|
||||
QuoteAmount: quoteAmount,
|
||||
BaseReserve: baseReserve,
|
||||
QuoteReserve: quoteReserve,
|
||||
UserBaseBalance: userBase,
|
||||
UserQuoteBalance: userQuote,
|
||||
User: user,
|
||||
EntryContract: entryContract,
|
||||
}
|
||||
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
|
||||
|
||||
@@ -894,7 +894,7 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
|
||||
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swapv2 parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
|
||||
baseAmount = decimal.NewFromInt(int64(amount))
|
||||
baseAmount = decimal.NewFromUint64(amount)
|
||||
if from.Equals(vault0Account) && to.Equals(token0Account) {
|
||||
event = "buy"
|
||||
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
|
||||
@@ -902,7 +902,7 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
|
||||
}
|
||||
baseFound = true
|
||||
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
|
||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
||||
quoteAmount = decimal.NewFromUint64(amount)
|
||||
if from.Equals(vault1Account) && to.Equals(token1Account) {
|
||||
event = "sell"
|
||||
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
|
||||
@@ -922,23 +922,23 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
|
||||
offset[1] += uint(skipOffset + 1)
|
||||
|
||||
swap := Swap{
|
||||
Program: SolProgramOrcaWhirPool,
|
||||
Event: event,
|
||||
Pool: pool,
|
||||
BaseMint: baseTokenBalance.MintAccount,
|
||||
QuoteMint: quoteTokenBalance.MintAccount,
|
||||
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
||||
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
||||
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
||||
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
||||
BaseAmount: baseAmount,
|
||||
QuoteAmount: quoteAmount,
|
||||
BaseReserve: baseReserve,
|
||||
QuoteReserve: quoteReserve,
|
||||
UserBaseBalance: userBase,
|
||||
UserQuoteBalance: userQuote,
|
||||
User: user,
|
||||
EntryContract: entryContract,
|
||||
Program: SolProgramOrcaWhirPool,
|
||||
Event: event,
|
||||
Pool: pool,
|
||||
BaseMint: baseTokenBalance.MintAccount,
|
||||
QuoteMint: quoteTokenBalance.MintAccount,
|
||||
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
||||
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
||||
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
||||
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
||||
BaseAmount: baseAmount,
|
||||
QuoteAmount: quoteAmount,
|
||||
BaseReserve: baseReserve,
|
||||
QuoteReserve: quoteReserve,
|
||||
UserBaseBalance: userBase,
|
||||
UserQuoteBalance: userQuote,
|
||||
User: user,
|
||||
EntryContract: entryContract,
|
||||
}
|
||||
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
|
||||
|
||||
@@ -1011,7 +1011,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
|
||||
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
||||
baseAmount = decimal.NewFromInt(int64(amount))
|
||||
baseAmount = decimal.NewFromUint64(amount)
|
||||
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
|
||||
event = "buy"
|
||||
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
||||
@@ -1019,7 +1019,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
|
||||
}
|
||||
baseFound = true
|
||||
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
||||
quoteAmount = decimal.NewFromUint64(amount)
|
||||
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool1UserQuote]) {
|
||||
event = "sell"
|
||||
} else if from.Equals(tx.rawTx.accountList[pool1UserQuote]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
||||
@@ -1087,7 +1087,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
|
||||
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
||||
baseAmount = decimal.NewFromInt(int64(amount))
|
||||
baseAmount = decimal.NewFromUint64(amount)
|
||||
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool2UserBase]) {
|
||||
event = "buy"
|
||||
} else if from.Equals(tx.rawTx.accountList[pool2UserBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
||||
@@ -1095,7 +1095,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
|
||||
}
|
||||
baseFound = true
|
||||
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
||||
quoteAmount = decimal.NewFromUint64(amount)
|
||||
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
|
||||
event = "sell"
|
||||
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
||||
@@ -1152,6 +1152,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
|
||||
limitMint,
|
||||
actualLimitAmount,
|
||||
)
|
||||
swaps[0].SlippageBps = decimal.Zero
|
||||
return swaps, offset, nil
|
||||
}
|
||||
|
||||
@@ -1220,7 +1221,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
||||
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
||||
baseAmount = decimal.NewFromInt(int64(amount))
|
||||
baseAmount = decimal.NewFromUint64(amount)
|
||||
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
|
||||
event = "buy"
|
||||
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
||||
@@ -1228,7 +1229,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
||||
}
|
||||
baseFound = true
|
||||
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
||||
quoteAmount = decimal.NewFromUint64(amount)
|
||||
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
||||
event = "sell"
|
||||
} else if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
||||
@@ -1294,7 +1295,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
||||
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
||||
baseAmount = decimal.NewFromInt(int64(amount))
|
||||
baseAmount = decimal.NewFromUint64(amount)
|
||||
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
||||
event = "buy"
|
||||
} else if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
||||
@@ -1302,7 +1303,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
||||
}
|
||||
baseFound = true
|
||||
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
||||
quoteAmount = decimal.NewFromInt(int64(amount))
|
||||
quoteAmount = decimal.NewFromUint64(amount)
|
||||
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
|
||||
event = "sell"
|
||||
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
||||
@@ -1359,5 +1360,6 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
|
||||
limitMint,
|
||||
actualLimitAmount,
|
||||
)
|
||||
swaps[0].SlippageBps = decimal.Zero
|
||||
return swaps, offset, nil
|
||||
}
|
||||
|
||||
23
orcawhirpool_test.go
Normal file
23
orcawhirpool_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package pump_parser
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestOrcaWhirlpoolRemoveLiquidityPreservesLargeUint64TransferAmounts(t *testing.T) {
|
||||
EnableAllParsers()
|
||||
|
||||
tx := mustParseRPCFixtureTx(t, "4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri")
|
||||
if len(tx.Swaps) == 0 {
|
||||
t.Fatal("expected parsed swaps")
|
||||
}
|
||||
|
||||
swap := tx.Swaps[0]
|
||||
if swap.Program != SolProgramOrcaWhirPool {
|
||||
t.Fatalf("program = %s, want %s", swap.Program, SolProgramOrcaWhirPool)
|
||||
}
|
||||
if swap.Event != TxEventRemoveLiquidity {
|
||||
t.Fatalf("event = %s, want %s", swap.Event, TxEventRemoveLiquidity)
|
||||
}
|
||||
|
||||
assertDecimalString(t, "base_amount", swap.BaseAmount, "101086439062")
|
||||
assertDecimalString(t, "quote_amount", swap.QuoteAmount, "9863327902766042414")
|
||||
}
|
||||
49
pump.go
49
pump.go
@@ -231,6 +231,19 @@ func pumpTradeAmountInfoFromArgs(args PumpTradeArgs) (swapMode SwapMode, fixedAm
|
||||
}
|
||||
}
|
||||
|
||||
func pumpCompleteMatchesTradeEvent(completeEvent CompleteEvent, tradeEvent PumpTradeEvent, bondingCurve solana.PublicKey) bool {
|
||||
if completeEvent.Mint != tradeEvent.Mint {
|
||||
return false
|
||||
}
|
||||
if completeEvent.User != tradeEvent.User {
|
||||
return false
|
||||
}
|
||||
if completeEvent.BondingCurve != bondingCurve {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func normalizePumpQuoteSideMint(s *Swap) {
|
||||
if s.FixedAmountSide == SwapAmountSideQuote && s.FixedMint.IsZero() {
|
||||
s.FixedMint = wSolMint
|
||||
@@ -366,6 +379,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
completeEvent CompleteEvent
|
||||
completed bool
|
||||
newoffset [2]uint
|
||||
tradeFound bool
|
||||
)
|
||||
|
||||
var prefixLen = offset[1]
|
||||
@@ -394,6 +408,9 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
}
|
||||
if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) {
|
||||
if bytes.Equal(innerInstr.Data[8:16], pumpTradeEventDiscriminator[8:16]) {
|
||||
if tradeFound {
|
||||
break
|
||||
}
|
||||
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&tradeEvent)
|
||||
if offset[1] == 0 {
|
||||
newoffset = [2]uint{offset[0] + 1, offset[1]}
|
||||
@@ -403,19 +420,31 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
if err != nil {
|
||||
return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
expectedIsBuy := !bytes.Equal(instruction.Data[:8], pumpSellDiscriminator[:])
|
||||
if tradeEvent.IsBuy != expectedIsBuy {
|
||||
tradeEvent = PumpTradeEvent{}
|
||||
continue
|
||||
}
|
||||
tradeFound = true
|
||||
if !tradeEvent.IsBuy {
|
||||
break
|
||||
}
|
||||
} else if bytes.Equal(innerInstr.Data[8:16], pumpCompleteEventDiscriminator[:]) {
|
||||
if !tradeFound {
|
||||
continue
|
||||
}
|
||||
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&completeEvent)
|
||||
if err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, result.accountList[instruction.Accounts[3]]) {
|
||||
break
|
||||
}
|
||||
if offset[1] == 0 {
|
||||
newoffset = [2]uint{offset[0] + 1, offset[1]}
|
||||
} else {
|
||||
newoffset = [2]uint{offset[0], prefixLen + uint(innerIndex) + 1}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, newoffset, fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
completed = true
|
||||
break
|
||||
}
|
||||
@@ -428,6 +457,11 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
|
||||
offset = [2]uint{newoffset[0], newoffset[1]}
|
||||
|
||||
var args PumpTradeArgs
|
||||
if err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args); err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump buy/sell decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
|
||||
event := ""
|
||||
baseTokenProgram := solana.TokenProgramID
|
||||
if tradeEvent.IsBuy {
|
||||
@@ -495,12 +529,9 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
|
||||
Cashback: isCashbackCoin,
|
||||
},
|
||||
}
|
||||
var args PumpTradeArgs
|
||||
if err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args); err == nil {
|
||||
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
|
||||
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||
normalizePumpQuoteSideMint(&swaps[0])
|
||||
}
|
||||
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
|
||||
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||
normalizePumpQuoteSideMint(&swaps[0])
|
||||
}
|
||||
if completed {
|
||||
swaps = append(swaps, Swap{
|
||||
|
||||
24
pump_test.go
24
pump_test.go
@@ -76,3 +76,27 @@ func TestCal(t *testing.T) {
|
||||
|
||||
fmt.Println(solana.MustPublicKeyFromBase58("BM9CcyErJcu2mjrFvUsRRrD3snGeHDDVirJLvL6EjvMN").IsOnCurve())
|
||||
}
|
||||
|
||||
func TestPumpCompleteMatchesTradeEvent(t *testing.T) {
|
||||
mint := solana.MustPublicKeyFromBase58("8GNGkNnfBuoTP3QRnmdNzSYuuE15M8tvcNvxNsV4pump")
|
||||
user := solana.MustPublicKeyFromBase58("DS95KxqUCCjwQaXhD7fhKatXbivwWDNrJdNV5ZcubGdz")
|
||||
bondingCurve := solana.MustPublicKeyFromBase58("Gz5EX3X7kUDS48baijJKubQDKy3BBKpnMJQ3f3W1e9jA")
|
||||
|
||||
tradeEvent := PumpTradeEvent{
|
||||
Mint: mint,
|
||||
User: user,
|
||||
}
|
||||
completeEvent := CompleteEvent{
|
||||
Mint: mint,
|
||||
User: user,
|
||||
BondingCurve: bondingCurve,
|
||||
}
|
||||
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, bondingCurve) {
|
||||
t.Fatal("pumpCompleteMatchesTradeEvent() = false, want true")
|
||||
}
|
||||
|
||||
completeEvent.User = solana.MustPublicKeyFromBase58("3g89wLRwJ5P22fkCdPJBAP7iiYAo6yY96geQvMYj6tYm")
|
||||
if pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, bondingCurve) {
|
||||
t.Fatal("pumpCompleteMatchesTradeEvent() = true for mismatched user")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]]),
|
||||
|
||||
1742
rawtx_binary.go
Normal file
1742
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
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
18
tx_binary.go
18
tx_binary.go
@@ -12,6 +12,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
"github.com/mr-tron/base58"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
@@ -186,7 +187,7 @@ func NewTxsBinary(txs []Tx) (*TxsBinary, error) {
|
||||
for i, tx := range txPtrs {
|
||||
binaryTx, err := newTxBinaryWithAddressTable(tx, addressTable, addressIndex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tx[%d]: %w", i, err)
|
||||
return nil, fmt.Errorf("tx[%d], %s: %w", i, base58.Encode(tx.TxHash[:]), err)
|
||||
}
|
||||
out.Txs = append(out.Txs, *binaryTx)
|
||||
}
|
||||
@@ -478,7 +479,7 @@ func (txs *TxsBinary) MarshalBinary() ([]byte, error) {
|
||||
enc.writeUint32(uint32(len(txs.Txs)))
|
||||
for i := range txs.Txs {
|
||||
if err := enc.writeTxBinaryBody(&txs.Txs[i], enumTable); err != nil {
|
||||
return nil, fmt.Errorf("tx[%d]: %w", i, err)
|
||||
return nil, fmt.Errorf("tx[%d], %s: %w", i, base58.Encode(txs.Txs[i].TxHash[:]), err)
|
||||
}
|
||||
}
|
||||
return enc.bytes(), nil
|
||||
@@ -728,7 +729,7 @@ func newSwapBinary(swap Swap, index int, addressIndex *txBinaryAddressIndex) (Sw
|
||||
|
||||
out := SwapBinary{
|
||||
Program: swap.Program,
|
||||
Event: swap.Event,
|
||||
Event: txBinaryCanonicalEvent(swap.Event),
|
||||
TxIndex: int32(swap.TxIndex),
|
||||
InstrIdx: swap.InstrIdx,
|
||||
InnerIdx: swap.InnerIdx,
|
||||
@@ -918,6 +919,17 @@ func txBinaryPlatformsFromTx(platforms map[string]platformInfo) ([]PlatformBinar
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func txBinaryCanonicalEvent(event string) string {
|
||||
switch event {
|
||||
case "add_liquidity_on_side":
|
||||
return TxEventAddLiquidityOneSide
|
||||
case "remove_liquidity_on_side":
|
||||
return TxEventRemoveLiquidityOneSide
|
||||
default:
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
func txBinaryMevAgentsFromTx(mevAgents map[string]mevInfo) ([]MevAgentBinary, error) {
|
||||
if len(mevAgents) == 0 {
|
||||
return nil, nil
|
||||
|
||||
@@ -354,6 +354,62 @@ func TestTxBinaryPreservesFractionalReserves(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxBinaryCanonicalizesOnSideEventAlias(t *testing.T) {
|
||||
tx := &Tx{
|
||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||
Block: 1,
|
||||
BlockIndex: 1,
|
||||
CuFee: decimal.NewFromInt(1),
|
||||
CUPrice: decimal.RequireFromString("0.000001"),
|
||||
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
|
||||
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
|
||||
ComputeUnitsConsumed: 1,
|
||||
CuLimit: 1,
|
||||
Swaps: []Swap{
|
||||
{
|
||||
Program: SolProgramOrcaWhirPool,
|
||||
Event: "remove_liquidity_on_side",
|
||||
Pool: mustPubKey("11111111111111111111111111111111"),
|
||||
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||
QuoteMint: solana.WrappedSol,
|
||||
BaseTokenProgram: solana.TokenProgramID,
|
||||
QuoteTokenProgram: solana.TokenProgramID,
|
||||
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
|
||||
BaseMintDecimals: 6,
|
||||
QuoteMintDecimals: 9,
|
||||
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
|
||||
BaseAmount: decimal.NewFromInt(10),
|
||||
QuoteAmount: decimal.Zero,
|
||||
SwapMode: SwapModeExactIn,
|
||||
FixedAmount: decimal.NewFromInt(10),
|
||||
FixedAmountSide: SwapAmountSideBase,
|
||||
FixedMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
|
||||
LimitAmountType: SwapLimitTypeMinOut,
|
||||
LimitAmount: decimal.Zero,
|
||||
LimitAmountSide: SwapAmountSideQuote,
|
||||
ActualLimitAmount: decimal.Zero,
|
||||
ActualLimitAmountSide: SwapAmountSideQuote,
|
||||
BaseReserve: decimal.RequireFromString("123.4"),
|
||||
QuoteReserve: decimal.RequireFromString("456.7"),
|
||||
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded, err := EncodeTxBinary(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeTxBinary() error = %v", err)
|
||||
}
|
||||
|
||||
decoded, err := DecodeTxBinary(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeTxBinary() error = %v", err)
|
||||
}
|
||||
if got := decoded.Swaps[0].Event; got != TxEventRemoveLiquidityOneSide {
|
||||
t.Fatalf("Event = %q, want %q", got, TxEventRemoveLiquidityOneSide)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
|
||||
tx1 := Tx{
|
||||
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
|
||||
|
||||
Reference in New Issue
Block a user