dflow parser

This commit is contained in:
thloyi
2026-01-07 18:02:45 +08:00
parent d9bc106eb1
commit b82b7d9b0e
4 changed files with 334 additions and 6 deletions

323
pkg/shreder/dflow.go Normal file
View File

@@ -0,0 +1,323 @@
package shreder
import (
"bytes"
"encoding/binary"
"fmt"
bin "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
var (
dflowProgramID = solana.MustPublicKeyFromBase58("DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH")
dflowSwapDisc = []byte{248, 198, 158, 145, 225, 117, 135, 200}
dflowSwap2Disc = []byte{65, 75, 63, 76, 235, 91, 91, 136}
dflowSwapWithDestinationDisc = []byte{168, 172, 24, 77, 197, 156, 135, 101}
dflowSwapWithDestinationNative = []byte{205, 77, 127, 108, 241, 32, 196, 195}
dflowSwap2WithDestinationDisc = []byte{95, 123, 213, 246, 122, 1, 86, 231}
dflowSwap2WithDestinationNative = []byte{222, 100, 184, 146, 186, 196, 105, 165}
wrappedSOL = solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112")
)
// Action enum tags (0-based, per dflow_idl Action variants)
const (
ActWhirlpoolsSwap uint8 = iota
ActClearpoolsSwap
ActRaydiumAmmSwap
ActLifinityV2Swap
ActMeteoraDlmmSwap
ActRaydiumClmmSwap
ActRaydiumClmmSwapV2
ActPhoenixSwap
ActPumpFunBuy
ActPumpFunSell
ActGammaSwap
ActObricV2Swap
ActPumpFunAmmBuy
ActPumpFunAmmSell
ActSolFiSwap
ActRubiconSwap
ActMeteoraDammV1Swap
ActRaydiumCpSwap
ActStabbleStableSwap
ActTesseraVSwap
ActMeteoraDammV2Swap
ActRaydiumLaunchlabSwap
ActMeteoraDbcSwap
ActHumidiFiSwap
ActWhirlpoolsSwapV2
ActMeteoraDlmmSwapV2
ActZeroFiSwap
ActAlphaQSwap
ActTokenSwap
ActSolFiV2Swap
ActMozartSwap
ActDFlowDynamicRouteV1
ActHeavenSwap
ActNexusSwap
ActSarosDlmmSwap
ActTransferFee
ActTransferFeeWithMint
ActRecordId
ActRecordId2
ActManifestSwap
ActBisonFiSwap
ActSanctumInfinitySwap
ActSanctumInfinityLiquidity
ActOpenPredictionsOrder
ActScorchSwap
ActIncludeAccount
)
// DynamicRouteV1CandidateAction tags
const (
drv1SolFi uint8 = iota
drv1Rubicon
drv1TesseraV
drv1HumidiFi
drv1SolFiV2
drv1Mozart
drv1ObricV2
drv1Nexus
)
// PumpFunAmmSellOptions { amount: u64, orchestrator_flags: OrchestratorFlags{flags u8} }
type pumpFunAmm struct {
Amount uint64
Flags uint8
}
type dflowAction struct {
Tag uint8
Pump *pumpFunAmm
}
type dflowSwapParams struct {
Actions []dflowAction
}
// bytes to skip for Action variants before/after PumpFunAmmSell; only PumpFunAmmSell is decoded.
func skipDflowAction(dec *bin.Decoder, tag uint8) (*pumpFunAmm, error) {
switch tag {
case ActWhirlpoolsSwap, ActClearpoolsSwap, ActWhirlpoolsSwapV2:
// amount u64 + bool + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1)
case ActRaydiumAmmSwap, ActLifinityV2Swap, ActPumpFunBuy, ActPumpFunSell, ActObricV2Swap,
ActSolFiSwap, ActRubiconSwap, ActMeteoraDammV1Swap, ActRaydiumCpSwap,
ActStabbleStableSwap, ActTesseraVSwap, ActMeteoraDammV2Swap, ActRaydiumLaunchlabSwap,
ActZeroFiSwap, ActAlphaQSwap, ActTokenSwap, ActSolFiV2Swap, ActMozartSwap, ActHeavenSwap,
ActNexusSwap, ActSarosDlmmSwap, ActManifestSwap, ActBisonFiSwap:
// amount u64 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1)
case ActMeteoraDlmmSwap, ActRaydiumClmmSwap, ActRaydiumClmmSwapV2, ActMeteoraDlmmSwapV2:
// amount u64 + u8 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1)
case ActPhoenixSwap:
// amount u64 + side u8 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1)
case ActGammaSwap:
// amount u64 + endorsed bool + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1)
case ActPumpFunAmmSell, ActPumpFunAmmBuy:
amt, err := dec.ReadUint64(binary.LittleEndian)
if err != nil {
return nil, err
}
flg, err := dec.ReadUint8()
if err != nil {
return nil, err
}
return &pumpFunAmm{Amount: amt, Flags: flg}, nil
case ActMeteoraDbcSwap:
// amount u64 + is_rate_limiter_applied bool + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1)
case ActHumidiFiSwap:
// amount u64 + swap_id u64 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 8 + 1)
case ActDFlowDynamicRouteV1:
// candidate_actions Vec<DynamicRouteV1CandidateAction> + amount u64 + orchestrator_flags u8
ln, err := dec.ReadUint32(binary.LittleEndian)
if err != nil {
return nil, err
}
for j := uint32(0); j < ln; j++ {
t, err := dec.ReadUint8()
if err != nil {
return nil, err
}
if t == drv1HumidiFi {
if err := dec.SkipBytes(8); err != nil {
return nil, err
}
}
// other variants carry no payload
}
if err := dec.SkipBytes(8); err != nil { // amount
return nil, err
}
return nil, dec.SkipBytes(1) // orchestrator_flags
case ActTransferFee, ActTransferFeeWithMint:
return nil, dec.SkipBytes(8)
case ActRecordId:
return nil, dec.SkipBytes(76)
case ActRecordId2:
return nil, dec.SkipBytes(4)
case ActSanctumInfinitySwap:
// amount u64 + src_lst_value_calc_accs u8 + dst_lst_value_calc_accs u8 + src_lst_index u32 + dst_lst_index u32 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1 + 4 + 4 + 1)
case ActSanctumInfinityLiquidity:
// amount u64 + lst_value_calc_accs u8 + lst_index u32 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 4 + 1)
case ActOpenPredictionsOrder:
// nonce u64 + order_outcome u8 + quoted_out_amount u64 + slippage_bps u16 + platform_fee_recipient_vault pubkey(32) + platform_fee_scale u16
return nil, dec.SkipBytes(8 + 1 + 8 + 2 + 32 + 2)
case ActScorchSwap:
// amount u64 + id u128 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 16 + 1)
case ActIncludeAccount:
return nil, nil
default:
return nil, fmt.Errorf("unsupported action tag %d", tag)
}
}
// SwapParams: actions Vec<Action>, quoted_out_amount u64, slippage_bps u16, platform_fee_bps u16
func decodeSwapParams(data []byte) (*dflowSwapParams, error) {
dec := bin.NewBorshDecoder(data)
out := &dflowSwapParams{}
ln, err := dec.ReadUint32(binary.LittleEndian)
if err != nil {
return nil, err
}
out.Actions = make([]dflowAction, 0, ln)
for i := uint32(0); i < ln; i++ {
tag, err := dec.ReadUint8()
if err != nil {
return nil, fmt.Errorf("actions[%d] tag: %w", i, err)
}
pump, err := skipDflowAction(dec, tag)
if err != nil {
return nil, fmt.Errorf("actions[%d]: %w", i, err)
}
out.Actions = append(out.Actions, dflowAction{Tag: tag, Pump: pump})
}
return out, nil
}
// Swap2Params: actions Vec<Action>, quoted_out_amount u64, slippage_bps u16, platform_fee_bps u16, positive_slippage_fee_limit_pct u8
func decodeSwap2Params(data []byte) (*dflowSwapParams, error) {
dec := bin.NewBorshDecoder(data)
out := &dflowSwapParams{}
ln, err := dec.ReadUint32(binary.LittleEndian)
if err != nil {
return nil, err
}
out.Actions = make([]dflowAction, 0, ln)
for i := uint32(0); i < ln; i++ {
tag, err := dec.ReadUint8()
if err != nil {
return nil, fmt.Errorf("actions[%d] tag: %w", i, err)
}
pump, err := skipDflowAction(dec, tag)
if err != nil {
return nil, fmt.Errorf("actions[%d]: %w", i, err)
}
out.Actions = append(out.Actions, dflowAction{Tag: tag, Pump: pump})
}
return out, nil
}
func parseDFlowInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) {
msg := tx.Message
if instructionIndex >= len(msg.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
ix := msg.Instructions[instructionIndex]
if len(ix.Data) < 8 {
return nil, nil
}
var err error
disc := ix.Data[:8]
payload := ix.Data[8:]
var params *dflowSwapParams
switch {
case bytes.Equal(disc, dflowSwapDisc), bytes.Equal(disc, dflowSwapWithDestinationDisc), bytes.Equal(disc, dflowSwapWithDestinationNative):
params, err = decodeSwapParams(payload)
case bytes.Equal(disc, dflowSwap2Disc), bytes.Equal(disc, dflowSwap2WithDestinationDisc), bytes.Equal(disc, dflowSwap2WithDestinationNative):
params, err = decodeSwap2Params(payload)
default:
return nil, nil
}
if err != nil {
return nil, err
}
if params == nil {
return nil, nil
}
var pump *pumpFunAmm
for _, act := range params.Actions {
if act.Tag == ActPumpFunAmmSell && act.Pump != nil {
pump = act.Pump
break
}
}
if pump == nil {
return nil, nil // only care about PumpFunAmmSell
}
// Require WSOL pair when destination mint is provided.
var (
srcIdx uint8
)
for i, acctIdx := range ix.Accounts {
if i < 6 {
continue
}
key, err := getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx))
if err != nil {
return nil, err
}
if key.Equals(pumpAmmProgramID) {
srcIdx = uint8(i + 4)
break
}
}
if srcIdx == 0 || srcIdx+1 >= uint8(len(ix.Accounts)) {
return nil, nil
}
baseMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(ix.Accounts[srcIdx]))
if err != nil {
return nil, err
}
quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(ix.Accounts[srcIdx+1]))
if err != nil {
return nil, err
}
if !quoteMint.Equals(solana.WrappedSol) {
return nil, nil
}
// Build TxSignal
sig := &TxSignal{
Label: "dflow",
TxHash: tx.Signatures[0].String(),
Maker: tx.Message.StaticAccountKeys[0].String(),
Program: "PumpAMM",
Event: "sell",
Token0Address: baseMint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(pump.Amount),
Token1Amount: decimal.Zero,
Token0AmountUint64: uint64(pump.Amount),
Token1AmountUint64: 0,
}
return sig, nil
}