Compare commits

...

1 Commits

Author SHA1 Message Date
thloyi
895230aaac update amm virtual quotes reserves 2026-07-17 17:02:34 +08:00
7 changed files with 528 additions and 69 deletions

View File

@@ -100,7 +100,8 @@ Each `Swap` describes one protocol-level swap, liquidity, migration, or pool ope
| `BaseMintDecimals` / `QuoteMintDecimals` | Decimals used to interpret raw token amounts. | | `BaseMintDecimals` / `QuoteMintDecimals` | Decimals used to interpret raw token amounts. |
| `User` | User or effective owner account for the action. If the parsed user is not on-curve, the parser may fall back to the transaction signer. | | `User` | User or effective owner account for the action. If the parsed user is not on-curve, the parser may fall back to the transaction signer. |
| `BaseAmount` / `QuoteAmount` | Actual parsed base-side and quote-side amounts, stored as `decimal.Decimal`. | | `BaseAmount` / `QuoteAmount` | Actual parsed base-side and quote-side amounts, stored as `decimal.Decimal`. |
| `BaseReserve` / `QuoteReserve` | Pool reserves when the protocol event or accounts expose them. | | `BaseReserve` / `QuoteReserve` | Pool reserves when the protocol event or accounts expose them. For Pump AMM buy/sell events, `QuoteReserve` is the post-trade effective quote reserve. |
| `RealQuoteReserve` / `VirtualQuoteReserve` | Quote-reserve components. Pump AMM buy/sell events expose the post-trade real reserve and the signed virtual reserve separately. |
| `UserBaseBalance` / `UserQuoteBalance` | User token balances after the transaction when available from token balance metadata. | | `UserBaseBalance` / `UserQuoteBalance` | User token balances after the transaction when available from token balance metadata. |
| `AfterSOLBalance` | User or signer SOL balance after the transaction. | | `AfterSOLBalance` | User or signer SOL balance after the transaction. |
| `EntryContract` | Known router / entry contract account when detected. | | `EntryContract` | Known router / entry contract account when detected. |
@@ -179,6 +180,7 @@ Parsed transaction binary:
- `EncodeTxsBinary` / `DecodeTxsBinary` for a batch of parsed `Tx`. - `EncodeTxsBinary` / `DecodeTxsBinary` for a batch of parsed `Tx`.
- `DecodeTxsBinaryReader` for streaming `PTXS` reads. - `DecodeTxsBinaryReader` for streaming `PTXS` reads.
- `MergeTxsBinaryBytes` and `MergeTxsBinarySourcesToWriter` for merging `PTXS` batches. - `MergeTxsBinaryBytes` and `MergeTxsBinarySourcesToWriter` for merging `PTXS` batches.
- Schema v5 persists `RealQuoteReserve` and signed `VirtualQuoteReserve`; v3/v4 inputs remain readable and default these components to legacy `QuoteReserve` and zero.
Raw transaction binary: Raw transaction binary:

View File

@@ -268,10 +268,12 @@ func (tx *Tx) Parser() error {
lastReserve := make(map[solana.PublicKey]reserveSnapshot, len(tx.Swaps)) lastReserve := make(map[solana.PublicKey]reserveSnapshot, len(tx.Swaps))
for _, swap := range tx.Swaps { for _, swap := range tx.Swaps {
lastReserve[pairKey(swap)] = reserveSnapshot{ lastReserve[pairKey(swap)] = reserveSnapshot{
baseMint: swap.BaseMint, baseMint: swap.BaseMint,
quoteMint: swap.QuoteMint, quoteMint: swap.QuoteMint,
baseReserve: swap.BaseReserve, baseReserve: swap.BaseReserve,
quoteReserve: swap.QuoteReserve, quoteReserve: swap.QuoteReserve,
realQuoteReserve: swap.RealQuoteReserve,
virtualQuoteReserve: swap.VirtualQuoteReserve,
} }
} }
@@ -281,6 +283,8 @@ func (tx *Tx) Parser() error {
if tx.Swaps[i].BaseMint == v.baseMint && tx.Swaps[i].QuoteMint == v.quoteMint { if tx.Swaps[i].BaseMint == v.baseMint && tx.Swaps[i].QuoteMint == v.quoteMint {
tx.Swaps[i].BaseReserve = v.baseReserve tx.Swaps[i].BaseReserve = v.baseReserve
tx.Swaps[i].QuoteReserve = v.quoteReserve tx.Swaps[i].QuoteReserve = v.quoteReserve
tx.Swaps[i].RealQuoteReserve = v.realQuoteReserve
tx.Swaps[i].VirtualQuoteReserve = v.virtualQuoteReserve
} else if tx.Swaps[i].BaseMint == v.quoteMint && tx.Swaps[i].QuoteMint == v.baseMint { } else if tx.Swaps[i].BaseMint == v.quoteMint && tx.Swaps[i].QuoteMint == v.baseMint {
tx.Swaps[i].BaseReserve = v.quoteReserve tx.Swaps[i].BaseReserve = v.quoteReserve
tx.Swaps[i].QuoteReserve = v.baseReserve tx.Swaps[i].QuoteReserve = v.baseReserve
@@ -297,10 +301,12 @@ func (tx *Tx) Parser() error {
} }
type reserveSnapshot struct { type reserveSnapshot struct {
baseMint solana.PublicKey baseMint solana.PublicKey
quoteMint solana.PublicKey quoteMint solana.PublicKey
baseReserve decimal.Decimal baseReserve decimal.Decimal
quoteReserve decimal.Decimal quoteReserve decimal.Decimal
realQuoteReserve decimal.Decimal
virtualQuoteReserve decimal.Decimal
} }
func cloneSwapPrograms(src map[solana.PublicKey]swapParser) map[solana.PublicKey]swapParser { func cloneSwapPrograms(src map[solana.PublicKey]swapParser) map[solana.PublicKey]swapParser {

View File

@@ -9,7 +9,7 @@ import (
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
) )
type ammBuyEvent struct { type ammBuyEventPrefix struct {
TimeStamp int64 TimeStamp int64
BaseAmountOut uint64 BaseAmountOut uint64
MaxQuoteAmountIn uint64 MaxQuoteAmountIn uint64
@@ -45,6 +45,38 @@ type ammBuyEvent struct {
Cashback uint64 Cashback uint64
} }
type ammBuyEvent struct {
ammBuyEventPrefix
BuybackFeeBasisPoints uint64
BuybackFee uint64
VirtualQuoteReserves agbinary.Int128
CanBoost bool
BaseSupply uint64
}
type ammTradeEventBuybackSuffix struct {
BuybackFeeBasisPoints uint64
BuybackFee uint64
}
type ammTradeEventVirtualSuffix struct {
VirtualQuoteReserves agbinary.Int128
CanBoost bool
BaseSupply uint64
}
func decodeAmmBuyEvent(data []byte) (ammBuyEvent, error) {
var event ammBuyEvent
decoder := agbinary.NewBorshDecoder(data)
if err := decoder.Decode(&event.ammBuyEventPrefix); err != nil {
return ammBuyEvent{}, err
}
if err := decodeAmmTradeEventSuffix(decoder, &event.BuybackFeeBasisPoints, &event.BuybackFee, &event.VirtualQuoteReserves, &event.CanBoost, &event.BaseSupply); err != nil {
return ammBuyEvent{}, err
}
return event, nil
}
type ammCreatePoolEvent struct { type ammCreatePoolEvent struct {
TimeStamp int64 TimeStamp int64
Index uint16 Index uint16
@@ -90,7 +122,7 @@ type ammDepositEvent struct {
UserPoolTokenAccount solana.PublicKey UserPoolTokenAccount solana.PublicKey
} }
type ammSellEvent struct { type ammSellEventPrefix struct {
Timestamp int64 Timestamp int64
BaseAmountIn uint64 BaseAmountIn uint64
MinQuoteAmountOut uint64 MinQuoteAmountOut uint64
@@ -119,6 +151,86 @@ type ammSellEvent struct {
Cashback uint64 Cashback uint64
} }
type ammSellEvent struct {
ammSellEventPrefix
BuybackFeeBasisPoints uint64
BuybackFee uint64
VirtualQuoteReserves agbinary.Int128
CanBoost bool
BaseSupply uint64
}
func decodeAmmSellEvent(data []byte) (ammSellEvent, error) {
var event ammSellEvent
decoder := agbinary.NewBorshDecoder(data)
if err := decoder.Decode(&event.ammSellEventPrefix); err != nil {
return ammSellEvent{}, err
}
if err := decodeAmmTradeEventSuffix(decoder, &event.BuybackFeeBasisPoints, &event.BuybackFee, &event.VirtualQuoteReserves, &event.CanBoost, &event.BaseSupply); err != nil {
return ammSellEvent{}, err
}
return event, nil
}
func decodeAmmTradeEventSuffix(
decoder *agbinary.Decoder,
buybackFeeBasisPoints *uint64,
buybackFee *uint64,
virtualQuoteReserves *agbinary.Int128,
canBoost *bool,
baseSupply *uint64,
) error {
remaining := decoder.Remaining()
if remaining == 0 {
return nil
}
if remaining < 16 {
return fmt.Errorf("pump amm trade event buyback suffix truncated: %d bytes", remaining)
}
var buyback ammTradeEventBuybackSuffix
if err := decoder.Decode(&buyback); err != nil {
return err
}
*buybackFeeBasisPoints = buyback.BuybackFeeBasisPoints
*buybackFee = buyback.BuybackFee
remaining = decoder.Remaining()
if remaining == 0 {
return nil
}
if remaining < 25 {
return fmt.Errorf("pump amm trade event virtual reserve suffix truncated: %d bytes", remaining)
}
var virtual ammTradeEventVirtualSuffix
if err := decoder.Decode(&virtual); err != nil {
return err
}
*virtualQuoteReserves = virtual.VirtualQuoteReserves
*canBoost = virtual.CanBoost
*baseSupply = virtual.BaseSupply
return nil
}
func pumpAmmVirtualQuoteReserve(value agbinary.Int128) decimal.Decimal {
return decimal.NewFromBigInt(value.BigInt(), 0)
}
func pumpAmmBuyPostQuoteReserves(event ammBuyEvent) (real, effective decimal.Decimal) {
// The event reserve is the pre-trade real vault balance. Keep the
// parser's existing LP-fee treatment by applying QuoteAmountIn here.
real = decimal.NewFromUint64(event.PoolQuoteTokenReserve).Add(decimal.NewFromUint64(event.QuoteAmountIn))
return real, real.Add(pumpAmmVirtualQuoteReserve(event.VirtualQuoteReserves))
}
func pumpAmmSellPostQuoteReserves(event ammSellEvent) (real, effective decimal.Decimal) {
// The event reserve is the pre-trade real vault balance. Keep the
// parser's existing LP-fee treatment by applying QuoteAmountOut here.
real = decimal.NewFromUint64(event.PoolQuoteTokenReserves).Sub(decimal.NewFromUint64(event.QuoteAmountOut))
return real, real.Add(pumpAmmVirtualQuoteReserve(event.VirtualQuoteReserves))
}
type ammWithdrawEvent struct { type ammWithdrawEvent struct {
Timestamp int64 Timestamp int64
LpTokenAmountIn uint64 LpTokenAmountIn uint64
@@ -249,6 +361,7 @@ func ammCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions Inne
QuoteAmount: decimal.NewFromUint64(createEvent.QuoteAmountIn), QuoteAmount: decimal.NewFromUint64(createEvent.QuoteAmountIn),
BaseReserve: decimal.NewFromUint64(createEvent.PoolBaseAmount), BaseReserve: decimal.NewFromUint64(createEvent.PoolBaseAmount),
QuoteReserve: decimal.NewFromUint64(createEvent.PoolQuoteAmount), QuoteReserve: decimal.NewFromUint64(createEvent.PoolQuoteAmount),
RealQuoteReserve: decimal.NewFromUint64(createEvent.PoolQuoteAmount),
UserBaseBalance: decimal.Decimal{}, UserBaseBalance: decimal.Decimal{},
UserQuoteBalance: decimal.Decimal{}, UserQuoteBalance: decimal.Decimal{},
EntryContract: entryContract, EntryContract: entryContract,
@@ -395,6 +508,7 @@ func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions Inn
QuoteAmount: decimal.NewFromUint64(quoteAmount), QuoteAmount: decimal.NewFromUint64(quoteAmount),
BaseReserve: baseReserve, BaseReserve: baseReserve,
QuoteReserve: quoteReserve, QuoteReserve: quoteReserve,
RealQuoteReserve: quoteReserve,
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
UserBaseBalance: userBase, UserBaseBalance: userBase,
UserQuoteBalance: userQuote, UserQuoteBalance: userQuote,
@@ -518,6 +632,7 @@ func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions In
QuoteAmount: decimal.NewFromUint64(quoteAmount), QuoteAmount: decimal.NewFromUint64(quoteAmount),
BaseReserve: baseReserve, BaseReserve: baseReserve,
QuoteReserve: quoteReserve, QuoteReserve: quoteReserve,
RealQuoteReserve: quoteReserve,
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
UserBaseBalance: userBase, UserBaseBalance: userBase,
UserQuoteBalance: userQuote, UserQuoteBalance: userQuote,
@@ -557,7 +672,7 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
if innerInstr.ProgramIDIndex == instruction.ProgramIDIndex && if innerInstr.ProgramIDIndex == instruction.ProgramIDIndex &&
bytes.Equal(innerInstr.Data[:8], pumpAmmEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[:8], pumpAmmEventDiscriminator[:]) &&
bytes.Equal(innerInstr.Data[8:16], pumpAmmBuyEventDiscriminator[:]) { bytes.Equal(innerInstr.Data[8:16], pumpAmmBuyEventDiscriminator[:]) {
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&event) event, err = decodeAmmBuyEvent(innerInstr.Data[16:])
if offset[1] == 0 { if offset[1] == 0 {
offset[0] += 1 offset[0] += 1
} else { } else {
@@ -632,27 +747,30 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
if event.IxName == "buy" { if event.IxName == "buy" {
quoteAmount = decimal.NewFromUint64(event.QuoteAmountIn) quoteAmount = decimal.NewFromUint64(event.QuoteAmountIn)
} }
realQuoteReserve, effectiveQuoteReserve := pumpAmmBuyPostQuoteReserves(event)
swap := Swap{ swap := Swap{
Program: SolProgramPumpAMM, Program: SolProgramPumpAMM,
Event: "buy", Event: "buy",
Pool: event.Pool, Pool: event.Pool,
BaseMint: baseMint, BaseMint: baseMint,
QuoteMint: quoteMint, QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram, BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram, QuoteTokenProgram: quoteTokenProgram,
Creator: event.CoinCreator, Creator: event.CoinCreator,
BaseMintDecimals: baseMintDecimals, BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals, QuoteMintDecimals: quoteMintDecimals,
User: eventUser, User: eventUser,
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut), BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
QuoteAmount: quoteAmount, QuoteAmount: quoteAmount,
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut), BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn), QuoteReserve: effectiveQuoteReserve,
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), RealQuoteReserve: realQuoteReserve,
Cashback: isCashbackCoin, VirtualQuoteReserve: pumpAmmVirtualQuoteReserve(event.VirtualQuoteReserves),
UserBaseBalance: userBase, Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
UserQuoteBalance: userQuote, Cashback: isCashbackCoin,
EntryContract: entryContract, UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
} }
if bytes.Equal(instruction.Data[:8], pumpAmmBuyV2Discriminator[:]) { if bytes.Equal(instruction.Data[:8], pumpAmmBuyV2Discriminator[:]) {
swap.SetSwapAmountInfo( swap.SetSwapAmountInfo(
@@ -699,7 +817,7 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
if innerInstr.ProgramIDIndex == instruction.ProgramIDIndex && if innerInstr.ProgramIDIndex == instruction.ProgramIDIndex &&
bytes.Equal(innerInstr.Data[:8], pumpAmmEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[:8], pumpAmmEventDiscriminator[:]) &&
bytes.Equal(innerInstr.Data[8:16], pumpAmmSellEventDiscriminator[:]) { bytes.Equal(innerInstr.Data[8:16], pumpAmmSellEventDiscriminator[:]) {
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&event) event, err = decodeAmmSellEvent(innerInstr.Data[16:])
if offset[1] == 0 { if offset[1] == 0 {
offset[0] += 1 offset[0] += 1
} else { } else {
@@ -770,27 +888,30 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
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
realQuoteReserve, effectiveQuoteReserve := pumpAmmSellPostQuoteReserves(event)
swap := Swap{ swap := Swap{
Program: SolProgramPumpAMM, Program: SolProgramPumpAMM,
Event: "sell", Event: "sell",
Pool: event.Pool, Pool: event.Pool,
BaseMint: baseMint, BaseMint: baseMint,
QuoteMint: quoteMint, QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram, BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram, QuoteTokenProgram: quoteTokenProgram,
Creator: event.CoinCreator, Creator: event.CoinCreator,
BaseMintDecimals: baseMintDecimals, BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals, QuoteMintDecimals: quoteMintDecimals,
User: eventUser, User: eventUser,
BaseAmount: decimal.NewFromUint64(event.BaseAmountIn), BaseAmount: decimal.NewFromUint64(event.BaseAmountIn),
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountOut), QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountOut),
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn), BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn),
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut), QuoteReserve: effectiveQuoteReserve,
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), RealQuoteReserve: realQuoteReserve,
Cashback: isCashbackCoin, VirtualQuoteReserve: pumpAmmVirtualQuoteReserve(event.VirtualQuoteReserves),
UserBaseBalance: userBase, Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
UserQuoteBalance: userQuote, Cashback: isCashbackCoin,
EntryContract: entryContract, UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
} }
swap.SetSwapAmountInfo( swap.SetSwapAmountInfo(
SwapModeExactIn, SwapModeExactIn,
@@ -893,6 +1014,7 @@ func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstru
QuoteAmount: decimal.NewFromUint64(event.QuoteAmountIn), QuoteAmount: decimal.NewFromUint64(event.QuoteAmountIn),
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn), BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn),
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves + event.QuoteAmountIn), QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves + event.QuoteAmountIn),
RealQuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves + event.QuoteAmountIn),
//Mayhem: false, //Mayhem: false,
UserBaseBalance: decimal.NewFromUint64(event.UserBaseTokenReserves - event.BaseAmountIn), UserBaseBalance: decimal.NewFromUint64(event.UserBaseTokenReserves - event.BaseAmountIn),
UserQuoteBalance: decimal.NewFromUint64(event.UserQuoteTokenReserves - event.QuoteAmountIn), UserQuoteBalance: decimal.NewFromUint64(event.UserQuoteTokenReserves - event.QuoteAmountIn),
@@ -994,6 +1116,7 @@ func withdrawParse(tx *Tx, instruction Instruction, innerInstructions InnerInstr
QuoteAmount: decimal.NewFromUint64(event.QuoteAmountOut), QuoteAmount: decimal.NewFromUint64(event.QuoteAmountOut),
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves - event.BaseAmountOut), BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves - event.BaseAmountOut),
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut), QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut),
RealQuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut),
//Mayhem: false, //Mayhem: false,
UserBaseBalance: decimal.NewFromUint64(event.UserBaseTokenReserves + event.BaseAmountOut), UserBaseBalance: decimal.NewFromUint64(event.UserBaseTokenReserves + event.BaseAmountOut),
UserQuoteBalance: decimal.NewFromUint64(event.UserQuoteTokenReserves + event.QuoteAmountOut), UserQuoteBalance: decimal.NewFromUint64(event.UserQuoteTokenReserves + event.QuoteAmountOut),

View File

@@ -1,11 +1,13 @@
package pump_parser package pump_parser
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
agbinary "github.com/gagliardetto/binary" agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go"
"github.com/mr-tron/base58"
"testing" "testing"
) )
@@ -40,3 +42,128 @@ func TestAmmBuyEvent(t *testing.T) {
fmt.Println(pumpAmmBuyEventDiscriminator) fmt.Println(pumpAmmBuyEventDiscriminator)
fmt.Println(pumpGetFeesDiscriminator) fmt.Println(pumpGetFeesDiscriminator)
} }
func TestDecodeAmmBuyEventSuffixCompatibility(t *testing.T) {
prefix := ammBuyEventPrefix{TimeStamp: 123, PoolQuoteTokenReserve: 456}
legacy, err := decodeAmmBuyEvent(encodeAmmEventParts(t, prefix))
if err != nil {
t.Fatalf("decodeAmmBuyEvent(legacy) error = %v", err)
}
if legacy.TimeStamp != prefix.TimeStamp || legacy.PoolQuoteTokenReserve != prefix.PoolQuoteTokenReserve {
t.Fatalf("legacy prefix mismatch: %+v", legacy.ammBuyEventPrefix)
}
if legacy.BuybackFeeBasisPoints != 0 || legacy.BuybackFee != 0 || legacy.CanBoost || legacy.BaseSupply != 0 || !pumpAmmVirtualQuoteReserve(legacy.VirtualQuoteReserves).IsZero() {
t.Fatalf("legacy suffix is not zero-valued: %+v", legacy)
}
buyback := ammTradeEventBuybackSuffix{BuybackFeeBasisPoints: 1000, BuybackFee: 77}
withBuyback, err := decodeAmmBuyEvent(encodeAmmEventParts(t, prefix, buyback))
if err != nil {
t.Fatalf("decodeAmmBuyEvent(buyback) error = %v", err)
}
if withBuyback.BuybackFeeBasisPoints != buyback.BuybackFeeBasisPoints || withBuyback.BuybackFee != buyback.BuybackFee {
t.Fatalf("buyback suffix mismatch: %+v", withBuyback)
}
if withBuyback.CanBoost || withBuyback.BaseSupply != 0 || !pumpAmmVirtualQuoteReserve(withBuyback.VirtualQuoteReserves).IsZero() {
t.Fatalf("pre-virtual suffix has unexpected virtual fields: %+v", withBuyback)
}
negativeVirtual := agbinary.Int128(agbinary.Uint128{Lo: ^uint64(122), Hi: ^uint64(0)})
virtual := ammTradeEventVirtualSuffix{
VirtualQuoteReserves: negativeVirtual,
CanBoost: true,
BaseSupply: 1_000_000,
}
current, err := decodeAmmBuyEvent(encodeAmmEventParts(t, prefix, buyback, virtual))
if err != nil {
t.Fatalf("decodeAmmBuyEvent(current) error = %v", err)
}
if got := pumpAmmVirtualQuoteReserve(current.VirtualQuoteReserves).String(); got != "-123" {
t.Fatalf("VirtualQuoteReserves = %s, want -123", got)
}
if !current.CanBoost || current.BaseSupply != virtual.BaseSupply {
t.Fatalf("current suffix mismatch: %+v", current)
}
}
func TestDecodeAmmSellEventSuffixCompatibility(t *testing.T) {
prefix := ammSellEventPrefix{Timestamp: 321, PoolQuoteTokenReserves: 654, QuoteAmountOut: 54}
buyback := ammTradeEventBuybackSuffix{BuybackFeeBasisPoints: 900, BuybackFee: 88}
virtual := ammTradeEventVirtualSuffix{
VirtualQuoteReserves: agbinary.Int128(agbinary.Uint128{Lo: 987}),
CanBoost: true,
BaseSupply: 2_000_000,
}
event, err := decodeAmmSellEvent(encodeAmmEventParts(t, prefix, buyback, virtual))
if err != nil {
t.Fatalf("decodeAmmSellEvent() error = %v", err)
}
if event.Timestamp != prefix.Timestamp || event.BuybackFee != buyback.BuybackFee || !event.CanBoost || event.BaseSupply != virtual.BaseSupply {
t.Fatalf("decoded sell event mismatch: %+v", event)
}
if got := pumpAmmVirtualQuoteReserve(event.VirtualQuoteReserves).String(); got != "987" {
t.Fatalf("VirtualQuoteReserves = %s, want 987", got)
}
real, effective := pumpAmmSellPostQuoteReserves(event)
if real.String() != "600" || effective.String() != "1587" {
t.Fatalf("post reserves = real %s effective %s", real, effective)
}
}
func TestDecodeAmmTradeEventRejectsTruncatedSuffix(t *testing.T) {
prefix := encodeAmmEventParts(t, ammBuyEventPrefix{})
for _, suffixLength := range []int{1, 15, 17, 40} {
data := append(append([]byte(nil), prefix...), make([]byte, suffixLength)...)
if _, err := decodeAmmBuyEvent(data); err == nil {
t.Fatalf("decodeAmmBuyEvent() error = nil for %d-byte suffix", suffixLength)
}
}
}
func TestDecodeAmmBuyEventDevnetVirtualReserve(t *testing.T) {
const eventDataBase58 = "6MF1ykxMQW5eFmo1ErWgds8V2wfEyqJPjEsoz4yRoKQCktPze5U297neF7huAQQ9DhxEJqHPwezxUh9mcQEsfdmnwW84YVxcfEzDUduRqc1zty2dLQZcFGzFeFC6fmpX75tkvuMvTN89HekHa9TEikGRuXhtWkeacjNoVqfbxXUp3FQxGSFoyeBaSdAfPMvXJ7zN4ALTDtn3a5g7HxJCPPWeUtYGcJig6RpKrC3DzAQ3x5SiiJiVhK1zdocmkkRXuTezt5L5Svrf9pbM4QkkwgvxrYqfUs5YeaLNqWYtNZfimLtENhQJcfrhQh2svCosCJ2Ux5iyyLZwJu7VLZibD41CwxuF6ngmrhde8BmEegmQ9f3L4qYhqgW5RJSiu1JnKMFqmTckuwGydQfTiyA9RsCtFxiazaAVim9SMARU2fqe9zfAPdRjBetAvTGkBxrERmmzhWVhER6g448mV1E1eeQjFWYRhtR33hSUpNUUP6dUF8CWae2BuJxaKexpiY3pyqUwbH7osvexbff8jpfpZGubP5UWjqhwijtF8tdkoEdAnb2TgWXqXMu4by5VDsEzwbgNK1DbkGbxrcXYwFTNskqoUqoNLPRaAcfnjTMA9cVme62HAj5WvxuzpL9Gpw7ZzSE2LYnVvzTa6PnN2bbpuqJpZ3d"
data, err := base58.Decode(eventDataBase58)
if err != nil {
t.Fatalf("base58.Decode() error = %v", err)
}
if len(data) < 16 || !bytes.Equal(data[:8], pumpAmmEventDiscriminator[:]) || !bytes.Equal(data[8:16], pumpAmmBuyEventDiscriminator[:]) {
t.Fatalf("unexpected event discriminator: len=%d", len(data))
}
event, err := decodeAmmBuyEvent(data[16:])
if err != nil {
t.Fatalf("decodeAmmBuyEvent(devnet) error = %v", err)
}
if event.TimeStamp != 1784271481 || event.PoolQuoteTokenReserve != 2_213_159_021 || event.QuoteAmountIn != 100_000_000 || event.QuoteAmountInWithLpFee != 98_785_184 {
t.Fatalf("devnet buy event core fields mismatch: %+v", event)
}
if event.BuybackFeeBasisPoints != 1000 || event.BuybackFee != 91_851 {
t.Fatalf("devnet buyback fields mismatch: bps=%d fee=%d", event.BuybackFeeBasisPoints, event.BuybackFee)
}
if got := pumpAmmVirtualQuoteReserve(event.VirtualQuoteReserves).String(); got != "583150126" {
t.Fatalf("VirtualQuoteReserves = %s, want 583150126", got)
}
if !event.CanBoost || event.BaseSupply != 1_000_000_000_000_000 {
t.Fatalf("devnet boost metadata mismatch: can_boost=%t base_supply=%d", event.CanBoost, event.BaseSupply)
}
real, effective := pumpAmmBuyPostQuoteReserves(event)
if real.String() != "2313159021" || effective.String() != "2896309147" {
t.Fatalf("post reserves = real %s effective %s", real, effective)
}
}
func encodeAmmEventParts(t *testing.T, parts ...any) []byte {
t.Helper()
var buf bytes.Buffer
encoder := agbinary.NewBorshEncoder(&buf)
for _, part := range parts {
if err := encoder.Encode(part); err != nil {
t.Fatalf("borsh encode %T: %v", part, err)
}
}
return buf.Bytes()
}

10
tx.go
View File

@@ -43,10 +43,12 @@ type Swap struct {
ActualLimitAmountSide SwapAmountSide ActualLimitAmountSide SwapAmountSide
SlippageBps decimal.Decimal SlippageBps decimal.Decimal
BaseReserve decimal.Decimal BaseReserve decimal.Decimal
QuoteReserve decimal.Decimal QuoteReserve decimal.Decimal
Mayhem bool RealQuoteReserve decimal.Decimal
Cashback bool VirtualQuoteReserve decimal.Decimal
Mayhem bool
Cashback bool
UserBaseBalance decimal.Decimal UserBaseBalance decimal.Decimal
UserQuoteBalance decimal.Decimal UserQuoteBalance decimal.Decimal

View File

@@ -8,6 +8,7 @@ import (
"io" "io"
"iter" "iter"
"math" "math"
"math/big"
"sort" "sort"
"strconv" "strconv"
@@ -18,7 +19,9 @@ import (
const ( const (
txBinarySchemaVersionV3 uint16 = 3 txBinarySchemaVersionV3 uint16 = 3
txBinarySchemaVersionCurrent uint16 = 4 txBinarySchemaVersionV4 uint16 = 4
txBinarySchemaVersionV5 uint16 = 5
txBinarySchemaVersionCurrent = txBinarySchemaVersionV5
txBinaryEnumVersionV1 uint16 = 1 txBinaryEnumVersionV1 uint16 = 1
txBinarySOLScale int32 = 9 txBinarySOLScale int32 = 9
@@ -102,6 +105,9 @@ type SwapBinary struct {
LpMint uint32 LpMint uint32
AfterSOLBalance float64 AfterSOLBalance float64
RealQuoteReserve uint64
VirtualQuoteReserve [16]byte
} }
type TxsBinary struct { type TxsBinary struct {
@@ -659,7 +665,7 @@ func (tx *TxBinary) ToTx() (*Tx, error) {
if len(tx.Swaps) > 0 { if len(tx.Swaps) > 0 {
out.Swaps = make([]Swap, 0, len(tx.Swaps)) out.Swaps = make([]Swap, 0, len(tx.Swaps))
for i, swap := range tx.Swaps { for i, swap := range tx.Swaps {
decodedSwap, err := swap.toSwap(tx.AddressTable, i) decodedSwap, err := swap.toSwap(tx.AddressTable, i, tx.SchemaVersion)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -797,6 +803,12 @@ func newSwapBinary(swap Swap, index int, addressIndex *txBinaryAddressIndex) (Sw
if out.QuoteReserve, err = txBinaryDecimalToFloat64Raw(swap.QuoteReserve, fmt.Sprintf("swap[%d].quote_reserve", index)); err != nil { if out.QuoteReserve, err = txBinaryDecimalToFloat64Raw(swap.QuoteReserve, fmt.Sprintf("swap[%d].quote_reserve", index)); err != nil {
return SwapBinary{}, err return SwapBinary{}, err
} }
if out.RealQuoteReserve, err = txBinaryDecimalToUint64(swap.RealQuoteReserve, fmt.Sprintf("swap[%d].real_quote_reserve", index)); err != nil {
return SwapBinary{}, err
}
if out.VirtualQuoteReserve, err = txBinaryDecimalToInt128(swap.VirtualQuoteReserve, fmt.Sprintf("swap[%d].virtual_quote_reserve", index)); err != nil {
return SwapBinary{}, err
}
if out.UserBaseBalance, err = txBinaryDecimalToUint64(swap.UserBaseBalance, fmt.Sprintf("swap[%d].user_base_balance", index)); err != nil { if out.UserBaseBalance, err = txBinaryDecimalToUint64(swap.UserBaseBalance, fmt.Sprintf("swap[%d].user_base_balance", index)); err != nil {
return SwapBinary{}, err return SwapBinary{}, err
} }
@@ -810,7 +822,7 @@ func newSwapBinary(swap Swap, index int, addressIndex *txBinaryAddressIndex) (Sw
return out, nil return out, nil
} }
func (swap SwapBinary) toSwap(addressTable []solana.PublicKey, index int) (Swap, error) { func (swap SwapBinary) toSwap(addressTable []solana.PublicKey, index int, schemaVersion uint16) (Swap, error) {
pool, err := txBinaryAddressAt(addressTable, swap.Pool, fmt.Sprintf("swap[%d].pool", index)) pool, err := txBinaryAddressAt(addressTable, swap.Pool, fmt.Sprintf("swap[%d].pool", index))
if err != nil { if err != nil {
return Swap{}, err return Swap{}, err
@@ -864,6 +876,20 @@ func (swap SwapBinary) toSwap(addressTable []solana.PublicKey, index int) (Swap,
return Swap{}, err return Swap{}, err
} }
quoteReserve := txBinaryFloat64ToDecimalRaw(swap.QuoteReserve)
realQuoteReserve := quoteReserve
virtualQuoteReserve := decimal.Zero
if schemaVersion >= txBinarySchemaVersionV5 {
realQuoteReserve = decimal.NewFromUint64(swap.RealQuoteReserve)
virtualQuoteReserve = txBinaryInt128ToDecimal(swap.VirtualQuoteReserve)
// A v3/v4 batch upgraded by the streaming merger has no component
// bytes to carry forward. Preserve its legacy meaning after the
// merged output is written as v5.
if realQuoteReserve.IsZero() && virtualQuoteReserve.IsZero() && !quoteReserve.IsZero() {
realQuoteReserve = quoteReserve
}
}
return Swap{ return Swap{
Program: swap.Program, Program: swap.Program,
Event: swap.Event, Event: swap.Event,
@@ -893,7 +919,9 @@ func (swap SwapBinary) toSwap(addressTable []solana.PublicKey, index int) (Swap,
ActualLimitAmountSide: swap.ActualLimitAmountSide, ActualLimitAmountSide: swap.ActualLimitAmountSide,
SlippageBps: decimal.NewFromUint64(swap.SlippageBps), SlippageBps: decimal.NewFromUint64(swap.SlippageBps),
BaseReserve: txBinaryFloat64ToDecimalRaw(swap.BaseReserve), BaseReserve: txBinaryFloat64ToDecimalRaw(swap.BaseReserve),
QuoteReserve: txBinaryFloat64ToDecimalRaw(swap.QuoteReserve), QuoteReserve: quoteReserve,
RealQuoteReserve: realQuoteReserve,
VirtualQuoteReserve: virtualQuoteReserve,
Mayhem: swap.Mayhem, Mayhem: swap.Mayhem,
Cashback: swap.Cashback, Cashback: swap.Cashback,
UserBaseBalance: decimal.NewFromUint64(swap.UserBaseBalance), UserBaseBalance: decimal.NewFromUint64(swap.UserBaseBalance),
@@ -1071,6 +1099,44 @@ func txBinaryDecimalToUint64(value decimal.Decimal, field string) (uint64, error
return bigInt.Uint64(), nil return bigInt.Uint64(), nil
} }
func txBinaryDecimalToInt128(value decimal.Decimal, field string) ([16]byte, error) {
var out [16]byte
if !value.Equal(value.Truncate(0)) {
return out, fmt.Errorf("%s must be an integer, got %s", field, value.String())
}
integer := value.BigInt()
limit := new(big.Int).Lsh(big.NewInt(1), 127)
minimum := new(big.Int).Neg(new(big.Int).Set(limit))
maximum := new(big.Int).Sub(new(big.Int).Set(limit), big.NewInt(1))
if integer.Cmp(minimum) < 0 || integer.Cmp(maximum) > 0 {
return out, fmt.Errorf("%s overflows int128: %s", field, value.String())
}
unsigned := new(big.Int).Set(integer)
if unsigned.Sign() < 0 {
unsigned.Add(unsigned, new(big.Int).Lsh(big.NewInt(1), 128))
}
var bigEndian [16]byte
unsigned.FillBytes(bigEndian[:])
for i := range out {
out[i] = bigEndian[len(bigEndian)-1-i]
}
return out, nil
}
func txBinaryInt128ToDecimal(raw [16]byte) decimal.Decimal {
var bigEndian [16]byte
for i := range raw {
bigEndian[i] = raw[len(raw)-1-i]
}
integer := new(big.Int).SetBytes(bigEndian[:])
if raw[len(raw)-1]&0x80 != 0 {
integer.Sub(integer, new(big.Int).Lsh(big.NewInt(1), 128))
}
return decimal.NewFromBigInt(integer, 0)
}
func txBinaryScaledDecimalToUint64(value decimal.Decimal, scale int32, field string) (uint64, error) { func txBinaryScaledDecimalToUint64(value decimal.Decimal, scale int32, field string) (uint64, error) {
return txBinaryDecimalToUint64(value.Shift(scale), field) return txBinaryDecimalToUint64(value.Shift(scale), field)
} }
@@ -1179,7 +1245,7 @@ func (enc *txBinaryEncoder) writeTxBinaryBody(tx *TxBinary, enumTable *txBinaryE
enc.writeUint32(tx.Signer) enc.writeUint32(tx.Signer)
enc.writeUint64(tx.Block) enc.writeUint64(tx.Block)
enc.writeUint64(tx.BlockIndex) enc.writeUint64(tx.BlockIndex)
if tx.SchemaVersion >= txBinarySchemaVersionCurrent { if tx.SchemaVersion >= txBinarySchemaVersionV4 {
enc.writeUint64(uint64(tx.BlockAt)) enc.writeUint64(uint64(tx.BlockAt))
} }
enc.writeBool(tx.TxHash != nil) enc.writeBool(tx.TxHash != nil)
@@ -1198,7 +1264,7 @@ func (enc *txBinaryEncoder) writeTxBinaryBody(tx *TxBinary, enumTable *txBinaryE
if err := enc.writeMevAgentEntries(tx.MevAgent, enumTable); err != nil { if err := enc.writeMevAgentEntries(tx.MevAgent, enumTable); err != nil {
return err return err
} }
if err := enc.writeSwaps(tx.Swaps, enumTable); err != nil { if err := enc.writeSwaps(tx.Swaps, enumTable, tx.SchemaVersion); err != nil {
return err return err
} }
return nil return nil
@@ -1230,7 +1296,7 @@ func (enc *txBinaryEncoder) writeMevAgentEntries(entries []MevAgentBinary, enumT
return nil return nil
} }
func (enc *txBinaryEncoder) writeSwaps(swaps []SwapBinary, enumTable *txBinaryEnumTable) error { func (enc *txBinaryEncoder) writeSwaps(swaps []SwapBinary, enumTable *txBinaryEnumTable, schemaVersion uint16) error {
enc.writeUint32(uint32(len(swaps))) enc.writeUint32(uint32(len(swaps)))
for i, swap := range swaps { for i, swap := range swaps {
programID, err := enumTable.programs.id(swap.Program) programID, err := enumTable.programs.id(swap.Program)
@@ -1280,6 +1346,10 @@ func (enc *txBinaryEncoder) writeSwaps(swaps []SwapBinary, enumTable *txBinaryEn
enc.writeUint32(swap.MigrateTopProgram) enc.writeUint32(swap.MigrateTopProgram)
enc.writeUint32(swap.LpMint) enc.writeUint32(swap.LpMint)
enc.writeFloat64(swap.AfterSOLBalance) enc.writeFloat64(swap.AfterSOLBalance)
if schemaVersion >= txBinarySchemaVersionV5 {
enc.writeUint64(swap.RealQuoteReserve)
enc.writeBytes(swap.VirtualQuoteReserve[:])
}
} }
return nil return nil
} }
@@ -1395,7 +1465,7 @@ func (dec *txBinaryDecoder) readMevAgentEntries(enumTable *txBinaryEnumTable) ([
} }
func (dec *txBinaryDecoder) readSwaps(enumTable *txBinaryEnumTable, _ []solana.PublicKey) ([]SwapBinary, error) { func (dec *txBinaryDecoder) readSwaps(enumTable *txBinaryEnumTable, _ []solana.PublicKey) ([]SwapBinary, error) {
return txBinaryReadSwaps(dec, enumTable) return txBinaryReadSwaps(dec, enumTable, txBinarySchemaVersionCurrent)
} }
func (dec *txBinaryDecoder) readTxBinaryBody(tx *TxBinary, enumTable *txBinaryEnumTable, addressTable []solana.PublicKey) error { func (dec *txBinaryDecoder) readTxBinaryBody(tx *TxBinary, enumTable *txBinaryEnumTable, addressTable []solana.PublicKey) error {
@@ -1651,7 +1721,7 @@ func txBinaryReadMevAgentEntries(dec txBinaryBodyReader, enumTable *txBinaryEnum
return out, nil return out, nil
} }
func txBinaryReadSwaps(dec txBinaryBodyReader, enumTable *txBinaryEnumTable) ([]SwapBinary, error) { func txBinaryReadSwaps(dec txBinaryBodyReader, enumTable *txBinaryEnumTable, schemaVersion uint16) ([]SwapBinary, error) {
count, err := dec.readUint32() count, err := dec.readUint32()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1798,6 +1868,16 @@ func txBinaryReadSwaps(dec txBinaryBodyReader, enumTable *txBinaryEnumTable) ([]
if swap.AfterSOLBalance, err = dec.readFloat64(); err != nil { if swap.AfterSOLBalance, err = dec.readFloat64(); err != nil {
return nil, err return nil, err
} }
if schemaVersion >= txBinarySchemaVersionV5 {
if swap.RealQuoteReserve, err = dec.readUint64(); err != nil {
return nil, err
}
rawVirtualQuoteReserve, err := dec.readN(len(swap.VirtualQuoteReserve))
if err != nil {
return nil, err
}
copy(swap.VirtualQuoteReserve[:], rawVirtualQuoteReserve)
}
out = append(out, swap) out = append(out, swap)
} }
return out, nil return out, nil
@@ -1815,7 +1895,7 @@ func txBinaryReadTxBody(dec txBinaryBodyReader, tx *TxBinary, enumTable *txBinar
if tx.BlockIndex, err = dec.readUint64(); err != nil { if tx.BlockIndex, err = dec.readUint64(); err != nil {
return err return err
} }
if tx.SchemaVersion >= txBinarySchemaVersionCurrent { if tx.SchemaVersion >= txBinarySchemaVersionV4 {
blockAt, err := dec.readUint64() blockAt, err := dec.readUint64()
if err != nil { if err != nil {
return err return err
@@ -1863,7 +1943,7 @@ func txBinaryReadTxBody(dec txBinaryBodyReader, tx *TxBinary, enumTable *txBinar
if tx.MevAgent, err = txBinaryReadMevAgentEntries(dec, enumTable); err != nil { if tx.MevAgent, err = txBinaryReadMevAgentEntries(dec, enumTable); err != nil {
return err return err
} }
if tx.Swaps, err = txBinaryReadSwaps(dec, enumTable); err != nil { if tx.Swaps, err = txBinaryReadSwaps(dec, enumTable, tx.SchemaVersion); err != nil {
return err return err
} }
return nil return nil

View File

@@ -82,6 +82,8 @@ func TestTxBinaryRoundTrip(t *testing.T) {
SlippageBps: decimal.RequireFromString("833.3333"), SlippageBps: decimal.RequireFromString("833.3333"),
BaseReserve: decimal.NewFromInt(5555), BaseReserve: decimal.NewFromInt(5555),
QuoteReserve: decimal.NewFromInt(9999), QuoteReserve: decimal.NewFromInt(9999),
RealQuoteReserve: decimal.NewFromInt(10122),
VirtualQuoteReserve: decimal.NewFromInt(-123),
Mayhem: true, Mayhem: true,
Cashback: false, Cashback: false,
UserBaseBalance: decimal.NewFromInt(777), UserBaseBalance: decimal.NewFromInt(777),
@@ -202,6 +204,12 @@ func TestTxBinaryRoundTrip(t *testing.T) {
if !swap.QuoteReserve.Equal(original.Swaps[0].QuoteReserve) { if !swap.QuoteReserve.Equal(original.Swaps[0].QuoteReserve) {
t.Fatalf("swap.QuoteReserve = %s, want %s", swap.QuoteReserve, original.Swaps[0].QuoteReserve) t.Fatalf("swap.QuoteReserve = %s, want %s", swap.QuoteReserve, original.Swaps[0].QuoteReserve)
} }
if !swap.RealQuoteReserve.Equal(original.Swaps[0].RealQuoteReserve) {
t.Fatalf("swap.RealQuoteReserve = %s, want %s", swap.RealQuoteReserve, original.Swaps[0].RealQuoteReserve)
}
if !swap.VirtualQuoteReserve.Equal(original.Swaps[0].VirtualQuoteReserve) {
t.Fatalf("swap.VirtualQuoteReserve = %s, want %s", swap.VirtualQuoteReserve, original.Swaps[0].VirtualQuoteReserve)
}
if !swap.UserBaseBalance.Equal(original.Swaps[0].UserBaseBalance) { if !swap.UserBaseBalance.Equal(original.Swaps[0].UserBaseBalance) {
t.Fatalf("swap.UserBaseBalance = %s, want %s", swap.UserBaseBalance, original.Swaps[0].UserBaseBalance) t.Fatalf("swap.UserBaseBalance = %s, want %s", swap.UserBaseBalance, original.Swaps[0].UserBaseBalance)
} }
@@ -1017,6 +1025,89 @@ func TestTxBinaryDecodeSchemaV3LeavesBlockAtZero(t *testing.T) {
} }
} }
func TestTxBinaryDecodeSchemaV4DefaultsQuoteReserveComponents(t *testing.T) {
original := &Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 42,
BlockIndex: 2,
BlockAt: 1710000042,
Swaps: []Swap{
{
Program: SolProgramPumpAMM,
Event: TxEventBuy,
QuoteReserve: decimal.NewFromInt(123456789),
RealQuoteReserve: decimal.NewFromInt(123000000),
VirtualQuoteReserve: decimal.NewFromInt(456789),
},
},
}
binaryTx, err := NewTxBinary(original)
if err != nil {
t.Fatalf("NewTxBinary() error = %v", err)
}
binaryTx.SchemaVersion = txBinarySchemaVersionV4
encoded, err := binaryTx.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary(v4) error = %v", err)
}
decoded, err := DecodeTxBinary(encoded)
if err != nil {
t.Fatalf("DecodeTxBinary(v4) error = %v", err)
}
if decoded.BlockAt != original.BlockAt {
t.Fatalf("BlockAt = %d, want %d", decoded.BlockAt, original.BlockAt)
}
if len(decoded.Swaps) != 1 {
t.Fatalf("Swaps len = %d, want 1", len(decoded.Swaps))
}
swap := decoded.Swaps[0]
if !swap.QuoteReserve.Equal(original.Swaps[0].QuoteReserve) {
t.Fatalf("QuoteReserve = %s, want %s", swap.QuoteReserve, original.Swaps[0].QuoteReserve)
}
if !swap.RealQuoteReserve.Equal(swap.QuoteReserve) {
t.Fatalf("RealQuoteReserve = %s, want legacy QuoteReserve %s", swap.RealQuoteReserve, swap.QuoteReserve)
}
if !swap.VirtualQuoteReserve.IsZero() {
t.Fatalf("VirtualQuoteReserve = %s, want 0", swap.VirtualQuoteReserve)
}
}
func TestTxBinarySignedInt128RoundTrip(t *testing.T) {
values := []string{
"-170141183460469231731687303715884105728",
"-123",
"-1",
"0",
"1",
"123",
"170141183460469231731687303715884105727",
}
for _, value := range values {
t.Run(value, func(t *testing.T) {
want := decimal.RequireFromString(value)
raw, err := txBinaryDecimalToInt128(want, "value")
if err != nil {
t.Fatalf("txBinaryDecimalToInt128() error = %v", err)
}
if got := txBinaryInt128ToDecimal(raw); !got.Equal(want) {
t.Fatalf("round trip = %s, want %s", got, want)
}
})
}
invalid := []string{
"-170141183460469231731687303715884105729",
"170141183460469231731687303715884105728",
"1.5",
}
for _, value := range invalid {
if _, err := txBinaryDecimalToInt128(decimal.RequireFromString(value), "value"); err == nil {
t.Fatalf("txBinaryDecimalToInt128(%s) error = nil", value)
}
}
}
func TestMergeTxsBinaryBytesUpgradesSchemaV3AndPreservesV4BlockAt(t *testing.T) { func TestMergeTxsBinaryBytesUpgradesSchemaV3AndPreservesV4BlockAt(t *testing.T) {
legacyTx := Tx{ legacyTx := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"), Signer: mustPubKey("So11111111111111111111111111111111111111112"),
@@ -1029,11 +1120,18 @@ func TestMergeTxsBinaryBytesUpgradesSchemaV3AndPreservesV4BlockAt(t *testing.T)
Block: 52, Block: 52,
BlockIndex: 2, BlockIndex: 2,
BlockAt: 1710000052, BlockAt: 1710000052,
Swaps: []Swap{
{
Program: SolProgramPumpAMM,
Event: TxEventBuy,
QuoteReserve: decimal.NewFromInt(222),
},
},
} }
merged, err := MergeTxsBinaryBytes([][]byte{ merged, err := MergeTxsBinaryBytes([][]byte{
mustEncodeTxsBinaryV3(t, []Tx{legacyTx}), mustEncodeTxsBinaryV3(t, []Tx{legacyTx}),
mustEncodeTxsBinary(t, []Tx{currentTx}), mustEncodeTxsBinaryV4(t, []Tx{currentTx}),
}) })
if err != nil { if err != nil {
t.Fatalf("MergeTxsBinaryBytes(v3,v4) error = %v", err) t.Fatalf("MergeTxsBinaryBytes(v3,v4) error = %v", err)
@@ -1060,6 +1158,9 @@ func TestMergeTxsBinaryBytesUpgradesSchemaV3AndPreservesV4BlockAt(t *testing.T)
if decoded[1].BlockAt != currentTx.BlockAt { if decoded[1].BlockAt != currentTx.BlockAt {
t.Fatalf("current BlockAt = %d, want %d", decoded[1].BlockAt, currentTx.BlockAt) t.Fatalf("current BlockAt = %d, want %d", decoded[1].BlockAt, currentTx.BlockAt)
} }
if len(decoded[1].Swaps) != 1 || !decoded[1].Swaps[0].RealQuoteReserve.Equal(currentTx.Swaps[0].QuoteReserve) || !decoded[1].Swaps[0].VirtualQuoteReserve.IsZero() {
t.Fatalf("v4 quote reserve components were not preserved: %+v", decoded[1].Swaps)
}
} }
func mustPubKey(value string) solana.PublicKey { func mustPubKey(value string) solana.PublicKey {
@@ -1109,6 +1210,24 @@ func mustEncodeTxsBinaryV3(t *testing.T, txs []Tx) []byte {
return encoded return encoded
} }
func mustEncodeTxsBinaryV4(t *testing.T, txs []Tx) []byte {
t.Helper()
binaryTxs, err := NewTxsBinary(txs)
if err != nil {
t.Fatalf("NewTxsBinary() error = %v", err)
}
binaryTxs.SchemaVersion = txBinarySchemaVersionV4
for i := range binaryTxs.Txs {
binaryTxs.Txs[i].SchemaVersion = txBinarySchemaVersionV4
}
encoded, err := binaryTxs.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary(v4) error = %v", err)
}
return encoded
}
func mustTxBinary(t *testing.T, data []byte) *TxsBinary { func mustTxBinary(t *testing.T, data []byte) *TxsBinary {
t.Helper() t.Helper()