rawtx binary
This commit is contained in:
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