Compare commits
2 Commits
273e87b8ad
...
v0.2.43
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43659ea4e4 | ||
|
|
6414e6a25f |
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
|
||||||
|
}
|
||||||
@@ -2006,16 +2006,12 @@ func resolveDlmmSwapAccounts(result *RawTx, accounts []int) (dlmmSwapAccounts, e
|
|||||||
if eventAuthorityPos < len(accounts) && accountList[accounts[eventAuthorityPos]].Equals(solana.MemoProgramID) {
|
if eventAuthorityPos < len(accounts) && accountList[accounts[eventAuthorityPos]].Equals(solana.MemoProgramID) {
|
||||||
eventAuthorityPos++
|
eventAuthorityPos++
|
||||||
}
|
}
|
||||||
programPos := eventAuthorityPos + 1
|
if eventAuthorityPos >= len(accounts) {
|
||||||
if programPos >= len(accounts) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !accountList[accounts[eventAuthorityPos]].Equals(meteoraDlmmEventAuthority) {
|
if !accountList[accounts[eventAuthorityPos]].Equals(meteoraDlmmEventAuthority) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !accountList[accounts[programPos]].Equals(meteoraDlmmProgram) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if hostFeePresent && oraclePos+1 < len(accounts) && dlmmIsSigner(result, accounts[oraclePos+1]) {
|
if hostFeePresent && oraclePos+1 < len(accounts) && dlmmIsSigner(result, accounts[oraclePos+1]) {
|
||||||
continue
|
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) {
|
func TestMeteoraDlmmInitializeParserUsesLbPairCreateEvent(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -616,6 +616,10 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
|||||||
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
|
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
|
||||||
}
|
}
|
||||||
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
|
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
|
||||||
|
quoteAmount := decimal.NewFromUint64(event.UserQuoteAmountIn)
|
||||||
|
if event.IxName == "buy" {
|
||||||
|
quoteAmount = decimal.NewFromUint64(event.QuoteAmountIn)
|
||||||
|
}
|
||||||
swap := Swap{
|
swap := Swap{
|
||||||
Program: SolProgramPumpAMM,
|
Program: SolProgramPumpAMM,
|
||||||
Event: "buy",
|
Event: "buy",
|
||||||
@@ -629,7 +633,7 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
|
|||||||
QuoteMintDecimals: quoteMintDecimals,
|
QuoteMintDecimals: quoteMintDecimals,
|
||||||
User: eventUser,
|
User: eventUser,
|
||||||
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
|
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
|
||||||
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountIn),
|
QuoteAmount: quoteAmount,
|
||||||
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
|
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
|
||||||
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
|
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
|
||||||
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user