Compare commits

...

5 Commits

Author SHA1 Message Date
thloyi
895230aaac update amm virtual quotes reserves 2026-07-17 17:02:34 +08:00
cachalots
387d6ee10b tx mev agent 2026-07-09 15:42:30 +08:00
cachalots
2fc2c2fb90 tx mev agent 2026-07-09 15:40:32 +08:00
cachalots
4facb86e0a go mod 2026-06-11 17:40:12 +08:00
cachalots
cc03798ff6 bloxroute tip 2026-06-11 17:22:43 +08:00
9 changed files with 579 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

@@ -116,6 +116,20 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("HZTmLyC683y74TW3HtGbNX5orxjm2sPuZBEYwwSgAM8v"): MevAgentBlocxRoute, solana.MustPublicKeyFromBase58("HZTmLyC683y74TW3HtGbNX5orxjm2sPuZBEYwwSgAM8v"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("FogxVNs6Mm2w9rnGL1vkARSwJxvLE8mujTv3LK8RnUhF"): MevAgentBlocxRoute, solana.MustPublicKeyFromBase58("FogxVNs6Mm2w9rnGL1vkARSwJxvLE8mujTv3LK8RnUhF"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("3UQUKjhMKaY2S6bjcQD6yHB7utcZt5bfarRCmctpRtUd"): MevAgentBlocxRoute, solana.MustPublicKeyFromBase58("3UQUKjhMKaY2S6bjcQD6yHB7utcZt5bfarRCmctpRtUd"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLx7XBqSg3LUPVf1bRgCnkJmgVZR8QEgDJBPqcRLHvp"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLx8KeZxinPwy6kkUgyzMLeqb2ARNsWjADG1dhSsVba"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxADBknoNj8WAGw2W6GBYeq848Xx6ajhaymV1YvrHm"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxAc88vRBwvcUQJEgcxNfBLvHPikY4csNsUmPeWea2"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxQ88oCiTsL8Xj4YWekKi1hjrgmbE3J3FFZ2xZHR3h"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxS7NoLuynNRJ4mCnEE2YbtwJFttYsEyp2ME7rp2yt"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxW6mCov7VEbrKc3S9tcBRcfSzRnLCbNp3Dfn3SJG5"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxXSGXs4mYPTC5okZXed1qzvjNwNJ48QJ82hT2V7w7"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxYi3vojbbB7hVzVDVTdBLVPhp7GJ3ZB3BwdK5sFXi"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxhLPgBXtUpX4b1bH3HatuMGMSKT9GnwtuCGiMSAqe"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxpY1mniuFW4PgkNA4JiNxoeKHFszryi6tNgyZAiAA"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxuETxd2tgWxBALNwPzAfHhsik4BzD3nrEBCiPNZQD"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxuL2gK5FW7xfahvwLrxLyW76vcCpNsKQY2CmnE6kV"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("bLxv4Hnub7nDJWHs8s17o9bGU65Bnx6Yqp2fqtMgHmm"): MevAgentBlocxRoute,
solana.MustPublicKeyFromBase58("TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq"): MevAgentNozomi, solana.MustPublicKeyFromBase58("TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4"): MevAgentNozomi, solana.MustPublicKeyFromBase58("noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE"): MevAgentNozomi, solana.MustPublicKeyFromBase58("noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE"): MevAgentNozomi,
@@ -226,7 +240,38 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("Astran35aiQUF57XZsmkWMtNCtXGLzs8upfiqXxth2bz"): MevAgentAstralane, solana.MustPublicKeyFromBase58("Astran35aiQUF57XZsmkWMtNCtXGLzs8upfiqXxth2bz"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AStRAnpi6kFrKypragExgeRoJ1QnKH7pbSjLAKQVWUum"): MevAgentAstralane, solana.MustPublicKeyFromBase58("AStRAnpi6kFrKypragExgeRoJ1QnKH7pbSjLAKQVWUum"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("ASTRaoF93eYt73TYvwtsv6fMWHWbGmMUZfVZPo3CRU9C"): MevAgentAstralane, solana.MustPublicKeyFromBase58("ASTRaoF93eYt73TYvwtsv6fMWHWbGmMUZfVZPo3CRU9C"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("ASTZHptaMgYVMX6DAocDr1vVXLran5PpfKfQtVTSWkfE"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AStZiY6EE532nQBBogmvcWemc2bwg2kHuR4Jrd5Cqaq5"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AStzprSDj14m1pdMuuomjLQwUL5X4d4yEKwiFfzfUSeC"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("ASTzWqJ1irGmrWZAkc1UUfVYbJknsxh7R3Jo4jYZ7Mbd"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("Gu2UGEfze3Gg5cHuEC4jGbyCufgpev75RkVvBdKKtf12"): MevAgentNozomi, solana.MustPublicKeyFromBase58("Gu2UGEfze3Gg5cHuEC4jGbyCufgpev75RkVvBdKKtf12"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("ByruakBXG9y6Bwv85YiUhwZnwYLWcYWfHVZJAXjCdcb5"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("15qWd4huAkoxvhDsHMfpUn27TW1YBYMMJJ2jkAkbeam"): MevAgentSolami,
solana.MustPublicKeyFromBase58("9XuGciSwr5wb7dLTQm91JhuBTvj3GG8WjuRDc3obeam"): MevAgentSolami,
solana.MustPublicKeyFromBase58("kiQioJNyFG7pU36ELLsRKXkeT48kFbk3b6rSgrWbeam"): MevAgentSolami,
solana.MustPublicKeyFromBase58("kjmVhW1UzJrW2sU5bY5NtZ79jpvjSStsj37Pzmabeam"): MevAgentSolami,
solana.MustPublicKeyFromBase58("kREnjPWFpt4AHeY5pijPmyXaCrMnbatUQJo7d3Xbeam"): MevAgentSolami,
solana.MustPublicKeyFromBase58("praRZG6N6MdbsT4EFpKgZJWReZGXQhAMFcH68oCbeam"): MevAgentSolami,
solana.MustPublicKeyFromBase58("SqoKQKU5uwBxovq3R7yEBxFwptc4z7vwoghU3M9beam"): MevAgentSolami,
solana.MustPublicKeyFromBase58("sV72TY66T1RfmDSeHPPbwX6wwJ3bBv5hd4ehJ8tbeam"): MevAgentSolami,
solana.MustPublicKeyFromBase58("swf8MyEeLo7gtRUo27UuJj6naCASUrypU7dbteSbeam"): MevAgentSolami,
solana.MustPublicKeyFromBase58("uiuaQsxA47JybQAVN4FTfYuoEDkMiXV1r591Aewbeam"): MevAgentSolami,
solana.MustPublicKeyFromBase58("LandX1EMgYtrfi4jVJ7ryby4mDpZcef6sVH9b9U1cog"): MevAgentLandX,
solana.MustPublicKeyFromBase58("LandX2gHE7y6RejuvoLJK4hJiKkrAwcrJJ34u19Gmo1"): MevAgentLandX,
solana.MustPublicKeyFromBase58("LandX3uK1cSUF7hbefKo3oL1j4FhvhxitiMRKut99Z6"): MevAgentLandX,
solana.MustPublicKeyFromBase58("LandX4wXzvDyWfq1XXn4StRjwPjzqb7je2kqDGLoT9L"): MevAgentLandX,
solana.MustPublicKeyFromBase58("LandX5T4jfRASrcn76owxHM4q9bduNGj24ev4Ufc5fP"): MevAgentLandX,
solana.MustPublicKeyFromBase58("LandX6RsjjnfaxfYAFTZX1TW9Cgfe9HFStj9nRDd5SK"): MevAgentLandX,
solana.MustPublicKeyFromBase58("LandX7LsdzXDAbbyBjAq5FxLTnGQaU3LojGQMLZCvkt"): MevAgentLandX,
solana.MustPublicKeyFromBase58("LandX8Gv3UkhjmgKdSEg4F7yG7Z2tiqhNDYDtxazydN"): MevAgentLandX,
solana.MustPublicKeyFromBase58("LandX9fhNN5S7fuBgekKBYeYvK1Sy9hbGvDmHsBGYFh"): MevAgentLandX,
solana.MustPublicKeyFromBase58("LandXXxDXaSS8MjrqG9nri51htrfM6V4R3zXDVCeV8R"): MevAgentLandX,
solana.MustPublicKeyFromBase58("GLaiv4GMRYQmthatDS98uQT4HoucgxWT8NeJz6oSwxeU"): MevAgentGlaive,
solana.MustPublicKeyFromBase58("GLaivL5uPrDpvd1wTtvat38KGqb5WLhEdqQfnmNd3oNr"): MevAgentGlaive,
solana.MustPublicKeyFromBase58("GLaivinAWh21NaJMhtExtD5G2gZs1xnvaYVZmwqobWZL"): MevAgentGlaive,
solana.MustPublicKeyFromBase58("GLaivJSUL71FcocYa8tks5vpVyYzvaDMHtyrzfQF2ABr"): MevAgentGlaive,
solana.MustPublicKeyFromBase58("GLaivRU6eDKrta3p3psFAWPEFLzCjeMHGpPUuQqTjtyv"): MevAgentGlaive,
solana.MustPublicKeyFromBase58("GLaivq5dU8qHayz9Qf13LjPfVy3SmUhbmickfGiZdmfh"): MevAgentGlaive,
solana.MustPublicKeyFromBase58("E8wD3SMD1trozPrvSN9F6SyuUXD7rrFDuR3WexGziKG5"): MevAgent0slot, solana.MustPublicKeyFromBase58("E8wD3SMD1trozPrvSN9F6SyuUXD7rrFDuR3WexGziKG5"): MevAgent0slot,
solana.MustPublicKeyFromBase58("18hCV7f9CPmZRAH3QCNZaGHhHeNSfisQKeKuFkQsPLY"): MevAgent0slot, solana.MustPublicKeyFromBase58("18hCV7f9CPmZRAH3QCNZaGHhHeNSfisQKeKuFkQsPLY"): MevAgent0slot,
solana.MustPublicKeyFromBase58("2sYKRWBNVY6UomMBi4juoMrrL98bqizDMn98cJ3cBmye"): MevAgent0slot, solana.MustPublicKeyFromBase58("2sYKRWBNVY6UomMBi4juoMrrL98bqizDMn98cJ3cBmye"): MevAgent0slot,
@@ -343,7 +388,10 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("bgDETv6tnt9mwYqAKebLXY5B5o6akiKJmAdU7Gd9G7H"): MevAgentNozomi, solana.MustPublicKeyFromBase58("bgDETv6tnt9mwYqAKebLXY5B5o6akiKJmAdU7Gd9G7H"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("bgH7YhymSykyvMa3nAZpzvrn73owJHU5iB75S1aiLT9"): MevAgentNozomi, solana.MustPublicKeyFromBase58("bgH7YhymSykyvMa3nAZpzvrn73owJHU5iB75S1aiLT9"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfngGVVQLiVRFbLWw3Ektiv17ef9NiRZbcgdAhh4ZEW"): MevAgentNozomi, solana.MustPublicKeyFromBase58("pfngGVVQLiVRFbLWw3Ektiv17ef9NiRZbcgdAhh4ZEW"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("mwGELGMgGGrNL1UibNCQeJHDE7qdPptWRYB6noUHmTj"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("nEFs3jph8HJt7honu3k7XtGUufMnwAvSXmXcKSPxryP"): MevAgentNozomi, solana.MustPublicKeyFromBase58("nEFs3jph8HJt7honu3k7XtGUufMnwAvSXmXcKSPxryP"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("nzUMeyyucuHSCZLw1aaX14d1si2mffT4PjVNdZcybot"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("n6Tc3GoTheA1pYF4MPV57KGmn3DHJCCuk3wHZmcUypp"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("Fa1con11xLjPddfzRwRUB16sbFZggp2JeJkCeWREyR8X"): MevagentFa1con, solana.MustPublicKeyFromBase58("Fa1con11xLjPddfzRwRUB16sbFZggp2JeJkCeWREyR8X"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con11TM1RuAQzbQzYjTy4Ekfap9Lnc9fnEbQYEd6Q"): MevagentFa1con, solana.MustPublicKeyFromBase58("Fa1con11TM1RuAQzbQzYjTy4Ekfap9Lnc9fnEbQYEd6Q"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con113Bvi76nS5AzUiRDC2fqjfzkNMUNRLgQybMYt"): MevagentFa1con, solana.MustPublicKeyFromBase58("Fa1con113Bvi76nS5AzUiRDC2fqjfzkNMUNRLgQybMYt"): MevagentFa1con,

View File

@@ -23,6 +23,9 @@ const (
MevAgentRaiden = "raiden" MevAgentRaiden = "raiden"
MevAgentZan = "zan" MevAgentZan = "zan"
MevAgentTunneling = "tunneling" MevAgentTunneling = "tunneling"
MevAgentGlaive = "glaive"
MevAgentLandX = "landx"
MevAgentSolami = "solami"
) )
const ( const (

View File

@@ -272,6 +272,8 @@ func (tx *Tx) Parser() error {
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
@@ -301,6 +305,8 @@ type reserveSnapshot struct {
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,6 +747,7 @@ 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",
@@ -647,7 +763,9 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
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,
RealQuoteReserve: realQuoteReserve,
VirtualQuoteReserve: pumpAmmVirtualQuoteReserve(event.VirtualQuoteReserves),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
Cashback: isCashbackCoin, Cashback: isCashbackCoin,
UserBaseBalance: userBase, UserBaseBalance: userBase,
@@ -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,6 +888,7 @@ 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",
@@ -785,7 +904,9 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
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,
RealQuoteReserve: realQuoteReserve,
VirtualQuoteReserve: pumpAmmVirtualQuoteReserve(event.VirtualQuoteReserves),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
Cashback: isCashbackCoin, Cashback: isCashbackCoin,
UserBaseBalance: userBase, UserBaseBalance: userBase,
@@ -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()
}

2
tx.go
View File

@@ -45,6 +45,8 @@ type Swap struct {
BaseReserve decimal.Decimal BaseReserve decimal.Decimal
QuoteReserve decimal.Decimal QuoteReserve decimal.Decimal
RealQuoteReserve decimal.Decimal
VirtualQuoteReserve decimal.Decimal
Mayhem bool Mayhem bool
Cashback bool Cashback bool

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()