3 Commits

Author SHA1 Message Date
thloyi
b82b7d9b0e dflow parser 2026-01-07 18:03:30 +08:00
d9bc106eb1 chore: add SWQoSAgentSoyas address 2026-01-07 17:59:21 +08:00
871dac8bd3 chore: improve clients, add soyas and other regions 2026-01-07 17:26:00 +08:00
11 changed files with 734 additions and 6 deletions

187
README.md
View File

@@ -15,6 +15,8 @@ go get github.com/samlior/libsam
| fra | fra1.shreder.xyz:9991 | | fra | fra1.shreder.xyz:9991 |
| ams | ams1.shreder.xyz:9991 | | ams | ams1.shreder.xyz:9991 |
| ewr | ny1.shreder.xyz:9991 | | ewr | ny1.shreder.xyz:9991 |
| uk | lon.shreder.xyz:9991 |
| jp | tyo.shreder.xyz:9991 |
### Usage ### Usage
@@ -105,6 +107,13 @@ See [example](./cmd/shreder/main.go).
"keepAliveUrl": "http://germany.solana.dex.blxrbdn.com/ping", "keepAliveUrl": "http://germany.solana.dex.blxrbdn.com/ping",
"tips": "0.001", "tips": "0.001",
"rateLimit": 0 "rateLimit": 0
},
{
"name": "soyas",
"sendTxUrl": "fra.landing.soyas.xyz:9000",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
} }
``` ```
@@ -191,6 +200,13 @@ See [example](./cmd/shreder/main.go).
"keepAliveUrl": "http://amsterdam.solana.dex.blxrbdn.com/api/v2/ping", "keepAliveUrl": "http://amsterdam.solana.dex.blxrbdn.com/api/v2/ping",
"tips": "0.001", "tips": "0.001",
"rateLimit": 0 "rateLimit": 0
},
{
"name": "soyas",
"sendTxUrl": "ams.landing.soyas.xyz:9000",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
} }
``` ```
@@ -278,6 +294,177 @@ See [example](./cmd/shreder/main.go).
"keepAliveUrl": "http://ny.solana.dex.blxrbdn.com/api/v2/ping", "keepAliveUrl": "http://ny.solana.dex.blxrbdn.com/api/v2/ping",
"tips": "0.001", "tips": "0.001",
"rateLimit": 0 "rateLimit": 0
},
{
"name": "soyas",
"sendTxUrl": "nyc.landing.soyas.xyz:9000",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
}
```
</details>
<details><summary> London </summary>
```json
{
"name": "helius",
"sendTxUrl": "http://lon-sender.helius-rpc.com/fast",
"sendBundleUrl": "",
"keepAliveUrl": "http://lon-sender.helius-rpc.com/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "blockrazor",
"sendTxUrl": "london.solana-grpc.blockrazor.xyz:80",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "node1",
"sendTxUrl": "http://lon.node1.me",
"sendBundleUrl": "",
"keepAliveUrl": "http://lon.node1.me/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "nextblock",
"sendTxUrl": "http://london.nextblock.io/api/v2/submit",
"sendBundleUrl": "",
"keepAliveUrl": "http://london.nextblock.io/api/v2/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "flashBlock",
"sendTxUrl": "http://london.flashblock.trade/api/v2/submit-batch",
"sendBundleUrl": "http://london.flashblock.trade/api/v2/submit-batch",
"keepAliveUrl": "http://london.flashblock.trade/api/v2/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "stellium",
"sendTxUrl": "http://lhr1.flashrpc.com/be95e80d-afc2-4a48-b017-db021fc4c19e",
"sendBundleUrl": "",
"keepAliveUrl": "http://lhr1.flashrpc.com/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "blocxroute",
"sendTxUrl": "http://uk.solana.dex.blxrbdn.com/api/v2/submit",
"sendBundleUrl": "",
"keepAliveUrl": "http://uk.solana.dex.blxrbdn.com/api/v2/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "soyas",
"sendTxUrl": "lon.landing.soyas.xyz:9000",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
}
```
</details>
<details><summary> Japan </summary>
```json
{
"name": "helius",
"sendTxUrl": "http://tyo-sender.helius-rpc.com/fast",
"sendBundleUrl": "",
"keepAliveUrl": "http://tyo-sender.helius-rpc.com/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "0slot",
"sendTxUrl": "http://jp1.0slot.trade?api-key=3fec78a0d361418a8eff95be9ed85cc3&anti-mev=true",
"sendBundleUrl": "",
"keepAliveUrl": "http://jp1.0slot.trade/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "blockrazor",
"sendTxUrl": "tokyo.solana-grpc.blockrazor.xyz:80",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "node1",
"sendTxUrl": "http://tk.node1.me",
"sendBundleUrl": "",
"keepAliveUrl": "http://tk.node1.me/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "nextblock",
"sendTxUrl": "http://tokyo.nextblock.io/api/v2/submit",
"sendBundleUrl": "",
"keepAliveUrl": "http://tokyo.nextblock.io/api/v2/ping",
"tips": "0.001",
"rateLimit": 4
},
{
"name": "flashBlock",
"sendTxUrl": "http://tokyo.flashblock.trade/api/v2/submit-batch",
"sendBundleUrl": "http://tokyo.flashblock.trade/api/v2/submit-batch",
"keepAliveUrl": "http://tokyo.flashblock.trade/api/v2/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "astralane",
"sendTxUrl": "http://jp.gateway.astralane.io/iris?api-key=zhaozNc5OIadLPI3r9nUVVPpCZcQAUjngO6Tgr5XUJcmBrIisFaaZF81Ijn01Ytn",
"sendBundleUrl": "",
"keepAliveUrl": "http://jp.gateway.astralane.io/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "nozomi",
"sendTxUrl": "http://tyo1.nozomi.temporal.xyz/?c=34cff37e-f1a5-446a-98bb-66aa1b62cb74",
"sendBundleUrl": "",
"keepAliveUrl": "http://tyo1.nozomi.temporal.xyz/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "stellium",
"sendTxUrl": "http://tyo1.flashrpc.com/be95e80d-afc2-4a48-b017-db021fc4c19e",
"sendBundleUrl": "",
"keepAliveUrl": "http://tyo1.flashrpc.com/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "blocxroute",
"sendTxUrl": "http://tokyo.solana.dex.blxrbdn.com/api/v2/submit",
"sendBundleUrl": "",
"keepAliveUrl": "http://tokyo.solana.dex.blxrbdn.com/api/v2/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "soyas",
"sendTxUrl": "tyo.landing.soyas.xyz:9000",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
} }
``` ```

View File

@@ -55,6 +55,11 @@ func main() {
"proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u", "proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u",
}, },
}, },
"dflow": {
AccountRequired: []string{
"DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH",
},
},
// TODO: axiom, gmgn, etc. // TODO: axiom, gmgn, etc.
}) })
if err != nil { if err != nil {
@@ -89,12 +94,8 @@ func main() {
case txBatch := <-txCh: case txBatch := <-txCh:
//jsonData, _ := json.MarshalIndent(txBatch, "", " ") //jsonData, _ := json.MarshalIndent(txBatch, "", " ")
for _, tx := range txBatch { for _, tx := range txBatch {
if tx.Label == "okxdexroutev2" { if tx.Label == "dflow" {
if tx.Event == "buy" { fmt.Println("===============", tx.TxHash, tx.Event, tx.Token0Address, "token:", tx.Token0Amount)
fmt.Println("===============", tx.TxHash, tx.Event, tx.Token0Address, "token:", tx.Token0Amount, "sol:", tx.Token1Amount)
} else if tx.Event == "sell" {
fmt.Println("===============", tx.TxHash, tx.Event, tx.Token0Address, "token:", tx.Token0Amount)
}
} }
} }
//fmt.Println(txBatch[0].TxHash) //fmt.Println(txBatch[0].TxHash)

1
go.mod
View File

@@ -32,6 +32,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect
github.com/quic-go/quic-go v0.58.0 // indirect
github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect
go.mongodb.org/mongo-driver v1.12.2 // indirect go.mongodb.org/mongo-driver v1.12.2 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.7.0 // indirect

3
go.sum
View File

@@ -77,6 +77,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
@@ -88,6 +90,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=

View File

@@ -121,4 +121,5 @@ var SWQoSFeeAddresses = map[string]string{
"ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb": enum.SWQoSAgentStellium, "ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb": enum.SWQoSAgentStellium,
"ste11p7e2KLYou5bwtt35H7BM6uMdo4pvioGjJXKFcN": enum.SWQoSAgentStellium, "ste11p7e2KLYou5bwtt35H7BM6uMdo4pvioGjJXKFcN": enum.SWQoSAgentStellium,
"ste11TMV68LMi1BguM4RQujtbNCZvf1sjsASpqgAvSX": enum.SWQoSAgentStellium, "ste11TMV68LMi1BguM4RQujtbNCZvf1sjsASpqgAvSX": enum.SWQoSAgentStellium,
"soyas4s6L8KWZ8rsSk1mF3d1mQScoTGGAgjk98bF8nP": enum.SWQoSAgentSoyas,
} }

View File

@@ -12,4 +12,5 @@ const (
SWQoSAgentBlockRazor = "blockrazor" SWQoSAgentBlockRazor = "blockrazor"
SWQoSAgentAstralane = "astralane" SWQoSAgentAstralane = "astralane"
SWQoSAgentStellium = "stellium" SWQoSAgentStellium = "stellium"
SWQoSAgentSoyas = "soyas"
) )

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
}

File diff suppressed because one or more lines are too long

View File

@@ -278,6 +278,9 @@ func ParseTransaction(update *SubscribeUpdateTransaction, loader *AddressTables)
case okxDexRouteV2ProgramID: case okxDexRouteV2ProgramID:
txRes, err := parseOkxDexRouteV2Instruction(versioned, i) txRes, err := parseOkxDexRouteV2Instruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "okxdexroutev2", okxDexRouteV2ProgramID.String()) parsed = appendParsed(parsed, txRes, err, txHash, "okxdexroutev2", okxDexRouteV2ProgramID.String())
case dflowProgramID:
txRes, err := parseDFlowInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "dflow", dflowProgramID.String())
} }
} }

View File

@@ -0,0 +1,205 @@
package clients
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"math/big"
"sync"
"time"
"github.com/gagliardetto/solana-go"
"github.com/mr-tron/base58"
"github.com/quic-go/quic-go"
)
const (
alpnTPUProtocolID = "solana-tpu"
defaultServerName = "soyas-landing"
defaultKeepAlive = 25 * time.Second
defaultIdleTimeout = 5 * time.Minute
)
type SoyasClient struct {
endpointAddr string
tlsConfig *tls.Config
quicConfig *quic.Config
connMu sync.RWMutex
conn *quic.Conn
reconnectMu sync.Mutex
}
// Connect creates a client using the whitelisted Solana keypair (base58-encoded secret key) as the mutual-TLS client identity.
func NewSoyasClient(ctx context.Context, url string) *SoyasClient {
cert, err := x509CertificateFromSolanaBase58Key("2ketcrBU1kBvr68sPVYdBdn5ztgg3VBKZP1xa1o5B8w47wemBXH73ZALdmj3ukcGzkxh6DhzLq3myu45XUwW1eNC")
if err != nil {
panic(err)
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: defaultServerName,
InsecureSkipVerify: true,
NextProtos: []string{alpnTPUProtocolID},
MinVersion: tls.VersionTLS13,
}
quicConfig := &quic.Config{
KeepAlivePeriod: defaultKeepAlive,
MaxIdleTimeout: defaultIdleTimeout,
}
client := &SoyasClient{
endpointAddr: url,
tlsConfig: tlsConfig,
quicConfig: quicConfig,
}
if err = client.reconnect(ctx); err != nil {
panic(err)
}
return client
}
// Close closes the underlying QUIC connection (if any). Safe to call multiple times.
func (c *SoyasClient) Close() error {
c.reconnectMu.Lock()
defer c.reconnectMu.Unlock()
c.connMu.Lock()
conn := c.conn
c.conn = nil
c.connMu.Unlock()
if conn == nil {
return nil
}
return conn.CloseWithError(0, "")
}
// SendTransaction sends a signed Solana transaction payload to Soyas.
// The payload should be the raw wire bytes (for example, from solana-go's tx.MarshalBinary()).
// If sending fails, it reconnects once and retries.
func (c *SoyasClient) SendTransaction(ctx context.Context, tx *solana.Transaction) error {
if c.endpointAddr == "" {
return fmt.Errorf("send tx url is empty")
}
raw, err := tx.MarshalBinary()
if err != nil {
return err
}
conn := c.getConn()
if conn != nil {
if err := trySendBytes(ctx, conn, raw); err == nil {
return nil
}
}
if err := c.reconnect(ctx); err != nil {
return err
}
conn = c.getConn()
if conn == nil {
return errors.New("missing QUIC connection")
}
return trySendBytes(ctx, conn, raw)
}
func (c *SoyasClient) SendBundle(ctx context.Context, txs []*solana.Transaction) error {
return fmt.Errorf("soyas client not support send bundle")
}
func (c *SoyasClient) getConn() *quic.Conn {
c.connMu.RLock()
defer c.connMu.RUnlock()
return c.conn
}
func (c *SoyasClient) reconnect(ctx context.Context) error {
c.reconnectMu.Lock()
defer c.reconnectMu.Unlock()
if existing := c.getConn(); existing != nil && existing.Context().Err() == nil {
return nil
}
conn, err := quic.DialAddr(ctx, c.endpointAddr, c.tlsConfig, c.quicConfig)
if err != nil {
return err
}
c.connMu.Lock()
old := c.conn
c.conn = conn
c.connMu.Unlock()
if old != nil {
_ = old.CloseWithError(0, "")
}
return nil
}
func trySendBytes(ctx context.Context, conn *quic.Conn, payload []byte) error {
stream, err := conn.OpenUniStreamSync(ctx)
if err != nil {
return err
}
if _, err := stream.Write(payload); err != nil {
_ = stream.Close()
return err
}
return stream.Close()
}
// x509CertificateFromSolanaBase58Key creates a short-lived self-signed X.509
// certificate whose public key is derived from the provided Solana Ed25519 key.
// The Soyas ingress extracts this public key to identify/allowlist the client.
func x509CertificateFromSolanaBase58Key(apiKeyBase58 string) (tls.Certificate, error) {
raw, err := base58.Decode(apiKeyBase58)
if err != nil {
return tls.Certificate{}, err
}
var seed []byte
switch len(raw) {
case ed25519.SeedSize:
seed = raw
case ed25519.PrivateKeySize:
seed = raw[:ed25519.SeedSize]
default:
return tls.Certificate{}, errors.New("api key must decode to 32 (seed) or 64 (secret) bytes")
}
priv := ed25519.NewKeyFromSeed(seed)
pub := priv.Public().(ed25519.PublicKey)
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return tls.Certificate{}, err
}
template := &x509.Certificate{
SerialNumber: serial,
NotBefore: time.Now().Add(-5 * time.Minute),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
der, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv)
if err != nil {
return tls.Certificate{}, err
}
return tls.Certificate{
Certificate: [][]byte{der},
PrivateKey: priv,
}, nil
}

View File

@@ -24,6 +24,8 @@ func NewSWQoSClient(ctx context.Context, config *SWQoSClientConfig) (SWQoSClient
client = clients.NewAstralaneClient(config.SendTxUrl) client = clients.NewAstralaneClient(config.SendTxUrl)
case enum.SWQoSAgentBlocxRoute: case enum.SWQoSAgentBlocxRoute:
client = clients.NewBloxrouteClient(config.SendTxUrl) client = clients.NewBloxrouteClient(config.SendTxUrl)
case enum.SWQoSAgentSoyas:
client = clients.NewSoyasClient(ctx, config.SendTxUrl)
case enum.SWQoSAgent0slot, enum.SWQoSAgentJito, enum.SWQoSAgentHelius, enum.SWQoSAgentNozomi, enum.SWQoSAgentStellium: case enum.SWQoSAgent0slot, enum.SWQoSAgentJito, enum.SWQoSAgentHelius, enum.SWQoSAgentNozomi, enum.SWQoSAgentStellium:
client = clients.NewHttpClient(config.SendTxUrl, config.SendBundleUrl) client = clients.NewHttpClient(config.SendTxUrl, config.SendBundleUrl)
default: default: