diff --git a/tx_binary.go b/tx_binary.go index 46b8dcd..cb737bc 100644 --- a/tx_binary.go +++ b/tx_binary.go @@ -17,7 +17,8 @@ import ( ) const ( - txBinarySchemaVersionCurrent uint16 = 3 + txBinarySchemaVersionV3 uint16 = 3 + txBinarySchemaVersionCurrent uint16 = 4 txBinaryEnumVersionV1 uint16 = 1 txBinarySOLScale int32 = 9 @@ -27,6 +28,10 @@ const ( var txBinaryMagic = [4]byte{'P', 'T', 'X', 'B'} var txsBinaryMagic = [4]byte{'P', 'T', 'X', 'S'} +func txBinarySchemaVersionSupported(version uint16) bool { + return version >= txBinarySchemaVersionV3 && version <= txBinarySchemaVersionCurrent +} + type TxBinary struct { SchemaVersion uint16 EnumVersion uint16 @@ -34,6 +39,7 @@ type TxBinary struct { Signer uint32 Block uint64 BlockIndex uint64 + BlockAt int64 TxHash *[64]byte CuFee uint64 Swaps []SwapBinary @@ -204,6 +210,7 @@ func newTxBinaryWithAddressTable(tx *Tx, addressTable []solana.PublicKey, addres EnumVersion: txBinaryEnumVersionV1, Block: tx.Block, BlockIndex: tx.BlockIndex, + BlockAt: tx.BlockAt, CuLimit: tx.CuLimit, ComputeUnitsConsumed: tx.ComputeUnitsConsumed, } @@ -411,6 +418,8 @@ func MergeTxsBinarySourcesToWriterWithOptions(sources []TxsBinaryReaderSource, w return fmt.Errorf("source[%d].batch[%d].tx[%d]: %w", sourceIndex, batchIndex, txIndex, err) } + tx.SchemaVersion = plan.schemaVersion + tx.EnumVersion = plan.enumVersion bodyBytes, err := txBinaryMarshalTxBody(&tx, plan.enumTable) if err != nil { reader.Close() @@ -432,7 +441,7 @@ func (tx *TxBinary) MarshalBinary() ([]byte, error) { if tx == nil { return nil, fmt.Errorf("tx binary is nil") } - if tx.SchemaVersion != txBinarySchemaVersionCurrent { + if !txBinarySchemaVersionSupported(tx.SchemaVersion) { return nil, fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion) } @@ -460,7 +469,7 @@ func (txs *TxsBinary) MarshalBinary() ([]byte, error) { if txs == nil { return nil, fmt.Errorf("txs binary is nil") } - if txs.SchemaVersion != txBinarySchemaVersionCurrent { + if !txBinarySchemaVersionSupported(txs.SchemaVersion) { return nil, fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion) } @@ -478,8 +487,11 @@ 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], %s: %w", i, base58.Encode(txs.Txs[i].TxHash[:]), err) + tx := txs.Txs[i] + tx.SchemaVersion = txs.SchemaVersion + tx.EnumVersion = txs.EnumVersion + if err := enc.writeTxBinaryBody(&tx, enumTable); err != nil { + return nil, fmt.Errorf("tx[%d], %s: %w", i, base58.Encode(tx.TxHash[:]), err) } } return enc.bytes(), nil @@ -520,7 +532,7 @@ func (tx *TxBinary) UnmarshalBinary(data []byte) error { if err != nil { return err } - if tx.SchemaVersion != txBinarySchemaVersionCurrent { + if !txBinarySchemaVersionSupported(tx.SchemaVersion) { return fmt.Errorf("unsupported tx binary schema version: %d", tx.SchemaVersion) } @@ -560,7 +572,7 @@ func (txs *TxsBinary) UnmarshalBinary(data []byte) error { if err != nil { return err } - if txs.SchemaVersion != txBinarySchemaVersionCurrent { + if !txBinarySchemaVersionSupported(txs.SchemaVersion) { return fmt.Errorf("unsupported tx binary schema version: %d", txs.SchemaVersion) } @@ -613,6 +625,7 @@ func (tx *TxBinary) ToTx() (*Tx, error) { Signer: signer, Block: tx.Block, BlockIndex: tx.BlockIndex, + BlockAt: tx.BlockAt, CuFee: decimal.NewFromUint64(tx.CuFee), CUPrice: decimal.NewFromUint64(tx.CUPrice).Shift(-txBinaryCUPriceScale), BeforeSolBalance: txBinaryFloat64ToDecimal(tx.BeforeSolBalance, txBinarySOLScale), @@ -1166,6 +1179,9 @@ func (enc *txBinaryEncoder) writeTxBinaryBody(tx *TxBinary, enumTable *txBinaryE enc.writeUint32(tx.Signer) enc.writeUint64(tx.Block) enc.writeUint64(tx.BlockIndex) + if tx.SchemaVersion >= txBinarySchemaVersionCurrent { + enc.writeUint64(uint64(tx.BlockAt)) + } enc.writeBool(tx.TxHash != nil) if tx.TxHash != nil { enc.writeBytes(tx.TxHash[:]) @@ -1474,7 +1490,7 @@ func (dec *txBinaryStreamDecoder) readTxsBinaryHeader() (*txsBinaryHeader, error if err != nil { return nil, err } - if schemaVersion != txBinarySchemaVersionCurrent { + if !txBinarySchemaVersionSupported(schemaVersion) { return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion) } @@ -1531,7 +1547,7 @@ func (dec *txBinaryStreamDecoder) readTxsBinaryHeaderOrEOF() (*txsBinaryHeader, if err != nil { return nil, err } - if schemaVersion != txBinarySchemaVersionCurrent { + if !txBinarySchemaVersionSupported(schemaVersion) { return nil, fmt.Errorf("unsupported tx binary schema version: %d", schemaVersion) } @@ -1799,6 +1815,13 @@ func txBinaryReadTxBody(dec txBinaryBodyReader, tx *TxBinary, enumTable *txBinar if tx.BlockIndex, err = dec.readUint64(); err != nil { return err } + if tx.SchemaVersion >= txBinarySchemaVersionCurrent { + blockAt, err := dec.readUint64() + if err != nil { + return err + } + tx.BlockAt = int64(blockAt) + } hasTxHash, err := dec.readBool() if err != nil { @@ -1854,7 +1877,9 @@ func txBinaryBuildMergePlan(sources []TxsBinaryReaderSource, opts TxsBinaryMerge builder := txBinaryAddressTableBuilder{ index: make(map[solana.PublicKey]struct{}), } - plan := &txsBinaryMergePlan{} + plan := &txsBinaryMergePlan{ + schemaVersion: txBinarySchemaVersionCurrent, + } hasBatch := false for sourceIndex, source := range sources { @@ -1896,15 +1921,10 @@ func txBinaryBuildMergePlan(sources []TxsBinaryReaderSource, opts TxsBinaryMerge } if !hasBatch { - plan.schemaVersion = header.schemaVersion plan.enumVersion = header.enumVersion plan.enumTable = header.enumTable hasBatch = true } else { - if header.schemaVersion != plan.schemaVersion { - reader.Close() - return nil, fmt.Errorf("source[%d].batch[%d]: schema version mismatch: got %d want %d", sourceIndex, batchIndex, header.schemaVersion, plan.schemaVersion) - } if header.enumVersion != plan.enumVersion { reader.Close() return nil, fmt.Errorf("source[%d].batch[%d]: enum version mismatch: got %d want %d", sourceIndex, batchIndex, header.enumVersion, plan.enumVersion) diff --git a/tx_binary_test.go b/tx_binary_test.go index 2b7ea4b..8176e4e 100644 --- a/tx_binary_test.go +++ b/tx_binary_test.go @@ -19,6 +19,7 @@ func TestTxBinaryRoundTrip(t *testing.T) { Signer: mustPubKey("So11111111111111111111111111111111111111112"), Block: 123456789, BlockIndex: 42, + BlockAt: 1710000000, TxHash: &txHash, CuFee: decimal.NewFromInt(5000), CUPrice: decimal.RequireFromString("0.123456"), @@ -118,6 +119,9 @@ func TestTxBinaryRoundTrip(t *testing.T) { if decoded.BlockIndex != original.BlockIndex { t.Fatalf("BlockIndex = %d, want %d", decoded.BlockIndex, original.BlockIndex) } + if decoded.BlockAt != original.BlockAt { + t.Fatalf("BlockAt = %d, want %d", decoded.BlockAt, original.BlockAt) + } if decoded.TxHash == nil { t.Fatal("TxHash = nil, want non-nil") } @@ -510,6 +514,7 @@ func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) { Signer: mustPubKey("So11111111111111111111111111111111111111112"), Block: 1, BlockIndex: 1, + BlockAt: 1710000001, CuFee: decimal.NewFromInt(1000), CUPrice: decimal.RequireFromString("0.123456"), BeforeSolBalance: decimal.RequireFromString("1.000000000"), @@ -560,6 +565,7 @@ func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) { tx2 := tx1 tx2.Block = 2 tx2.BlockIndex = 2 + tx2.BlockAt = 1710000002 tx2.CuFee = decimal.NewFromInt(2000) tx2.AfterSOLBalance = decimal.RequireFromString("0.700000000") tx2.Swaps = []Swap{tx1.Swaps[0]} @@ -581,6 +587,9 @@ func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) { if decoded[0].Signer != tx1.Signer || decoded[1].Signer != tx2.Signer { t.Fatalf("decoded signer mismatch") } + if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt { + t.Fatalf("decoded block_at mismatch") + } if decoded[0].Swaps[0].Pool != tx1.Swaps[0].Pool || decoded[1].Swaps[0].Pool != tx2.Swaps[0].Pool { t.Fatalf("decoded shared address mismatch") } @@ -603,6 +612,7 @@ func TestDecodeTxsBinaryReader(t *testing.T) { Signer: mustPubKey("So11111111111111111111111111111111111111112"), Block: 100, BlockIndex: 7, + BlockAt: 1710000100, CuFee: decimal.NewFromInt(111), CUPrice: decimal.RequireFromString("0.123456"), BeforeSolBalance: decimal.RequireFromString("1.000000000"), @@ -649,6 +659,7 @@ func TestDecodeTxsBinaryReader(t *testing.T) { tx2 := tx1 tx2.Block = 101 tx2.BlockIndex = 8 + tx2.BlockAt = 1710000101 tx2.CuFee = decimal.NewFromInt(222) tx2.AfterSOLBalance = decimal.RequireFromString("0.300000000") tx2.Swaps = []Swap{tx1.Swaps[0]} @@ -677,6 +688,9 @@ func TestDecodeTxsBinaryReader(t *testing.T) { if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block { t.Fatalf("decoded block mismatch") } + if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt { + t.Fatalf("decoded block_at mismatch") + } if decoded[0].Swaps[0].BaseAmount.Cmp(tx1.Swaps[0].BaseAmount) != 0 { t.Fatalf("decoded tx1 swap base amount = %s, want %s", decoded[0].Swaps[0].BaseAmount, tx1.Swaps[0].BaseAmount) } @@ -724,6 +738,7 @@ func TestMergeTxsBinaryBytes(t *testing.T) { Signer: mustPubKey("So11111111111111111111111111111111111111112"), Block: 11, BlockIndex: 1, + BlockAt: 1710000011, CuFee: decimal.NewFromInt(10), CUPrice: decimal.RequireFromString("0.000123"), BeforeSolBalance: decimal.RequireFromString("1.100000000"), @@ -755,6 +770,7 @@ func TestMergeTxsBinaryBytes(t *testing.T) { Signer: mustPubKey("SysvarRent111111111111111111111111111111111"), Block: 12, BlockIndex: 2, + BlockAt: 1710000012, CuFee: decimal.NewFromInt(20), CUPrice: decimal.RequireFromString("0.000456"), BeforeSolBalance: decimal.RequireFromString("2.200000000"), @@ -818,6 +834,9 @@ func TestMergeTxsBinaryBytes(t *testing.T) { if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block { t.Fatalf("decoded block mismatch") } + if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt { + t.Fatalf("decoded block_at mismatch") + } } func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) { @@ -825,6 +844,7 @@ func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) { Signer: mustPubKey("So11111111111111111111111111111111111111112"), Block: 21, BlockIndex: 1, + BlockAt: 1710000021, CuFee: decimal.NewFromInt(1), CUPrice: decimal.RequireFromString("0.000001"), BeforeSolBalance: decimal.RequireFromString("1.000000000"), @@ -835,9 +855,11 @@ func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) { tx2 := tx1 tx2.Block = 22 tx2.BlockIndex = 2 + tx2.BlockAt = 1710000022 tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111") tx3 := tx1 tx3.Block = 23 + tx3.BlockAt = 1710000023 tx3.BlockIndex = 3 tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111") @@ -880,6 +902,9 @@ func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) { if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block || decoded[2].Block != tx3.Block { t.Fatalf("decoded block order mismatch") } + if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx2.BlockAt || decoded[2].BlockAt != tx3.BlockAt { + t.Fatalf("decoded block_at order mismatch") + } } func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) { @@ -887,6 +912,7 @@ func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) { Signer: mustPubKey("So11111111111111111111111111111111111111112"), Block: 31, BlockIndex: 1, + BlockAt: 1710000031, CuFee: decimal.NewFromInt(1), CUPrice: decimal.RequireFromString("0.000001"), BeforeSolBalance: decimal.RequireFromString("1.000000000"), @@ -897,10 +923,12 @@ func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) { tx2 := tx1 tx2.Block = 32 tx2.BlockIndex = 2 + tx2.BlockAt = 1710000032 tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111") tx3 := tx1 tx3.Block = 33 tx3.BlockIndex = 3 + tx3.BlockAt = 1710000033 tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111") batch1, err := EncodeTxsBinary([]Tx{tx1}) @@ -960,15 +988,127 @@ func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) { if decoded[0].Block != tx1.Block || decoded[1].Block != tx3.Block { t.Fatalf("decoded block order mismatch after skip") } + if decoded[0].BlockAt != tx1.BlockAt || decoded[1].BlockAt != tx3.BlockAt { + t.Fatalf("decoded block_at order mismatch after skip") + } if source.opens != 2 { t.Fatalf("source.opens = %d, want 2", source.opens) } } +func TestTxBinaryDecodeSchemaV3LeavesBlockAtZero(t *testing.T) { + original := &Tx{ + Signer: mustPubKey("So11111111111111111111111111111111111111112"), + Block: 41, + BlockIndex: 1, + BlockAt: 1710000041, + } + + encoded := mustEncodeTxBinaryV3(t, original) + decoded, err := DecodeTxBinary(encoded) + if err != nil { + t.Fatalf("DecodeTxBinary(v3) error = %v", err) + } + if decoded.Block != original.Block || decoded.BlockIndex != original.BlockIndex { + t.Fatalf("decoded block mismatch: got (%d,%d), want (%d,%d)", decoded.Block, decoded.BlockIndex, original.Block, original.BlockIndex) + } + if decoded.BlockAt != 0 { + t.Fatalf("BlockAt = %d, want 0 for legacy v3", decoded.BlockAt) + } +} + +func TestMergeTxsBinaryBytesUpgradesSchemaV3AndPreservesV4BlockAt(t *testing.T) { + legacyTx := Tx{ + Signer: mustPubKey("So11111111111111111111111111111111111111112"), + Block: 51, + BlockIndex: 1, + BlockAt: 1710000051, + } + currentTx := Tx{ + Signer: mustPubKey("SysvarRent111111111111111111111111111111111"), + Block: 52, + BlockIndex: 2, + BlockAt: 1710000052, + } + + merged, err := MergeTxsBinaryBytes([][]byte{ + mustEncodeTxsBinaryV3(t, []Tx{legacyTx}), + mustEncodeTxsBinary(t, []Tx{currentTx}), + }) + if err != nil { + t.Fatalf("MergeTxsBinaryBytes(v3,v4) error = %v", err) + } + + var mergedBinary TxsBinary + if err := mergedBinary.UnmarshalBinary(merged); err != nil { + t.Fatalf("UnmarshalBinary(merged) error = %v", err) + } + if mergedBinary.SchemaVersion != txBinarySchemaVersionCurrent { + t.Fatalf("merged schema version = %d, want %d", mergedBinary.SchemaVersion, txBinarySchemaVersionCurrent) + } + + decoded, err := DecodeTxsBinary(merged) + if err != nil { + t.Fatalf("DecodeTxsBinary(merged) error = %v", err) + } + if len(decoded) != 2 { + t.Fatalf("decoded len = %d, want 2", len(decoded)) + } + if decoded[0].BlockAt != 0 { + t.Fatalf("legacy BlockAt = %d, want 0", decoded[0].BlockAt) + } + if decoded[1].BlockAt != currentTx.BlockAt { + t.Fatalf("current BlockAt = %d, want %d", decoded[1].BlockAt, currentTx.BlockAt) + } +} + func mustPubKey(value string) solana.PublicKey { return solana.MustPublicKeyFromBase58(value) } +func mustEncodeTxBinaryV3(t *testing.T, tx *Tx) []byte { + t.Helper() + + binaryTx, err := NewTxBinary(tx) + if err != nil { + t.Fatalf("NewTxBinary() error = %v", err) + } + binaryTx.SchemaVersion = txBinarySchemaVersionV3 + encoded, err := binaryTx.MarshalBinary() + if err != nil { + t.Fatalf("MarshalBinary(v3) error = %v", err) + } + return encoded +} + +func mustEncodeTxsBinary(t *testing.T, txs []Tx) []byte { + t.Helper() + + encoded, err := EncodeTxsBinary(txs) + if err != nil { + t.Fatalf("EncodeTxsBinary() error = %v", err) + } + return encoded +} + +func mustEncodeTxsBinaryV3(t *testing.T, txs []Tx) []byte { + t.Helper() + + binaryTxs, err := NewTxsBinary(txs) + if err != nil { + t.Fatalf("NewTxsBinary() error = %v", err) + } + binaryTxs.SchemaVersion = txBinarySchemaVersionV3 + for i := range binaryTxs.Txs { + binaryTxs.Txs[i].SchemaVersion = txBinarySchemaVersionV3 + } + encoded, err := binaryTxs.MarshalBinary() + if err != nil { + t.Fatalf("MarshalBinary(v3) error = %v", err) + } + return encoded +} + func mustTxBinary(t *testing.T, data []byte) *TxsBinary { t.Helper()