From 9ece4aebb97ee86e8ecf1f9a45283badf6b7f0a9 Mon Sep 17 00:00:00 2001 From: thloyi Date: Mon, 9 Feb 2026 14:46:19 +0800 Subject: [PATCH] all parser --- consts.go | 4 + enum.go | 1 + globals.go | 31 + go.mod | 10 +- go.sum | 71 +-- internal/test/test.go | 866 ++++++++++++++++++++++++++ internal/test2/test.go | 828 +++++++++++++++++++++++++ meta.go | 148 ++++- metaoradlmm.go | 81 +-- metaorapool.go | 880 ++++++++++++++++++++++++++ meteora_bonding_curve.go | 389 ++++++++++++ meteoradamm.go | 479 +++++++++++++++ orcawhirpool.go | 1262 ++++++++++++++++++++++++++++++++++++++ parser.go | 98 ++- pump.go | 17 +- pump_test.go | 10 +- pumpamm.go | 10 +- raydiumclmm.go | 375 +++++++++++ raydiumcpmm.go | 408 ++++++++++++ raydiumlaunchlab.go | 470 ++++++++++++++ raydiumv4.go | 399 ++++++++++++ tx.go | 57 +- 22 files changed, 6720 insertions(+), 174 deletions(-) create mode 100644 internal/test/test.go create mode 100644 internal/test2/test.go create mode 100644 metaorapool.go create mode 100644 meteora_bonding_curve.go create mode 100644 meteoradamm.go create mode 100644 orcawhirpool.go create mode 100644 raydiumclmm.go create mode 100644 raydiumcpmm.go create mode 100644 raydiumlaunchlab.go create mode 100644 raydiumv4.go diff --git a/consts.go b/consts.go index 957ef7c..93bf895 100644 --- a/consts.go +++ b/consts.go @@ -3,6 +3,7 @@ package pump_parser import "github.com/gagliardetto/solana-go" var platformFeeAddresses = map[solana.PublicKey]string{ + solana.MustPublicKeyFromBase58("HeZVpHj9jLwTVtMMbzQRf6mLtFPkWNSg11o68qrbUBa3"): PlatformGMGN, solana.MustPublicKeyFromBase58("BB5dnY55FXS1e1NXqZDwCzgdYJdMCj3B92PU6Q5Fb6DT"): PlatformGMGN, solana.MustPublicKeyFromBase58("7sHXjs1j7sDJGVSMSPjD1b4v3FD6uRSvRWfhRdfv5BiA"): PlatformGMGN, solana.MustPublicKeyFromBase58("ByRRgnZenY6W2sddo1VJzX9o4sMU4gPDUkcmgrpGBxRy"): PlatformGMGN, @@ -12,6 +13,7 @@ var platformFeeAddresses = map[solana.PublicKey]string{ solana.MustPublicKeyFromBase58("dBhdrmwBkRa66XxBuAK4WZeZnsZ6bHeHCCLXa3a8bTJ"): PlatformGMGN, solana.MustPublicKeyFromBase58("6TxjC5wJzuuZgTtnTMipwwULEbMPx5JPW3QwWkdTGnrn"): PlatformGMGN, solana.MustPublicKeyFromBase58("AVUCZyuT35YSuj4RH7fwiyPu82Djn2Hfg7y2ND2XcnZH"): PlatformPhoton, + solana.MustPublicKeyFromBase58("9yj3zvLS3fDMqi1F8zhkaWfq8TZpZWHe6cz1Sgt7djXf"): PlatformPhoton, solana.MustPublicKeyFromBase58("7LCZckF6XXGQ1hDY6HFXBKWAtiUgL9QY5vj1C4Bn1Qjj"): PlatformAxiom, solana.MustPublicKeyFromBase58("4V65jvcDG9DSQioUVqVPiUcUY9v6sb6HKtMnsxSKEz5S"): PlatformAxiom, solana.MustPublicKeyFromBase58("CeA3sPZfWWToFEBmw5n1Y93tnV66Vmp8LacLzsVprgxZ"): PlatformAxiom, @@ -42,6 +44,7 @@ var platformFeeAddresses = map[solana.PublicKey]string{ solana.MustPublicKeyFromBase58("3kxSQybWEeQZsMuNWMRJH4TxrhwoDwfv41TNMLRzFP5A"): PlatformMEVX, solana.MustPublicKeyFromBase58("BS3CyJ9rRC4Tp8G7f86r6hGvuu3XdrVGNVpbNM9U5WRZ"): PlatformMEVX, solana.MustPublicKeyFromBase58("97VmzkjX9w8gMFS2RnHTSjtMEDbifGXBq9pgosFdFnM"): PlatformTradeWiz, + solana.MustPublicKeyFromBase58("9rxM513XS4ruBbrGqCaRWuztmE34uxkFoMmp8SAAL7ar"): PlatformTradeWiz, solana.MustPublicKeyFromBase58("F34kcgMgCF7mYWkwLN3WN7KrFprr2NbwxuLvXx4fbztj"): PlatformSolTradingBot, solana.MustPublicKeyFromBase58("96aFQc9qyqpjMfqdUeurZVYRrrwPJG2uPV6pceu4B1yb"): PlatformSolTradingBot, solana.MustPublicKeyFromBase58("5wkyL2FLEcyUUgc3UeGntHTAfWfzDrVuxMnaMm7792Gk"): PlatformMoonshotMoney, @@ -236,6 +239,7 @@ var entryContractAddresses = map[solana.PublicKey]string{ solana.MustPublicKeyFromBase58("BBRouter1cVunVXvkcqeKkZQcBK7ruan37PPm3xzWaXD"): EntryContractBonkBot, solana.MustPublicKeyFromBase58("B3111yJCeHBcA1bizdJjUFPALfhAfSRnAbJzGUtnt56A"): EntryContractBinanceWallet, solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9"): EntryContractAxiom, + solana.MustPublicKeyFromBase58("B3jytJa6Tzpn4Ly7GNnDm3dMGqUin5aMRm5aPsJGU5G7"): EntryContractTradewiz, } var okxDexRoutersV2 = solana.MustPublicKeyFromBase58("proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u") diff --git a/enum.go b/enum.go index d995bcf..dc0e09b 100644 --- a/enum.go +++ b/enum.go @@ -52,6 +52,7 @@ const ( EntryContractMayhem = "pumpMayhem" EntryContractTerm = "term" EntryContractUnknown = "unknown" + EntryContractTradewiz = "tradewiz" ) const ( diff --git a/globals.go b/globals.go index f27fd02..4f54fc4 100644 --- a/globals.go +++ b/globals.go @@ -1,6 +1,7 @@ package pump_parser import ( + "encoding/binary" "errors" "fmt" @@ -38,6 +39,36 @@ func getInnerInstructions(innerInstructions InnerInstructions, offset uint) ([]I return inners, nil } +func parseTokenTransfer(tx *RawTx, instr Instruction) (from solana.PublicKey, to solana.PublicKey, amount uint64, err error) { + if len(instr.Accounts) < 3 { + return solana.PublicKey{}, solana.PublicKey{}, 0, fmt.Errorf("not enough accounts for token transfer instruction") + } + programAccount := tx.accountList[instr.ProgramIDIndex] + if !programAccount.Equals(solana.TokenProgramID) && !programAccount.Equals(solana.Token2022ProgramID) { + return solana.PublicKey{}, solana.PublicKey{}, 0, fmt.Errorf("not a token program instruction") + } + if len(instr.Data) < 9 { + return solana.PublicKey{}, solana.PublicKey{}, 0, fmt.Errorf("invalid data length for token transfer instruction") + } + method := instr.Data[0] + if method != 3 && method != 12 { // Transfer instruction + return solana.PublicKey{}, solana.PublicKey{}, 0, fmt.Errorf("not a token transfer instruction") + } + if method == 3 { + // Transfer + amount = binary.LittleEndian.Uint64(instr.Data[1:9]) + from = tx.accountList[instr.Accounts[0]] + to = tx.accountList[instr.Accounts[1]] + } else { + // TransferChecked + amount = binary.LittleEndian.Uint64(instr.Data[1:9]) + from = tx.accountList[instr.Accounts[0]] + to = tx.accountList[instr.Accounts[2]] + } + + return from, to, amount, nil +} + func isMayhemPump(feeAccount solana.PublicKey) bool { for _, mayhemFeeAccount := range mayhemFeeAccounts { if feeAccount.Equals(mayhemFeeAccount) { diff --git a/go.mod b/go.mod index 880ad14..8ade731 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,12 @@ require ( github.com/shopspring/decimal v1.4.0 go.onsig.ai/onsig/yellowstone-proto v1.0.0 google.golang.org/grpc v1.78.0 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.31.1 ) require ( filippo.io/edwards25519 v1.1.0 // indirect - github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/blendle/zapdriver v1.3.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -22,6 +23,12 @@ require ( github.com/gagliardetto/treeout v0.1.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect @@ -39,6 +46,7 @@ require ( go.uber.org/zap v1.27.1 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect diff --git a/go.sum b/go.sum index 7b4188d..20ce4c1 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,9 @@ -filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= -filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI= github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= -github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -21,8 +16,6 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= @@ -41,8 +34,6 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -80,8 +71,9 @@ github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwX github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= @@ -95,16 +87,22 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -122,14 +120,11 @@ github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -140,7 +135,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= @@ -148,8 +142,6 @@ github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjW github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -165,8 +157,6 @@ github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+D github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= -github.com/streamingfast/logging v0.0.0-20250918142248-ac5a1e292845 h1:VMA0pZ3MI8BErRA3kh8dKJThP5d0Xh5vZVk5yFIgH/A= -github.com/streamingfast/logging v0.0.0-20250918142248-ac5a1e292845/go.mod h1:BtDq81Tyc7H8up5aXNi/I95nPmG3C0PLEqGWY/iWQ2E= github.com/streamingfast/logging v0.0.0-20251216203033-fdad0a00f1ca h1:D9r6WXATiqumhUTqSysurIi3N50z4orVBW+TEMp50Q4= github.com/streamingfast/logging v0.0.0-20251216203033-fdad0a00f1ca/go.mod h1:fJ5nP7ZSMB4MQQ6RM7cF+LiSQ43b5cVletcSUNL8z2M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -178,7 +168,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -186,15 +175,9 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= -go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.onsig.ai/onsig/yellowstone-proto v1.0.0 h1:+XBNIoyl3HoQGBhgWCf8Ma3zNoUHKorFV8tR+HnE4Lw= @@ -215,29 +198,24 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= -go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= @@ -252,11 +230,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -276,14 +251,14 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 h1:6/3JGEh1C88g7m+qzzTbl3A0FtsLguXieqofVLU/JAo= -golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -305,8 +280,6 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -315,8 +288,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -325,16 +296,11 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -353,21 +319,14 @@ golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -382,4 +341,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/internal/test/test.go b/internal/test/test.go new file mode 100644 index 0000000..0594dcc --- /dev/null +++ b/internal/test/test.go @@ -0,0 +1,866 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "log" + "log/slog" + "strings" + "time" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/jackc/pgtype" + "github.com/shopspring/decimal" + solana_parser "github.com/thloyi/pump-parser" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var ( + blockFlag = flag.Uint64("block", 0, "block number to process") + blockRange = flag.Uint64("range", 100, "number of blocks to compare") +) + +func main() { + flag.Parse() + slot := *blockFlag + if slot == 0 { + fmt.Println("please provide a valid block number using -block flag") + return + } + client := rpc.New("http://127.0.0.1:18899") + dsn := "host=10.180.183.27 user=postgres password=123456789 dbname=solana port=5432 sslmode=disable TimeZone=UTC" + db := NewGorm(dsn) + for { + if slot > *blockFlag+*blockRange { + fmt.Printf("compare done for blocks %d to %d\n", *blockFlag, slot-1) + break + } + dbTxs, err := getBlockTxsFromDb(db, slot) + if err != nil { + fmt.Println("get block txs error:", err) + return + } + dbAction, err := getBlockActionsFromDb(db, slot) + if err != nil { + fmt.Println("get block actions error:", err) + return + } + var data = NewBlockData(decimal.NewFromFloat(100.0)) + + var rewards = false + var version uint64 = 0 + blocks, err := client.GetBlockWithOpts(context.Background(), slot, &rpc.GetBlockOpts{ + TransactionDetails: rpc.TransactionDetailsFull, + Rewards: &rewards, + Commitment: rpc.CommitmentFinalized, + Encoding: solana.EncodingBase64, + MaxSupportedTransactionVersion: &version, + }) + if err != nil { + slot++ + fmt.Println("get block error:", err) + continue + } + solana_parser.EnableAllParsers() + + var txs []*solana_parser.Tx + for i, tx := range blocks.Transactions { + var blockTime uint64 + if blocks.BlockTime != nil { + blockTime = uint64(*blocks.BlockTime) + } + rawTx, err := solana_parser.FromRpcTransactionWithMeta(tx, &blockTime, slot, int64(i)) + if err != nil { + fmt.Println("from rpc tx error:", i, err) + break + } + if rawTx.Meta.Err != nil { + continue + } + parsedTx, err := solana_parser.ParseRawTx(rawTx) + if err != nil { + fmt.Println("parse tx error:", i, rawTx.TxHash(), err) + continue + } + txs = append(txs, parsedTx) + } + var parseErr bool + for _, result := range txs { + swapsLen := len(result.Swaps) + for i := 0; i < swapsLen; i++ { + action := result.Swaps[i] + var actions []solana_parser.Swap = make([]solana_parser.Swap, 0, 2) + actions = append(actions, action) + if i+1 < swapsLen { + nextAction := result.Swaps[i+1] + if action.Event == "buy" && nextAction.Event == "complete" && + action.Program == solana_parser.SolProgramPump && + nextAction.Program == solana_parser.SolProgramPump && + action.BaseMint == nextAction.BaseMint { + actions = append(actions, nextAction) + i++ + } + if action.Event == "migrate" && nextAction.Event == "create" && + action.Program == solana_parser.SolProgramPump && + nextAction.Program == solana_parser.SolProgramPumpAMM && + action.BaseMint == nextAction.BaseMint { + actions = append(actions, nextAction) + i++ + } + } + if err = HandleAction(context.Background(), result, actions, data); err != nil { + //h.logger.Errorf("handle action error: %s - %v", result.RawTx.Transaction.Signatures[0].String(), err) + fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err) + parseErr = true + } + } + } + fmt.Println("slot", slot, "tx count: ", len(data.Txs)) + + // compare db and parsed data + _, _ = compareTxs(dbTxs, data.Txs) + _, miss2 := compareActions(dbAction, data.Actions) + if miss2 > 0 { + break + } + if parseErr { + break + } + time.Sleep(time.Millisecond * 200) + slot++ + + } +} + +var ( + meteoraDammV2Program = solana.MustPublicKeyFromBase58("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG") + raydiumCPmmProgramID = solana.MustPublicKeyFromBase58("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C") +) + +func HandleAction(ctx context.Context, tx *solana_parser.Tx, swaps []solana_parser.Swap, data *BlockData) error { + swapLen := len(swaps) + if len(swaps) == 0 { + return nil + } + if swaps[0].BaseMint != solana_parser.WSOL && swaps[0].QuoteMint != solana_parser.WSOL && !swaps[0].QuoteMint.IsZero() { + return nil + } + + if len(swaps) == 0 { + return nil + } + event := swaps[0].Event + swap := swaps[0] + action := SwapGetter{swap} + switch event { + case "buy", "sell": + + data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price)) + if swap.Program == solana_parser.SolProgramPump { + if swapLen == 2 && swaps[1].Event == "complete" { + t := pgtype.Timestamptz{} + t.Set(time.Unix(tx.BlockAt, 0)) + data.AppendAction(Action{ + Maker: swaps[1].User.String(), + Token: swaps[1].BaseMint.String(), + Pair: swaps[1].Pool.String(), + Action: "pump-migrate", + Block: tx.Block, + BlockAt: t, + TxHash: tx.GetTxHash(), + }) + } + } + return data.SetPair(action, tx.Block, "") + + case "create": + pair, err := action.GetPair(tx.Block, "") + if err != nil { + return err + } + data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price)) + data.Pairs[pair.Address] = *pair + case "add_liquidity", "remove_liquidity", "deposit", "withdraw", "add", "remove": + liquidityTx, err := action.GetLiquidityTx(tx, uint64(swap.TxIndex)) + if liquidityTx == nil { + return err + } + data.AppendTx(*liquidityTx) + return data.SetPair(action, tx.Block, "") + } + + if event != "migrate" { + return nil + } + if swap.Program == solana_parser.SolProgramPump { + t := pgtype.Timestamptz{} + t.Set(time.Unix(tx.BlockAt, 0)) + if swapLen == 2 && swaps[1].Event == "create" && swaps[1].Program == solana_parser.SolProgramPumpAMM && swaps[1].BaseMint == swap.BaseMint { + tokenMint := swap.BaseMint.String() + data.AppendAction(Action{ + Maker: swap.User.String(), + Token: tokenMint, + Pair: swaps[1].Pool.String(), + Action: "on-pumpswap", + Block: tx.Block, + BlockAt: t, + TxHash: tx.GetTxHash(), + }) + data.NewRaydium = append(data.NewRaydium, tokenMint) + } + } else if swap.Program == solana_parser.SolProgramRaydiumLaunchLab || swap.Program == solana_parser.SolProgramRaydiumLaunchLabBonk { + t := pgtype.Timestamptz{} + t.Set(time.Unix(tx.BlockAt, 0)) + var actionType string + if action.MigrateTopProgram == raydiumCPmmProgramID { + actionType = "on-raydium-cpmm" + } else { + actionType = "on-raydium-amm" + } + data.AppendAction(Action{ + Maker: action.User.String(), + Token: action.BaseMint.String(), + Pair: action.MigrateToPool.String(), + Action: actionType, + Block: tx.Block, + BlockAt: t, + TxHash: tx.GetTxHash(), + }) + } else if swap.Program == solana_parser.SolProgramMeteoraBondingCurve { + t := pgtype.Timestamptz{} + t.Set(time.Unix(tx.BlockAt, 0)) + var actionType string + if swap.MigrateTopProgram == meteoraDammV2Program { + actionType = "on-meteora-amm-v2" + } else { + actionType = "on-meteora-amm-v1" + } + data.AppendAction(Action{ + Maker: action.User.String(), + Token: action.BaseMint.String(), + Pair: action.MigrateToPool.String(), + Action: actionType, + Block: uint64(tx.Block), + BlockAt: t, + TxHash: tx.GetTxHash(), + }) + } + + return nil +} + +type Pair struct { + Id string `gorm:"column:id;primaryKey;default:uuid_generate_v4()"` + Address string + Name string + Token0 string + Token1 string + LpToken string + ChainId int64 + Reserve0 decimal.Decimal + Reserve1 decimal.Decimal + Block uint64 + BlockAt *pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at,omitempty"` + CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"created_at,omitempty"` + SortId uint64 + Program string + + IsCreate bool `gorm:"-"` + //TokenObj *Token `gorm:"-" json:"token_obj,omitempty"` + UpdateSlot uint64 `gorm:"-"` + InDB bool `gorm:"-"` +} + +type Tx struct { + Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"` + PairAddress string `json:"pair_address"` + Maker string `json:"maker"` + Token0Address string `json:"token0_address"` + Token1Address string `json:"token1_address"` + Token0Amount decimal.Decimal `json:"token0Amount" gorm:"column:token0_amount;type:numeric"` + Token1Amount decimal.Decimal `json:"token1Amount" gorm:"column:token1_amount;type:numeric"` + PriceUsd decimal.Decimal `json:"price_usd" gorm:"column:price_usd;type:numeric"` + AmountUsd decimal.Decimal `json:"amount_usd" gorm:"column:amount_usd;type:numeric"` + Block uint64 `json:"block"` + BlockIndex uint64 `json:"index"` + Event string `json:"event"` + TxHash string `json:"tx_hash"` + TxIndex uint64 `json:"topic_index"` + Program string `json:"program"` + BlockAt pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at"` + CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"` + TotalSupply string `gorm:"total_supply"` + AfterReserve0 string `gorm:"after_reserve0"` + AfterReserve1 string `gorm:"after_reserve1"` + PositionChange int64 `gorm:"position_change"` + Platform string `gorm:"column:tx_platform;type:platform;default:'none'" json:"tx_platform"` + PlatformFee decimal.Decimal `gorm:"-" json:"-"` // TODO: save to db + CUPrice decimal.Decimal `gorm:"column:tx_cu_price;type:numeric" json:"tx_cu_price"` + MevAgent string `gorm:"column:tx_mev_agent;type:mev_agent;default:'none'" json:"tx_mev_agent"` + MevAgentFee decimal.Decimal `gorm:"column:tx_mev_agent_fee;type:numeric" json:"tx_mev_agent_fee"` + AfterSOLBalance decimal.Decimal `gorm:"column:after_sol_balance;type:numeric" json:"after_sol_balance"` + EntryContract string `gorm:"column:tx_entry_contract;type:entry_contract;default:'none'" json:"tx_entry_contract"` +} + +type Action struct { + Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"` + Maker string `json:"maker"` + Token string `json:"token"` + Pair string `json:"pair"` + Action string `json:"action"` + Block uint64 `json:"block"` + BlockAt pgtype.Timestamptz `json:"block_at"` + TxHash string `json:"tx_hash"` + CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"` +} + +type BlockData struct { + Pairs map[string]Pair + Txs []Tx + Actions []Action + Price decimal.Decimal + NewRaydium []string +} + +func NewBlockData(price decimal.Decimal) *BlockData { + return &BlockData{ + Pairs: make(map[string]Pair), + Txs: make([]Tx, 0), + Actions: make([]Action, 0), + Price: price, + NewRaydium: make([]string, 0), + } +} + +func (bd *BlockData) AppendTx(tx Tx) { + bd.Txs = append(bd.Txs, tx) +} + +func (bd *BlockData) AppendAction(action Action) { + bd.Actions = append(bd.Actions, action) +} + +func (bd *BlockData) SetPair(action SwapGetter, block uint64, _ string) error { + pair, err := action.GetPair(block, "") + if err != nil { + return err + } + bd.Pairs[pair.Address] = *pair + return nil +} + +type SwapGetter struct { + solana_parser.Swap +} + +const ( + PositionChangeNone = int64(iota) + PositionChangeNewBuy + PositionChangeBuyMore + PositionChangeSellPart + PositionChangeSellAll +) + +func (spg SwapGetter) GetLiquidityTx(tx *solana_parser.Tx, index uint64) (*Tx, error) { + if spg.BaseMint != solana.WrappedSol && spg.QuoteMint != solana.WrappedSol { + return nil, nil + } + + var ( + token0 string + amount0 decimal.Decimal + amount1 decimal.Decimal + pool0 decimal.Decimal + pool1 decimal.Decimal + + event string + ) + + if spg.BaseMint == solana.WrappedSol { + amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + token0 = spg.QuoteMint.String() + pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + } else { + amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + token0 = spg.BaseMint.String() + pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + } + if spg.Event == "deposit" || spg.Event == "add" || spg.Event == "add_liquidity" || spg.Event == "add_liquidity_one_side" { + event = "add" + } else if spg.Event == "withdraw" || spg.Event == "remove" || spg.Event == "remove_liquidity" || spg.Event == "remove_liquidity_one_side" { + event = "remove" + } + if event == "" { + return nil, nil + } + + mevName, mevFee := tx.CheckMevAgent() + platformName, platformFee := tx.CheckPlatformOnSig(spg.Swap) + + pairString := "" + if spg.Program == solana_parser.SolProgramPump { + pairString = spg.BaseMint.String() + } else { + pairString = spg.Pool.String() + } + t := pgtype.Timestamptz{} + _ = t.Set(time.Unix(tx.BlockAt, 0)) + return &Tx{ + PairAddress: pairString, + Maker: spg.User.String(), + Token0Address: token0, + Token1Address: "So11111111111111111111111111111111111111112", + Token0Amount: amount0, + Token1Amount: amount1, + Block: tx.Block, + BlockIndex: tx.BlockIndex, + Event: event, + TxHash: tx.GetTxHash(), + TxIndex: index, + BlockAt: t, + Program: spg.Program, + AfterReserve0: pool0.String(), + AfterReserve1: pool1.String(), + Platform: platformName, + PlatformFee: platformFee, + CUPrice: tx.CUPrice, + MevAgent: mevName, + MevAgentFee: mevFee, + AfterSOLBalance: spg.AfterSOLBalance, + EntryContract: spg.CheckEntryContract(), + }, nil +} + +func (spg SwapGetter) GetTx(tx *solana_parser.Tx, index uint64, price decimal.Decimal) Tx { + var ( + token0 string + amount0 decimal.Decimal + amount1 decimal.Decimal + pool0 decimal.Decimal + pool1 decimal.Decimal + + event string + ) + + if spg.BaseMint == solana.WrappedSol { + amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + token0 = spg.QuoteMint.String() + pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + if spg.Event == "buy" { + event = "sell" + } else if spg.Event == "sell" { + event = "buy" + } + } else { + amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + token0 = spg.BaseMint.String() + pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + event = spg.Event + } + + priceUsd := decimal.Zero + if amount0.GreaterThan(priceUsd) { + priceUsd = amount1.Div(amount0).Mul(price) + } + pc := PositionChangeNone + if event == "buy" { + pc = PositionChangeNewBuy + if spg.BaseMint == solana.WrappedSol { + if spg.UserQuoteBalance.GreaterThan(spg.QuoteAmount) { + pc = PositionChangeBuyMore + } + } else { + if spg.UserBaseBalance.GreaterThan(spg.BaseAmount) { + pc = PositionChangeBuyMore + } + } + } else if event == "sell" { + pc = PositionChangeSellPart + if spg.BaseMint == solana.WrappedSol { + if spg.UserQuoteBalance.Div(decimal.New(1, int32(spg.QuoteMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) { + pc = PositionChangeSellAll + } + } else { + if spg.UserBaseBalance.Div(decimal.New(1, int32(spg.BaseMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) { + pc = PositionChangeSellAll + } + } + } + + mevName, mevFee := tx.CheckMevAgent() + platformName, platformFee := tx.CheckPlatformOnSig(spg.Swap) + + if mevName == "" { + mevName = "none" + } + if mevName == "unknown" { + mevName = "none" + mevFee = decimal.Zero + } + pairString := "" + if spg.Program == solana_parser.SolProgramPump { + pairString = spg.BaseMint.String() + } else { + pairString = spg.Pool.String() + } + t := pgtype.Timestamptz{} + _ = t.Set(time.Unix(tx.BlockAt, 0)) + + return Tx{ + PairAddress: pairString, + Maker: spg.User.String(), + Token0Address: token0, + Token1Address: "So11111111111111111111111111111111111111112", + Token0Amount: amount0, + Token1Amount: amount1, + PriceUsd: priceUsd, + AmountUsd: amount1.Mul(price), + Block: tx.Block, + BlockIndex: tx.BlockIndex, + Event: event, + TxHash: tx.GetTxHash(), + TxIndex: index, + BlockAt: t, + Program: spg.Program, + AfterReserve0: pool0.String(), + AfterReserve1: pool1.String(), + PositionChange: pc, + Platform: platformName, + PlatformFee: platformFee, + CUPrice: tx.CUPrice, + MevAgent: mevName, + MevAgentFee: mevFee, + AfterSOLBalance: spg.AfterSOLBalance, + EntryContract: spg.CheckEntryContract(), + } +} + +func (spg SwapGetter) GetPair(slot uint64, _ string) (*Pair, error) { + //pump amm + if spg.Program == solana_parser.SolProgramPump { + tokenMint := spg.BaseMint.String() + return &Pair{ + Address: tokenMint, + Token0: tokenMint, + Token1: "So11111111111111111111111111111111111111112", + ChainId: 900, + Reserve0: spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))), + Reserve1: spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))), + IsCreate: spg.Event == "create", + Program: spg.Program, + UpdateSlot: slot, + }, nil + } else { + var ( + token0 string + amount0 decimal.Decimal + amount1 decimal.Decimal + ) + if spg.BaseMint.IsZero() || spg.QuoteMint.IsZero() { + return nil, errors.New("base mint or quote mint is empty") + } + + if spg.BaseMint == solana.WrappedSol { + amount0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + amount1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + //decimal0 = spg.QuoteMintDecimals + token0 = spg.QuoteMint.String() + } else { + amount0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + amount1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + //decimal0 = a.BaseDecimals + token0 = spg.BaseMint.String() + } + + return &Pair{ + Address: spg.Pool.String(), + LpToken: spg.LpMint.String(), + Token0: token0, + Token1: "So11111111111111111111111111111111111111112", + ChainId: 900, + Reserve0: amount0, + Reserve1: amount1, + IsCreate: false, + Program: spg.Program, + UpdateSlot: slot, + }, nil + } +} + +func getBlockTxsFromDb(db *gorm.DB, block uint64) ([]Tx, error) { + var txs []Tx + result := db.Table("tx").Where("block = ?", block).Find(&txs) + return txs, result.Error +} + +func getBlockActionsFromDb(db *gorm.DB, block uint64) ([]Action, error) { + var txs []Action + result := db.Table("action").Where("block = ?", block).Find(&txs) + return txs, result.Error +} + +type dbLog struct { + logger *slog.Logger +} + +func (l *dbLog) Printf(format string, args ...interface{}) { + l.logger.Info(fmt.Sprintf(format, args...)) +} + +func newDbLog() *dbLog { + return &dbLog{logger: slog.Default()} +} + +func NewGorm(dsn string) *gorm.DB { + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ + Logger: logger.New(newDbLog(), logger.Config{ + Colorful: false, + LogLevel: logger.Warn, + SlowThreshold: time.Second * 10, + IgnoreRecordNotFoundError: true, + }), + }) + if err != nil { + panic(err) + } + + return db +} + +func compareTxs(dbTxs []Tx, dataTxs []Tx) (diff int, missing int) { + dataByHash := make(map[string][]Tx, len(dataTxs)) + for _, tx := range dataTxs { + dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)] = append(dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)], tx) + } + + for _, dbTx := range dbTxs { + candidates := dataByHash[fmt.Sprintf("%s-%d", dbTx.TxHash, dbTx.TxIndex)] + if len(candidates) == 0 { + missing++ + log.Printf("missing tx: %s", txCompareString(dbTx)) + continue + } + matched := false + for _, dataTx := range candidates { + if txEqualWithoutHash(dbTx, dataTx) { + matched = true + break + } + } + if !matched { + diff++ + log.Printf("tx diff hash=%s, program=%s, event:%s: %s, ", dbTx.TxHash, dbTx.Program, dbTx.Event, txCompareDiffString(dbTx, candidates[0])) + } + } + log.Printf("compare txs done: db=%d parsed=%d missing=%d diff=%d", len(dbTxs), len(dataTxs), missing, diff) + return diff, missing +} + +func withinOnePercentDecimal(a decimal.Decimal, b decimal.Decimal) bool { + if a.IsZero() { + return b.IsZero() + } + diff := a.Sub(b).Abs() + threshold := a.Abs().Mul(decimal.NewFromFloat(0.03)) + return diff.LessThanOrEqual(threshold) +} + +func withinOnePercentStringDecimal(a string, b string) bool { + ad, errA := decimal.NewFromString(a) + bd, errB := decimal.NewFromString(b) + if errA != nil || errB != nil { + return a == b + } + return withinOnePercentDecimal(ad, bd) +} + +func txEqualWithoutHash(a Tx, b Tx) bool { + //mevMatch := a.MevAgent == b.MevAgent || (a.MevAgent == "none" && b.MevAgent == "unknown") || (a.MevAgent == "unknown" && b.MevAgent == "none") + //mevNone := a.MevAgent == "none" || a.MevAgent == "unknown" + + return ((a.Program == solana_parser.SolProgramMeteoraBondingCurve && a.Event == "create") || a.PairAddress == b.PairAddress) && + a.Token1Address == b.Token1Address && + (a.Token0Address == "" || a.Token0Address == b.Token0Address) && + //a.Maker == b.Maker && + (a.Token0Address == "" || withinOnePercentDecimal(a.Token0Amount, b.Token0Amount)) && + ((a.Token1Amount.LessThan(decimal.NewFromInt(10)) && b.Token1Amount.LessThan(decimal.NewFromInt(10))) || withinOnePercentDecimal(a.Token1Amount, b.Token1Amount)) && + a.Block == b.Block && + a.BlockIndex == b.BlockIndex && + a.Event == b.Event && + a.TxIndex == b.TxIndex && + a.Program == b.Program && + (a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0)) && + (a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1)) && + // a.PositionChange == b.PositionChange && + (a.Platform == b.Platform || (a.Platform == "photon" && b.Platform == "fake") || (a.Platform == "trojan" && b.Platform == "fake")) && + a.CUPrice.String() == b.CUPrice.String() // && + //mevMatch && + //(mevNone || a.MevAgentFee.String() == b.MevAgentFee.String()) && + //(a.Program == solana_parser.SolProgramRaydiumV4 || a.AfterSOLBalance.String() == b.AfterSOLBalance.String()) + //&& + // a.EntryContract == b.EntryContract +} + +func txCompareDiffString(a Tx, b Tx) string { + var diffs []string + if a.PairAddress != b.PairAddress { + diffs = append(diffs, fmt.Sprintf("PairAddress db=%s data=%s", a.PairAddress, b.PairAddress)) + } + //if a.Maker != b.Maker { + // diffs = append(diffs, fmt.Sprintf("Maker db=%s, data=%s", a.Maker, b.Maker)) + //} + if a.Token1Address != b.Token1Address { + diffs = append(diffs, fmt.Sprintf("Token1Address db=%s data=%s", a.Token1Address, b.Token1Address)) + } + if a.Token0Address != b.Token0Address { + diffs = append(diffs, fmt.Sprintf("Token0Address db=%s data=%s", a.Token0Address, b.Token0Address)) + } + if !withinOnePercentDecimal(a.Token0Amount, b.Token0Amount) { + diffs = append(diffs, fmt.Sprintf("Token0Amount db=%s data=%s", a.Token0Amount.String(), b.Token0Amount.String())) + } + if !withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) { + diffs = append(diffs, fmt.Sprintf("Token1Amount db=%s data=%s", a.Token1Amount.String(), b.Token1Amount.String())) + } + if a.Block != b.Block { + diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block)) + } + if a.BlockIndex != b.BlockIndex { + diffs = append(diffs, fmt.Sprintf("BlockIndex db=%d data=%d", a.BlockIndex, b.BlockIndex)) + } + if a.Event != b.Event { + diffs = append(diffs, fmt.Sprintf("Event db=%s data=%s", a.Event, b.Event)) + } + if a.TxIndex != b.TxIndex { + diffs = append(diffs, fmt.Sprintf("TxIndex db=%d data=%d", a.TxIndex, b.TxIndex)) + } + if a.Program != b.Program { + diffs = append(diffs, fmt.Sprintf("Program db=%s data=%s", a.Program, b.Program)) + } + if !withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0) { + diffs = append(diffs, fmt.Sprintf("AfterReserve0 db=%s data=%s", a.AfterReserve0, b.AfterReserve0)) + } + if !withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1) { + diffs = append(diffs, fmt.Sprintf("AfterReserve1 db=%s data=%s", a.AfterReserve1, b.AfterReserve1)) + } + //if a.PositionChange != b.PositionChange { + // diffs = append(diffs, fmt.Sprintf("PositionChange db=%d data=%d", a.PositionChange, b.PositionChange)) + //} + if a.Platform != b.Platform { + diffs = append(diffs, fmt.Sprintf("Platform db=%s data=%s", a.Platform, b.Platform)) + } + if a.CUPrice.String() != b.CUPrice.String() { + diffs = append(diffs, fmt.Sprintf("CUPrice db=%s data=%s", a.CUPrice.String(), b.CUPrice.String())) + } + //if a.MevAgent != b.MevAgent { + // diffs = append(diffs, fmt.Sprintf("MevAgent db=%s data=%s", a.MevAgent, b.MevAgent)) + //} + //if a.MevAgentFee.String() != b.MevAgentFee.String() { + // diffs = append(diffs, fmt.Sprintf("MevAgentFee db=%s data=%s", a.MevAgentFee.String(), b.MevAgentFee.String())) + //} + //if a.AfterSOLBalance.String() != b.AfterSOLBalance.String() { + // diffs = append(diffs, fmt.Sprintf("AfterSOLBalance db=%s data=%s", a.AfterSOLBalance.String(), b.AfterSOLBalance.String())) + //} + //if a.EntryContract != b.EntryContract { + // diffs = append(diffs, fmt.Sprintf("EntryContract db=%s data=%s", a.EntryContract, b.EntryContract)) + //} + return strings.Join(diffs, "; ") +} + +func compareActions(dbActions []Action, dataActions []Action) (diff, missing int) { + dataByHash := make(map[string][]Action, len(dataActions)) + for _, action := range dataActions { + dataByHash[action.TxHash] = append(dataByHash[action.TxHash], action) + } + + for _, dbAction := range dbActions { + candidates := dataByHash[dbAction.TxHash] + if len(candidates) == 0 { + missing++ + log.Printf("missing action: %s", actionCompareString(dbAction)) + continue + } + matched := false + for _, dataAction := range candidates { + if actionEqualWithoutHash(dbAction, dataAction) { + matched = true + break + } + } + if !matched { + diff++ + log.Printf("action diff hash=%s: %s", dbAction.TxHash, actionCompareDiffString(dbAction, candidates[0])) + } + } + log.Printf("compare actions done: db=%d parsed=%d missing=%d diff=%d", len(dbActions), len(dataActions), missing, diff) + return diff, missing +} + +func actionEqualWithoutHash(a Action, b Action) bool { + return a.Maker == b.Maker && + a.Token == b.Token && + a.Pair == b.Pair && + a.Action == b.Action && + a.Block == b.Block +} + +func actionCompareDiffString(a Action, b Action) string { + var diffs []string + if a.Maker != b.Maker { + diffs = append(diffs, fmt.Sprintf("Maker db=%s data=%s", a.Maker, b.Maker)) + } + if a.Token != b.Token { + diffs = append(diffs, fmt.Sprintf("Token db=%s data=%s", a.Token, b.Token)) + } + if a.Pair != b.Pair { + diffs = append(diffs, fmt.Sprintf("Pair db=%s data=%s", a.Pair, b.Pair)) + } + if a.Action != b.Action { + diffs = append(diffs, fmt.Sprintf("Action db=%s data=%s", a.Action, b.Action)) + } + if a.Block != b.Block { + diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block)) + } + return strings.Join(diffs, "; ") +} + +func actionCompareString(action Action) string { + return fmt.Sprintf("Maker=%s Token=%s Pair=%s Action=%s Block=%d TxHash=%s", action.Maker, action.Token, action.Pair, action.Action, action.Block, action.TxHash) +} + +func txCompareString(tx Tx) string { + return fmt.Sprintf( + "tx.Program=%s TxHash=%s PairAddress=%s Token1Address=%s Token0Amount=%s Token1Amount=%s Block=%d BlockIndex=%d Event=%s TxIndex=%d AfterReserve0=%s AfterReserve1=%s PositionChange=%d Platform=%s CUPrice=%s MevAgent=%s MevAgentFee=%s AfterSOLBalance=%s EntryContract=%s", + tx.Program, + tx.TxHash, + tx.PairAddress, + tx.Token1Address, + tx.Token0Amount.String(), + tx.Token1Amount.String(), + tx.Block, + tx.BlockIndex, + tx.Event, + tx.TxIndex, + tx.AfterReserve0, + tx.AfterReserve1, + tx.PositionChange, + tx.Platform, + tx.CUPrice.String(), + tx.MevAgent, + tx.MevAgentFee.String(), + tx.AfterSOLBalance.String(), + tx.EntryContract, + ) +} diff --git a/internal/test2/test.go b/internal/test2/test.go new file mode 100644 index 0000000..4d9c72f --- /dev/null +++ b/internal/test2/test.go @@ -0,0 +1,828 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + "log/slog" + "strings" + "time" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/jackc/pgtype" + "github.com/shopspring/decimal" + solana_parser "github.com/thloyi/pump-parser" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var () + +func main() { + + var slot uint64 = 399015152 + var data = NewBlockData(decimal.NewFromFloat(100.0)) + client := rpc.New("https://staked.helius-rpc.com?api-key=") + var rewards = false + var version uint64 = 0 + blocks, err := client.GetBlockWithOpts(context.Background(), slot, &rpc.GetBlockOpts{ + TransactionDetails: rpc.TransactionDetailsFull, + Rewards: &rewards, + Commitment: rpc.CommitmentFinalized, + Encoding: solana.EncodingBase64, + MaxSupportedTransactionVersion: &version, + }) + if err != nil { + slot++ + fmt.Println("get block error:", err) + return + } + solana_parser.EnableAllParsers() + + var txs []*solana_parser.Tx + for i, tx := range blocks.Transactions { + var blockTime uint64 + if blocks.BlockTime != nil { + blockTime = uint64(*blocks.BlockTime) + } + rawTx, err := solana_parser.FromRpcTransactionWithMeta(tx, &blockTime, slot, int64(i)) + if err != nil { + fmt.Println("from rpc tx error:", i, err) + break + } + if rawTx.Meta.Err != nil { + continue + } + parsedTx, err := solana_parser.ParseRawTx(rawTx) + if err != nil { + fmt.Println("parse tx error:", i, rawTx.TxHash(), err) + break + } + txs = append(txs, parsedTx) + } + for _, result := range txs { + swapsLen := len(result.Swaps) + for i := 0; i < swapsLen; i++ { + action := result.Swaps[i] + var actions []solana_parser.Swap = make([]solana_parser.Swap, 0, 2) + actions = append(actions, action) + if i+1 < swapsLen { + nextAction := result.Swaps[i+1] + if action.Event == "buy" && nextAction.Event == "complete" && + action.Program == solana_parser.SolProgramPump && + nextAction.Program == solana_parser.SolProgramPump && + action.BaseMint == nextAction.BaseMint { + actions = append(actions, nextAction) + i++ + } + if action.Event == "migrate" && nextAction.Event == "create" && + action.Program == solana_parser.SolProgramPump && + nextAction.Program == solana_parser.SolProgramPumpAMM && + action.BaseMint == nextAction.BaseMint { + actions = append(actions, nextAction) + i++ + } + } + if err = HandleAction(context.Background(), result, actions, data); err != nil { + //h.logger.Errorf("handle action error: %s - %v", result.RawTx.Transaction.Signatures[0].String(), err) + fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err) + } + } + if result.GetTxHash() == "4h3yrAfMfHYHgf2DBnaecRjuSw4UTirySej65PSapPPPASvBADo143NhptQyVQdiCKypoSs2tzh3EhYxcgxVNLHD" { + fmt.Println("xxx") + } + } + fmt.Println("slot", slot, "tx count: ", len(data.Txs)) + +} + +var ( + meteoraDammV2Program = solana.MustPublicKeyFromBase58("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG") + raydiumCPmmProgramID = solana.MustPublicKeyFromBase58("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C") +) + +func HandleAction(ctx context.Context, tx *solana_parser.Tx, swaps []solana_parser.Swap, data *BlockData) error { + swapLen := len(swaps) + if len(swaps) == 0 { + return nil + } + if swaps[0].BaseMint != solana_parser.WSOL && swaps[0].QuoteMint != solana_parser.WSOL && !swaps[0].QuoteMint.IsZero() { + return nil + } + + if len(swaps) == 0 { + return nil + } + event := swaps[0].Event + swap := swaps[0] + action := SwapGetter{swap} + switch event { + case "buy", "sell": + + data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price)) + if swap.Program == solana_parser.SolProgramPump { + if swapLen == 2 && swaps[1].Event == "complete" { + t := pgtype.Timestamptz{} + t.Set(time.Unix(tx.BlockAt, 0)) + data.AppendAction(Action{ + Maker: swaps[1].User.String(), + Token: swaps[1].BaseMint.String(), + Pair: swaps[1].Pool.String(), + Action: "pump-migrate", + Block: tx.Block, + BlockAt: t, + TxHash: tx.GetTxHash(), + }) + } + } + return data.SetPair(action, tx.Block, "") + + case "create": + pair, err := action.GetPair(tx.Block, "") + if err != nil { + return err + } + data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price)) + data.Pairs[pair.Address] = *pair + case "add_liquidity", "remove_liquidity", "deposit", "withdraw", "add", "remove": + liquidityTx, err := action.GetLiquidityTx(tx, uint64(swap.TxIndex)) + if liquidityTx == nil { + return err + } + data.AppendTx(*liquidityTx) + return data.SetPair(action, tx.Block, "") + } + + if event != "migrate" { + return nil + } + if swap.Program == solana_parser.SolProgramPump { + t := pgtype.Timestamptz{} + t.Set(time.Unix(tx.BlockAt, 0)) + if swapLen == 2 && swaps[1].Event == "create" && swaps[1].Program == solana_parser.SolProgramPumpAMM && swaps[1].BaseMint == swap.BaseMint { + tokenMint := swap.BaseMint.String() + data.AppendAction(Action{ + Maker: swap.User.String(), + Token: tokenMint, + Pair: swaps[1].Pool.String(), + Action: "on-pumpswap", + Block: tx.Block, + BlockAt: t, + TxHash: tx.GetTxHash(), + }) + data.NewRaydium = append(data.NewRaydium, tokenMint) + } + } else if swap.Program == solana_parser.SolProgramRaydiumLaunchLab || swap.Program == solana_parser.SolProgramRaydiumLaunchLabBonk { + t := pgtype.Timestamptz{} + t.Set(time.Unix(tx.BlockAt, 0)) + var actionType string + if action.MigrateTopProgram == raydiumCPmmProgramID { + actionType = "on-raydium-cpmm" + } else { + actionType = "on-raydium-amm" + } + data.AppendAction(Action{ + Maker: action.User.String(), + Token: action.BaseMint.String(), + Pair: action.MigrateToPool.String(), + Action: actionType, + Block: tx.Block, + BlockAt: t, + TxHash: tx.GetTxHash(), + }) + } else if swap.Program == solana_parser.SolProgramMeteoraBondingCurve { + t := pgtype.Timestamptz{} + t.Set(time.Unix(tx.BlockAt, 0)) + var actionType string + if swap.MigrateTopProgram == meteoraDammV2Program { + actionType = "on-meteora-amm-v2" + } else { + actionType = "on-meteora-amm-v1" + } + data.AppendAction(Action{ + Maker: action.User.String(), + Token: action.BaseMint.String(), + Pair: action.MigrateToPool.String(), + Action: actionType, + Block: uint64(tx.Block), + BlockAt: t, + TxHash: tx.GetTxHash(), + }) + } + + return nil +} + +type Pair struct { + Id string `gorm:"column:id;primaryKey;default:uuid_generate_v4()"` + Address string + Name string + Token0 string + Token1 string + LpToken string + ChainId int64 + Reserve0 decimal.Decimal + Reserve1 decimal.Decimal + Block uint64 + BlockAt *pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at,omitempty"` + CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"created_at,omitempty"` + SortId uint64 + Program string + + IsCreate bool `gorm:"-"` + //TokenObj *Token `gorm:"-" json:"token_obj,omitempty"` + UpdateSlot uint64 `gorm:"-"` + InDB bool `gorm:"-"` +} + +type Tx struct { + Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"` + PairAddress string `json:"pair_address"` + Maker string `json:"maker"` + Token0Address string `json:"token0_address"` + Token1Address string `json:"token1_address"` + Token0Amount decimal.Decimal `json:"token0Amount" gorm:"column:token0_amount;type:numeric"` + Token1Amount decimal.Decimal `json:"token1Amount" gorm:"column:token1_amount;type:numeric"` + PriceUsd decimal.Decimal `json:"price_usd" gorm:"column:price_usd;type:numeric"` + AmountUsd decimal.Decimal `json:"amount_usd" gorm:"column:amount_usd;type:numeric"` + Block uint64 `json:"block"` + BlockIndex uint64 `json:"index"` + Event string `json:"event"` + TxHash string `json:"tx_hash"` + TxIndex uint64 `json:"topic_index"` + Program string `json:"program"` + BlockAt pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at"` + CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"` + TotalSupply string `gorm:"total_supply"` + AfterReserve0 string `gorm:"after_reserve0"` + AfterReserve1 string `gorm:"after_reserve1"` + PositionChange int64 `gorm:"position_change"` + Platform string `gorm:"column:tx_platform;type:platform;default:'none'" json:"tx_platform"` + PlatformFee decimal.Decimal `gorm:"-" json:"-"` // TODO: save to db + CUPrice decimal.Decimal `gorm:"column:tx_cu_price;type:numeric" json:"tx_cu_price"` + MevAgent string `gorm:"column:tx_mev_agent;type:mev_agent;default:'none'" json:"tx_mev_agent"` + MevAgentFee decimal.Decimal `gorm:"column:tx_mev_agent_fee;type:numeric" json:"tx_mev_agent_fee"` + AfterSOLBalance decimal.Decimal `gorm:"column:after_sol_balance;type:numeric" json:"after_sol_balance"` + EntryContract string `gorm:"column:tx_entry_contract;type:entry_contract;default:'none'" json:"tx_entry_contract"` +} + +type Action struct { + Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"` + Maker string `json:"maker"` + Token string `json:"token"` + Pair string `json:"pair"` + Action string `json:"action"` + Block uint64 `json:"block"` + BlockAt pgtype.Timestamptz `json:"block_at"` + TxHash string `json:"tx_hash"` + CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"` +} + +type BlockData struct { + Pairs map[string]Pair + Txs []Tx + Actions []Action + Price decimal.Decimal + NewRaydium []string +} + +func NewBlockData(price decimal.Decimal) *BlockData { + return &BlockData{ + Pairs: make(map[string]Pair), + Txs: make([]Tx, 0), + Actions: make([]Action, 0), + Price: price, + NewRaydium: make([]string, 0), + } +} + +func (bd *BlockData) AppendTx(tx Tx) { + bd.Txs = append(bd.Txs, tx) +} + +func (bd *BlockData) AppendAction(action Action) { + bd.Actions = append(bd.Actions, action) +} + +func (bd *BlockData) SetPair(action SwapGetter, block uint64, _ string) error { + pair, err := action.GetPair(block, "") + if err != nil { + return err + } + bd.Pairs[pair.Address] = *pair + return nil +} + +type SwapGetter struct { + solana_parser.Swap +} + +const ( + PositionChangeNone = int64(iota) + PositionChangeNewBuy + PositionChangeBuyMore + PositionChangeSellPart + PositionChangeSellAll +) + +func (spg SwapGetter) GetLiquidityTx(tx *solana_parser.Tx, index uint64) (*Tx, error) { + if spg.BaseMint != solana.WrappedSol && spg.QuoteMint != solana.WrappedSol { + return nil, nil + } + + var ( + token0 string + amount0 decimal.Decimal + amount1 decimal.Decimal + pool0 decimal.Decimal + pool1 decimal.Decimal + + event string + ) + + if spg.BaseMint == solana.WrappedSol { + amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + token0 = spg.QuoteMint.String() + pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + } else { + amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + token0 = spg.BaseMint.String() + pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + } + if spg.Event == "deposit" || spg.Event == "add" || spg.Event == "add_liquidity" || spg.Event == "add_liquidity_one_side" { + event = "add" + } else if spg.Event == "withdraw" || spg.Event == "remove" || spg.Event == "remove_liquidity" || spg.Event == "remove_liquidity_one_side" { + event = "remove" + } + if event == "" { + return nil, nil + } + + mevName, mevFee := tx.CheckMevAgent() + platformName, platformFee := tx.CheckPlatform(spg.Swap) + + pairString := "" + if spg.Program == solana_parser.SolProgramPump { + pairString = spg.BaseMint.String() + } else { + pairString = spg.Pool.String() + } + t := pgtype.Timestamptz{} + _ = t.Set(time.Unix(tx.BlockAt, 0)) + return &Tx{ + PairAddress: pairString, + Maker: spg.User.String(), + Token0Address: token0, + Token1Address: "So11111111111111111111111111111111111111112", + Token0Amount: amount0, + Token1Amount: amount1, + Block: tx.Block, + BlockIndex: tx.BlockIndex, + Event: event, + TxHash: tx.GetTxHash(), + TxIndex: index, + BlockAt: t, + Program: spg.Program, + AfterReserve0: pool0.String(), + AfterReserve1: pool1.String(), + Platform: platformName, + PlatformFee: platformFee, + CUPrice: tx.CUPrice, + MevAgent: mevName, + MevAgentFee: mevFee, + AfterSOLBalance: spg.AfterSOLBalance, + EntryContract: spg.CheckEntryContract(), + }, nil +} + +func (spg SwapGetter) GetTx(tx *solana_parser.Tx, index uint64, price decimal.Decimal) Tx { + var ( + token0 string + amount0 decimal.Decimal + amount1 decimal.Decimal + pool0 decimal.Decimal + pool1 decimal.Decimal + + event string + ) + + if spg.BaseMint == solana.WrappedSol { + amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + token0 = spg.QuoteMint.String() + pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + if spg.Event == "buy" { + event = "sell" + } else if spg.Event == "sell" { + event = "buy" + } + } else { + amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + token0 = spg.BaseMint.String() + pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + event = spg.Event + } + + priceUsd := decimal.Zero + if amount0.GreaterThan(priceUsd) { + priceUsd = amount1.Div(amount0).Mul(price) + } + pc := PositionChangeNone + if event == "buy" { + pc = PositionChangeNewBuy + if spg.BaseMint == solana.WrappedSol { + if spg.UserQuoteBalance.GreaterThan(spg.QuoteAmount) { + pc = PositionChangeBuyMore + } + } else { + if spg.UserBaseBalance.GreaterThan(spg.BaseAmount) { + pc = PositionChangeBuyMore + } + } + } else if event == "sell" { + pc = PositionChangeSellPart + if spg.BaseMint == solana.WrappedSol { + if spg.UserQuoteBalance.Div(decimal.New(1, int32(spg.QuoteMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) { + pc = PositionChangeSellAll + } + } else { + if spg.UserBaseBalance.Div(decimal.New(1, int32(spg.BaseMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) { + pc = PositionChangeSellAll + } + } + } + + mevName, mevFee := tx.CheckMevAgent() + platformName, platformFee := tx.CheckPlatformOnSig(spg.Swap) + + if mevName == "" { + mevName = "none" + } + if mevName == "unknown" { + mevName = "none" + mevFee = decimal.Zero + } + pairString := "" + if spg.Program == solana_parser.SolProgramPump { + pairString = spg.BaseMint.String() + } else { + pairString = spg.Pool.String() + } + t := pgtype.Timestamptz{} + _ = t.Set(time.Unix(tx.BlockAt, 0)) + + return Tx{ + PairAddress: pairString, + Maker: spg.User.String(), + Token0Address: token0, + Token1Address: "So11111111111111111111111111111111111111112", + Token0Amount: amount0, + Token1Amount: amount1, + PriceUsd: priceUsd, + AmountUsd: amount1.Mul(price), + Block: tx.Block, + BlockIndex: tx.BlockIndex, + Event: event, + TxHash: tx.GetTxHash(), + TxIndex: index, + BlockAt: t, + Program: spg.Program, + AfterReserve0: pool0.String(), + AfterReserve1: pool1.String(), + PositionChange: pc, + Platform: platformName, + PlatformFee: platformFee, + CUPrice: tx.CUPrice, + MevAgent: mevName, + MevAgentFee: mevFee, + AfterSOLBalance: spg.AfterSOLBalance, + EntryContract: spg.CheckEntryContract(), + } +} + +func (spg SwapGetter) GetPair(slot uint64, _ string) (*Pair, error) { + //pump amm + if spg.Program == solana_parser.SolProgramPump { + tokenMint := spg.BaseMint.String() + return &Pair{ + Address: tokenMint, + Token0: tokenMint, + Token1: "So11111111111111111111111111111111111111112", + ChainId: 900, + Reserve0: spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))), + Reserve1: spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))), + IsCreate: spg.Event == "create", + Program: spg.Program, + UpdateSlot: slot, + }, nil + } else { + var ( + token0 string + amount0 decimal.Decimal + amount1 decimal.Decimal + ) + if spg.BaseMint.IsZero() || spg.QuoteMint.IsZero() { + return nil, errors.New("base mint or quote mint is empty") + } + + if spg.BaseMint == solana.WrappedSol { + amount0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + amount1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + //decimal0 = spg.QuoteMintDecimals + token0 = spg.QuoteMint.String() + } else { + amount0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) + amount1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) + //decimal0 = a.BaseDecimals + token0 = spg.BaseMint.String() + } + + return &Pair{ + Address: spg.Pool.String(), + LpToken: spg.LpMint.String(), + Token0: token0, + Token1: "So11111111111111111111111111111111111111112", + ChainId: 900, + Reserve0: amount0, + Reserve1: amount1, + IsCreate: false, + Program: spg.Program, + UpdateSlot: slot, + }, nil + } +} + +func getBlockTxsFromDb(db *gorm.DB, block uint64) ([]Tx, error) { + var txs []Tx + result := db.Table("tx").Where("block = ?", block).Find(&txs) + return txs, result.Error +} + +func getBlockActionsFromDb(db *gorm.DB, block uint64) ([]Action, error) { + var txs []Action + result := db.Table("action").Where("block = ?", block).Find(&txs) + return txs, result.Error +} + +type dbLog struct { + logger *slog.Logger +} + +func (l *dbLog) Printf(format string, args ...interface{}) { + l.logger.Info(fmt.Sprintf(format, args...)) +} + +func newDbLog() *dbLog { + return &dbLog{logger: slog.Default()} +} + +func NewGorm(dsn string) *gorm.DB { + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ + Logger: logger.New(newDbLog(), logger.Config{ + Colorful: false, + LogLevel: logger.Warn, + SlowThreshold: time.Second * 10, + IgnoreRecordNotFoundError: true, + }), + }) + if err != nil { + panic(err) + } + + return db +} + +func compareTxs(dbTxs []Tx, dataTxs []Tx) (diff int, missing int) { + dataByHash := make(map[string][]Tx, len(dataTxs)) + for _, tx := range dataTxs { + dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)] = append(dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)], tx) + } + + for _, dbTx := range dbTxs { + candidates := dataByHash[fmt.Sprintf("%s-%d", dbTx.TxHash, dbTx.TxIndex)] + if len(candidates) == 0 { + missing++ + log.Printf("missing tx: %s", txCompareString(dbTx)) + continue + } + matched := false + for _, dataTx := range candidates { + if txEqualWithoutHash(dbTx, dataTx) { + matched = true + break + } + } + if !matched { + diff++ + log.Printf("tx diff hash=%s, program=%s, event:%s: %s, ", dbTx.TxHash, dbTx.Program, dbTx.Event, txCompareDiffString(dbTx, candidates[0])) + } + } + log.Printf("compare txs done: db=%d parsed=%d missing=%d diff=%d", len(dbTxs), len(dataTxs), missing, diff) + return diff, missing +} + +func withinOnePercentDecimal(a decimal.Decimal, b decimal.Decimal) bool { + if a.IsZero() { + return b.IsZero() + } + diff := a.Sub(b).Abs() + threshold := a.Abs().Mul(decimal.NewFromFloat(0.03)) + return diff.LessThanOrEqual(threshold) +} + +func withinOnePercentStringDecimal(a string, b string) bool { + ad, errA := decimal.NewFromString(a) + bd, errB := decimal.NewFromString(b) + if errA != nil || errB != nil { + return a == b + } + return withinOnePercentDecimal(ad, bd) +} + +func txEqualWithoutHash(a Tx, b Tx) bool { + //mevMatch := a.MevAgent == b.MevAgent || (a.MevAgent == "none" && b.MevAgent == "unknown") || (a.MevAgent == "unknown" && b.MevAgent == "none") + //mevNone := a.MevAgent == "none" || a.MevAgent == "unknown" + + return a.PairAddress == b.PairAddress && + a.Token1Address == b.Token1Address && + (a.Token0Address == "" || a.Token0Address == b.Token0Address) && + //a.Maker == b.Maker && + (a.Token0Address == "" || withinOnePercentDecimal(a.Token0Amount, b.Token0Amount)) && + withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) && + a.Block == b.Block && + a.BlockIndex == b.BlockIndex && + a.Event == b.Event && + a.TxIndex == b.TxIndex && + a.Program == b.Program && + (a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0)) && + (a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1)) && + // a.PositionChange == b.PositionChange && + (a.Platform == b.Platform || (a.Platform == "photon" && b.Platform == "fake") || (a.Platform == "trojan" && b.Platform == "fake")) && + a.CUPrice.String() == b.CUPrice.String() // && + //mevMatch && + //(mevNone || a.MevAgentFee.String() == b.MevAgentFee.String()) && + //(a.Program == solana_parser.SolProgramRaydiumV4 || a.AfterSOLBalance.String() == b.AfterSOLBalance.String()) + //&& + // a.EntryContract == b.EntryContract +} + +func txCompareDiffString(a Tx, b Tx) string { + var diffs []string + if a.PairAddress != b.PairAddress { + diffs = append(diffs, fmt.Sprintf("PairAddress db=%s data=%s", a.PairAddress, b.PairAddress)) + } + //if a.Maker != b.Maker { + // diffs = append(diffs, fmt.Sprintf("Maker db=%s, data=%s", a.Maker, b.Maker)) + //} + if a.Token1Address != b.Token1Address { + diffs = append(diffs, fmt.Sprintf("Token1Address db=%s data=%s", a.Token1Address, b.Token1Address)) + } + if a.Token0Address != b.Token0Address { + diffs = append(diffs, fmt.Sprintf("Token0Address db=%s data=%s", a.Token0Address, b.Token0Address)) + } + if !withinOnePercentDecimal(a.Token0Amount, b.Token0Amount) { + diffs = append(diffs, fmt.Sprintf("Token0Amount db=%s data=%s", a.Token0Amount.String(), b.Token0Amount.String())) + } + if !withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) { + diffs = append(diffs, fmt.Sprintf("Token1Amount db=%s data=%s", a.Token1Amount.String(), b.Token1Amount.String())) + } + if a.Block != b.Block { + diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block)) + } + if a.BlockIndex != b.BlockIndex { + diffs = append(diffs, fmt.Sprintf("BlockIndex db=%d data=%d", a.BlockIndex, b.BlockIndex)) + } + if a.Event != b.Event { + diffs = append(diffs, fmt.Sprintf("Event db=%s data=%s", a.Event, b.Event)) + } + if a.TxIndex != b.TxIndex { + diffs = append(diffs, fmt.Sprintf("TxIndex db=%d data=%d", a.TxIndex, b.TxIndex)) + } + if a.Program != b.Program { + diffs = append(diffs, fmt.Sprintf("Program db=%s data=%s", a.Program, b.Program)) + } + if !withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0) { + diffs = append(diffs, fmt.Sprintf("AfterReserve0 db=%s data=%s", a.AfterReserve0, b.AfterReserve0)) + } + if !withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1) { + diffs = append(diffs, fmt.Sprintf("AfterReserve1 db=%s data=%s", a.AfterReserve1, b.AfterReserve1)) + } + //if a.PositionChange != b.PositionChange { + // diffs = append(diffs, fmt.Sprintf("PositionChange db=%d data=%d", a.PositionChange, b.PositionChange)) + //} + if a.Platform != b.Platform { + diffs = append(diffs, fmt.Sprintf("Platform db=%s data=%s", a.Platform, b.Platform)) + } + if a.CUPrice.String() != b.CUPrice.String() { + diffs = append(diffs, fmt.Sprintf("CUPrice db=%s data=%s", a.CUPrice.String(), b.CUPrice.String())) + } + //if a.MevAgent != b.MevAgent { + // diffs = append(diffs, fmt.Sprintf("MevAgent db=%s data=%s", a.MevAgent, b.MevAgent)) + //} + //if a.MevAgentFee.String() != b.MevAgentFee.String() { + // diffs = append(diffs, fmt.Sprintf("MevAgentFee db=%s data=%s", a.MevAgentFee.String(), b.MevAgentFee.String())) + //} + //if a.AfterSOLBalance.String() != b.AfterSOLBalance.String() { + // diffs = append(diffs, fmt.Sprintf("AfterSOLBalance db=%s data=%s", a.AfterSOLBalance.String(), b.AfterSOLBalance.String())) + //} + //if a.EntryContract != b.EntryContract { + // diffs = append(diffs, fmt.Sprintf("EntryContract db=%s data=%s", a.EntryContract, b.EntryContract)) + //} + return strings.Join(diffs, "; ") +} + +func compareActions(dbActions []Action, dataActions []Action) (diff, missing int) { + dataByHash := make(map[string][]Action, len(dataActions)) + for _, action := range dataActions { + dataByHash[action.TxHash] = append(dataByHash[action.TxHash], action) + } + + for _, dbAction := range dbActions { + candidates := dataByHash[dbAction.TxHash] + if len(candidates) == 0 { + missing++ + log.Printf("missing action: %s", actionCompareString(dbAction)) + continue + } + matched := false + for _, dataAction := range candidates { + if actionEqualWithoutHash(dbAction, dataAction) { + matched = true + break + } + } + if !matched { + diff++ + log.Printf("action diff hash=%s: %s", dbAction.TxHash, actionCompareDiffString(dbAction, candidates[0])) + } + } + log.Printf("compare actions done: db=%d parsed=%d missing=%d diff=%d", len(dbActions), len(dataActions), missing, diff) + return diff, missing +} + +func actionEqualWithoutHash(a Action, b Action) bool { + return a.Maker == b.Maker && + a.Token == b.Token && + a.Pair == b.Pair && + a.Action == b.Action && + a.Block == b.Block +} + +func actionCompareDiffString(a Action, b Action) string { + var diffs []string + if a.Maker != b.Maker { + diffs = append(diffs, fmt.Sprintf("Maker db=%s data=%s", a.Maker, b.Maker)) + } + if a.Token != b.Token { + diffs = append(diffs, fmt.Sprintf("Token db=%s data=%s", a.Token, b.Token)) + } + if a.Pair != b.Pair { + diffs = append(diffs, fmt.Sprintf("Pair db=%s data=%s", a.Pair, b.Pair)) + } + if a.Action != b.Action { + diffs = append(diffs, fmt.Sprintf("Action db=%s data=%s", a.Action, b.Action)) + } + if a.Block != b.Block { + diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block)) + } + return strings.Join(diffs, "; ") +} + +func actionCompareString(action Action) string { + return fmt.Sprintf("Maker=%s Token=%s Pair=%s Action=%s Block=%d TxHash=%s", action.Maker, action.Token, action.Pair, action.Action, action.Block, action.TxHash) +} + +func txCompareString(tx Tx) string { + return fmt.Sprintf( + "tx.Program=%s TxHash=%s PairAddress=%s Token1Address=%s Token0Amount=%s Token1Amount=%s Block=%d BlockIndex=%d Event=%s TxIndex=%d AfterReserve0=%s AfterReserve1=%s PositionChange=%d Platform=%s CUPrice=%s MevAgent=%s MevAgentFee=%s AfterSOLBalance=%s EntryContract=%s", + tx.Program, + tx.TxHash, + tx.PairAddress, + tx.Token1Address, + tx.Token0Amount.String(), + tx.Token1Amount.String(), + tx.Block, + tx.BlockIndex, + tx.Event, + tx.TxIndex, + tx.AfterReserve0, + tx.AfterReserve1, + tx.PositionChange, + tx.Platform, + tx.CUPrice.String(), + tx.MevAgent, + tx.MevAgentFee.String(), + tx.AfterSOLBalance.String(), + tx.EntryContract, + ) +} diff --git a/meta.go b/meta.go index 22a4547..3e8f99a 100644 --- a/meta.go +++ b/meta.go @@ -35,11 +35,12 @@ var pumpMigrateEventDiscriminator = calculateDiscriminator("event:CompletePumpAm var pumpBuyEventDiscriminator = [8]byte{189, 219, 127, 211, 78, 230, 97, 238} var ( - pumpAmmProgram = solana.MustPublicKeyFromBase58("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") - wSolMint = solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112") - usdcMint = solana.MustPublicKeyFromBase58("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") - usd1Mint = solana.MustPublicKeyFromBase58("USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB") - meteoraDlmmProgram = solana.MustPublicKeyFromBase58("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo") + pumpAmmProgram = solana.MustPublicKeyFromBase58("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") + wSolMint = solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112") + usdcMint = solana.MustPublicKeyFromBase58("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") + usd1Mint = solana.MustPublicKeyFromBase58("USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB") + meteoraDlmmProgram = solana.MustPublicKeyFromBase58("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo") + meteoraDammV2Program = solana.MustPublicKeyFromBase58("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG") ) var ( @@ -88,6 +89,141 @@ var ( meteoraDlmmRemoveLiquidityEventDiscriminator = calculateDiscriminator("event:RemoveLiquidity") ) +var ( + // metaora pool + metaoraPoolProgramID = solana.MustPublicKeyFromBase58("Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB") + + metaoraPoolInitializePermissionedPoolDiscriminator = calculateDiscriminator("global:initialize_permissioned_pool") + metaoraPoolInitializePermissionlessPoolDiscriminator = calculateDiscriminator("global:initialize_permissionless_pool") + metaoraPoolInitializePermissionlessPoolWithFeeTierDiscriminator = calculateDiscriminator("global:initialize_permissionless_pool_with_fee_tier") + metaoraPoolInitializePermissionlessConstantProductPoolWithConfigDiscriminator = calculateDiscriminator("global:initialize_permissionless_constant_product_pool_with_config") + metaoraPoolInitializePermissionlessConstantProductPoolWithConfig2Discriminator = calculateDiscriminator("global:initialize_permissionless_constant_product_pool_with_config2") + metaoraPoolInitializeCustomizablePermissionlessConstantProductPoolDiscriminator = calculateDiscriminator("global:initialize_customizable_permissionless_constant_product_pool") + + metaoraPoolSwapDiscriminator = calculateDiscriminator("global:swap") + metaoraPoolAddImbalanceLiquidityDiscriminator = calculateDiscriminator("global:add_imbalance_liquidity") + metaoraPoolAddBalanceLiquidityDiscriminator = calculateDiscriminator("global:add_balance_liquidity") + metaoraPoolRemoveLiquiditySingleSideDiscriminator = calculateDiscriminator("global:remove_liquidity_single_side") + metaoraPoolRemoveBalanceLiquidityDiscriminator = calculateDiscriminator("global:remove_balance_liquidity") + metaoraPoolClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee") + metaoraPoolBootstrapLiquidityDiscriminator = calculateDiscriminator("global:bootstrap_liquidity") +) + +var ( + metaoraBcProgramID = solana.MustPublicKeyFromBase58("dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN") + + metaoraBcInitializedPoolDiscriminator = calculateDiscriminator("global:initialize_virtual_pool_with_spl_token") + metaoraBcInitialize2022PoolDiscriminator = calculateDiscriminator("global:initialize_virtual_pool_with_token2022") + metaoraBcMigrateMeteoraDammDiscriminator = calculateDiscriminator("global:migrate_meteora_damm") + metaoraBcMigrateMeteoraDammV2Discriminator = calculateDiscriminator("global:migration_damm_v2") + metaoraBcSwapDiscriminator = calculateDiscriminator("global:swap") + metaoraBcSwapV2Discriminator = calculateDiscriminator("global:swap2") + metaoraBcEventInitializePoolDiscriminator = [8]byte{228, 50, 246, 85, 203, 66, 134, 37} + metaoraBcEventSwapDiscriminator = [8]byte{27, 60, 21, 213, 138, 170, 187, 147} + metaoraBcEventSwap2Discriminator = [8]byte{189, 66, 51, 168, 38, 80, 117, 153} + metaoraBcEventCompleteDiscriminator = [8]byte{229, 231, 86, 84, 156, 134, 75, 24} +) + +var ( + meteoraDammV2AddLiquidityDiscriminator = calculateDiscriminator("global:add_liquidity") + meteoraDammV2RemoveLiquidityDiscriminator = calculateDiscriminator("global:remove_liquidity") + meteoraDammV2RemoveAllLiquidityDiscriminator = calculateDiscriminator("global:remove_all_liquidity") + meteoraDammV2SwapDiscriminator = calculateDiscriminator("global:swap") + meteoraDammV2SwapV2Discriminator = calculateDiscriminator("global:swap2") + meteoraDammV2InitializeCustomizablePoolDiscriminator = calculateDiscriminator("global:initialize_customizable_pool") + meteoraDammV2InitializePoolWithDynamicConfig = calculateDiscriminator("global:initialize_pool_with_dynamic_config") + meteoraDammV2InitializePoolDiscriminator = calculateDiscriminator("global:initialize_pool") +) + +var ( + orcaProgramID = solana.MustPublicKeyFromBase58("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc") + + orcaInitializePoolDiscriminator = calculateDiscriminator("global:initialize_pool") + orcaInitializePoolV2Discriminator = calculateDiscriminator("global:initialize_pool_v2") + orcaInitializePoolWithAdaptiveFeeDiscriminator = calculateDiscriminator("global:initialize_pool_with_adaptive_fee") + + orcaIncreaseLiquidityDiscriminator = calculateDiscriminator("global:increase_liquidity") + orcaDecreaseLiquidityDiscriminator = calculateDiscriminator("global:decrease_liquidity") + orcaDecreaseLiquidityV2Discriminator = calculateDiscriminator("global:decrease_liquidity_v2") + orcaIncreaseLiquidityV2Discriminator = calculateDiscriminator("global:increase_liquidity_v2") + + orcaCollectFeesDiscriminator = calculateDiscriminator("global:collect_fees") + orcaCollectProtocolFeesDiscriminator = calculateDiscriminator("global:collect_protocol_fees") + orcaCollectFeesV2Discriminator = calculateDiscriminator("global:collect_fees_v2") + orcaCollectProtocolFeesV2Discriminator = calculateDiscriminator("global:collect_protocol_fees_v2") + + orcaSwapDiscriminator = calculateDiscriminator("global:swap") + orcaTwoHopSwapDiscriminator = calculateDiscriminator("global:two_hop_swap") + orcaSwapV2Discriminator = calculateDiscriminator("global:swap_v2") + orcaTwoHopSwapV2Discriminator = calculateDiscriminator("global:two_hop_swap_v2") +) + +var ( + raydiumClmmProgramID solana.PublicKey = solana.MustPublicKeyFromBase58("CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK") + + raydiumClmmCreatePoolDiscriminator = calculateDiscriminator("global:create_pool") + raydiumClmmCollectProtocolFeeDiscriminator = calculateDiscriminator("global:collect_protocol_fee") + raydiumClmmCollectFundFeeDiscriminator = calculateDiscriminator("global:collect_fund_fee") + raydiumClmmOpenPositionDiscriminator = calculateDiscriminator("global:open_position") + raydiumClmmOpenPositionV2Discriminator = calculateDiscriminator("global:open_position_v2") + raydiumClmmOpenPositionWithToken22NftDiscriminator = calculateDiscriminator("global:open_position_with_token22_nft") + + raydiumClmmIncreaseLiquidityDiscriminator = calculateDiscriminator("global:increase_liquidity") + raydiumClmmDecreaseLiquidityDiscriminator = calculateDiscriminator("global:decrease_liquidity") + raydiumClmmIncreaseLiquidityV2Discriminator = calculateDiscriminator("global:increase_liquidity_v2") + raydiumClmmDecreaseLiquidityV2Discriminator = calculateDiscriminator("global:decrease_liquidity_v2") + raydiumClmmSwapDiscriminator = calculateDiscriminator("global:swap") + raydiumClmmSwapV2Discriminator = calculateDiscriminator("global:swap_v2") +) + +var ( + raydiumCPmmProgramID = solana.MustPublicKeyFromBase58("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C") + + raydiumCPmmSwapBaseInputDiscriminator = [8]byte{143, 190, 90, 218, 196, 30, 51, 222} + raydiumCPmmSwapBaseOutputDiscriminator = [8]byte{55, 217, 98, 86, 163, 74, 180, 173} + + raydiumCPmmWithdrawDiscriminator = [8]byte{183, 18, 70, 156, 148, 109, 161, 34} + + raydiumCPmmDepositDiscriminator = [8]byte{242, 35, 198, 137, 82, 225, 242, 182} + + raydiumCPmmCollectProtocolFeeDiscriminator = [8]byte{136, 136, 252, 221, 194, 66, 126, 89} + raydiumCPmmCollectFundFeeDiscriminator = [8]byte{167, 138, 78, 149, 223, 194, 6, 126} + + raydiumCPmmInitializeDiscriminator = [8]byte{175, 175, 109, 31, 13, 152, 155, 237} + raydiumCPmmInitializeWithPermissionDiscriminator = calculateDiscriminator("global:initialize_with_permission") +) + +var ( + raydiumV4Program = solana.MustPublicKeyFromBase58("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8") +) + +const ( + raydiumV4InitializePoolDiscriminator = uint8(1) + + raydiumV4SwapBaseInDiscriminator = uint8(9) + raydiumV4SwapBaseOutDiscriminator = uint8(11) + + raydiumV4AddLiquidityDiscriminator = uint8(3) + raydiumV4RemoveLiquidityDiscriminator = uint8(4) + raydiumV4WithdrawPNLDiscriminator = uint8(7) +) + +var ( + raydiumLaunchLabProgramID = solana.MustPublicKeyFromBase58("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj") + bonkPlatformConfig = solana.MustPublicKeyFromBase58("FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1") + + raydiumLaunchLabCreatePoolEvnet = [8]byte{151, 215, 226, 9, 118, 161, 115, 174} + raydiumLaunchLabTradeEvnet = [8]byte{189, 219, 127, 211, 78, 230, 97, 238} + raydiumLaunchLabInitializeV2PoolDiscriminator = [8]byte{67, 153, 175, 39, 218, 16, 38, 32} + raydiumLaunchLabInitializeWithToken2022PoolDiscriminator = [8]byte{37, 190, 126, 222, 44, 154, 171, 17} + raydiumLaunchLabSellExactInDiscriminator = [8]byte{0x95, 0x27, 0xde, 0x9b, 0xd3, 0x7c, 0x98, 0x1a} + raydiumLaunchLabSellExactOutDiscriminator = [8]byte{0x5f, 0xc8, 0x47, 0x22, 0x08, 0x09, 0x0b, 0xa6} + raydiumLaunchLabBuyExactInDiscriminator = [8]byte{0xfa, 0xea, 0x0d, 0x7b, 0xd5, 0x9c, 0x13, 0xec} + raydiumLaunchLabBuyExactOutDiscriminator = [8]byte{0x18, 0xd3, 0x74, 0x28, 0x69, 0x03, 0x99, 0x38} + raydiumLaunchLabMigrateToAmmDiscriminator = [8]byte{0xcf, 0x52, 0xc0, 0x91, 0xfe, 0xcf, 0x91, 0xdf} + raydiumLaunchLabMigrateToCpmmDiscriminator = [8]byte{0x88, 0x5c, 0xc8, 0x67, 0x1c, 0xda, 0x90, 0x8c} +) + // Program PumpAmm program ID var budgGetProgram = solana.MustPublicKeyFromBase58("ComputeBudget111111111111111111111111111111") @@ -99,6 +235,4 @@ var createAccountWithSeedDiscriminator = uint32(3) var systemProgram = solana.MustPublicKeyFromBase58("11111111111111111111111111111111") -var raydiumLaunchLabProgramID = solana.MustPublicKeyFromBase58("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj") - var eventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29} diff --git a/metaoradlmm.go b/metaoradlmm.go index 9104dda..1b829ac 100644 --- a/metaoradlmm.go +++ b/metaoradlmm.go @@ -193,7 +193,6 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI } decode := instruction.Data if len(decode) < 8 { - offset[1] += 1 return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm program instruction data too short, offset, %d, %d", offset[0], offset[1]) } @@ -255,11 +254,11 @@ func metaoradlmmInitializeParser(tx *Tx, instruction Instruction, innerInstructi var programIndex = instruction.ProgramIDIndex for innerIndex, innerInstr := range inners { - if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraInitializeLbPairEventDiscriminator[:]) { + if innerInstr.ProgramIDIndex == programIndex && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraInitializeLbPairEventDiscriminator[:]) { if offset[1] == 0 { offset[0] += 1 } else { - offset[1] += uint(innerIndex) + 1 + prefixLen + offset[1] = uint(innerIndex) + 1 + prefixLen } break } @@ -281,7 +280,6 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In decode := instruction.Data if len(decode) < 8 { - offset[1] += 1 return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm swap instruction data too short, offset, %d, %d", offset[0], offset[1]) } @@ -321,11 +319,31 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In tokenXProgram := result.accountList[accounts.tokenXProgramIdx] tokenYProgram := result.accountList[accounts.tokenYProgramIdx] - swapEvent, nextOffset, err := dlmmSwapEventFromInnerInstructions(innerInstructions, instruction, offset) + var prefixLen = offset[1] + var swapEvent dlmmSwapEvent + inners, err := getInnerInstructions(innerInstructions, prefixLen) if err != nil { - return nil, nextOffset, err + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm swap get inner instructions error: %v, offset, %d, %d", err, offset[0], prefixLen) + } + for innerIndex, innerInstr := range inners { + if innerInstr.ProgramIDIndex != instruction.ProgramIDIndex { + continue + } + if len(innerInstr.Data) < 16 || !bytes.Equal(innerInstr.Data[:8], eventDiscriminator[:]) || !bytes.Equal(innerInstr.Data[8:16], meteoraDlmmSwapEventDiscriminator[:]) { + continue + } + + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(innerIndex) + 1 + prefixLen + } + + if err := agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&swapEvent); err != nil { + return nil, offset, fmt.Errorf("meteora dlmm swap event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + break } - offset = nextOffset baseMint, quoteMint, baseIsX := dlmmSelectBaseQuote(tokenXMint, tokenYMint) baseTokenProgram := tokenXProgram @@ -818,30 +836,6 @@ func dlmmSelectBaseQuote(tokenX, tokenY solana.PublicKey) (baseMint solana.Publi return tokenX, tokenY, true } -func dlmmSwapEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmSwapEvent, [2]uint, error) { - var prefixLen = offset[1] - inners, err := getInnerInstructions(innerInstructions, prefixLen) - if err != nil { - return dlmmSwapEvent{}, increaseOffset(offset), fmt.Errorf("meteora dlmm swap get inner instructions error: %v, offset, %d, %d", err, offset[0], prefixLen) - } - for innerIndex, innerInstr := range inners { - if innerInstr.ProgramIDIndex != instruction.ProgramIDIndex { - continue - } - event, ok := dlmmDecodeSwapEvent(innerInstr.Data) - if !ok { - continue - } - if offset[1] == 0 { - offset[0] += 1 - } else { - offset[1] += uint(innerIndex) + 1 + prefixLen - } - return event, offset, nil - } - return dlmmSwapEvent{}, increaseOffset(offset), fmt.Errorf("meteora dlmm swap event not found, offset, %d, %d", offset[0], prefixLen) -} - func dlmmAddLiquidityEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmAddLiquidityEvent, [2]uint, error) { var prefixLen = offset[1] inners, err := getInnerInstructions(innerInstructions, prefixLen) @@ -859,7 +853,7 @@ func dlmmAddLiquidityEventFromInnerInstructions(innerInstructions InnerInstructi if offset[1] == 0 { offset[0] += 1 } else { - offset[1] += uint(innerIndex) + 1 + prefixLen + offset[1] = uint(innerIndex) + 1 + prefixLen } return event, offset, nil } @@ -883,34 +877,13 @@ func dlmmRemoveLiquidityEventFromInnerInstructions(innerInstructions InnerInstru if offset[1] == 0 { offset[0] += 1 } else { - offset[1] += uint(innerIndex) + 1 + prefixLen + offset[1] = uint(innerIndex) + 1 + prefixLen } return event, offset, nil } return dlmmRemoveLiquidityEvent{}, increaseOffset(offset), fmt.Errorf("meteora dlmm remove liquidity event not found, offset, %d, %d", offset[0], prefixLen) } -func dlmmDecodeSwapEvent(data []byte) (dlmmSwapEvent, bool) { - switch { - case len(data) >= 8 && bytes.Equal(data[:8], meteoraDlmmSwapEventDiscriminator[:]): - var event dlmmSwapEvent - if err := agbinary.NewBorshDecoder(data[8:]).Decode(&event); err != nil { - return dlmmSwapEvent{}, false - } - return event, true - case len(data) >= 16 && - bytes.Equal(data[:8], eventDiscriminator[:]) && - bytes.Equal(data[8:16], meteoraDlmmSwapEventDiscriminator[:]): - var event dlmmSwapEvent - if err := agbinary.NewBorshDecoder(data[16:]).Decode(&event); err != nil { - return dlmmSwapEvent{}, false - } - return event, true - default: - return dlmmSwapEvent{}, false - } -} - func dlmmDecodeAddLiquidityEvent(data []byte) (dlmmAddLiquidityEvent, bool) { switch { case len(data) >= 8 && bytes.Equal(data[:8], meteoraDlmmAddLiquidityEventDiscriminator[:]): diff --git a/metaorapool.go b/metaorapool.go new file mode 100644 index 0000000..8bde0bf --- /dev/null +++ b/metaorapool.go @@ -0,0 +1,880 @@ +package pump_parser + +import ( + "bytes" + "encoding/binary" + "fmt" + + agbinary "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/shopspring/decimal" +) + +type metaoraPoolInitializePoolData struct { + TokenAAmount uint64 `json:"tokenAAmount"` + TokenBAmount uint64 `json:"tokenBAmount"` +} + +var ( + meteoraVaultProgram = solana.MustPublicKeyFromBase58("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi") + meteoraVaultDepositDiscriminator = []byte{0xf2, 0x23, 0xc6, 0x89, 0x52, 0xe1, 0xf2, 0xb6} + meteoraVaultWithdrawDiscriminator = []byte{0xb7, 0x12, 0x46, 0x9c, 0x94, 0x6d, 0xa1, 0x22} + + tokenProgramMintToDiscriminator = []byte{0x07} + tokenProgramTransferDiscriminator = []byte{0x03} + tokenProgramBurnDiscriminator = []byte{0x08} +) + +func metaoraPoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(metaoraPoolProgramID) { + return nil, increaseOffset(offset), fmt.Errorf("metaoraPool program instruction not found, offset, %d, %d", offset[0], offset[1]) + } + + decode := instruction.Data + if len(decode) < 8 { + return nil, increaseOffset(offset), fmt.Errorf("metaoraPool program instruction data too short, offset, %d, %d", offset[0], offset[1]) + } + + discriminator := *(*[8]byte)(decode[:8]) + + switch discriminator { + case metaoraPoolInitializePermissionlessConstantProductPoolWithConfigDiscriminator, + metaoraPoolInitializePermissionlessConstantProductPoolWithConfig2Discriminator: + return metaoraPoolInitializePermissionlessConstantProductPoolWithConfig(tx, instruction, innerInstructions, offset) + case metaoraPoolInitializePermissionlessPoolDiscriminator, + metaoraPoolInitializePermissionlessPoolWithFeeTierDiscriminator, + metaoraPoolInitializeCustomizablePermissionlessConstantProductPoolDiscriminator: + return metaoraPoolInitializePermissionlessPool(tx, instruction, innerInstructions, offset) + case metaoraPoolInitializePermissionedPoolDiscriminator: + return metaoraPoolInitializePermissionedPool(tx, instruction, innerInstructions, offset) + case metaoraPoolSwapDiscriminator: + return metaoraPoolSwap(tx, instruction, innerInstructions, offset) + case metaoraPoolAddImbalanceLiquidityDiscriminator, + metaoraPoolAddBalanceLiquidityDiscriminator, + metaoraPoolBootstrapLiquidityDiscriminator: + return metaoraPoolAddLiquidity(tx, instruction, innerInstructions, offset) + case metaoraPoolRemoveLiquiditySingleSideDiscriminator, + metaoraPoolRemoveBalanceLiquidityDiscriminator, + metaoraPoolClaimFeeDiscriminator: + return metaoraPoolRemoveLiquidity(tx, instruction, innerInstructions, offset) + default: + return nil, increaseOffset(offset), InstructionIgnoredError + } + +} + +// InitializePermissionlessConstantProductPoolWithConfig +// InitializePermissionlessConstantProductPoolWithConfig2 +func metaoraPoolInitializePermissionlessConstantProductPoolWithConfig(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + var data metaoraPoolInitializePoolData + err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&data) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize initialize pool data: %w", err) + } + + if len(instruction.Accounts) < 20 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction") + } + + tokenAMint := tx.rawTx.accountList[instruction.Accounts[3]] + tokenBMint := tx.rawTx.accountList[instruction.Accounts[4]] + + baseVaultAccountIndex := instruction.Accounts[7] + quoteVaultAccountIndex := instruction.Accounts[8] + baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) + quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) + + if baseVaultTokenBalance == nil || quoteVaultTokenBalance == nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token balances") + } + + baseReserve, err := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err) + } + + quoteReserve, err := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err) + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + + var meteoraVaultProgramId int + for i, acc := range tx.rawTx.accountList { + if acc.Equals(meteoraVaultProgram) { + meteoraVaultProgramId = i + break + } + } + + var baseFound, quoteFound bool + + if meteoraVaultProgramId > 0 { + for innerIndex, innerInstr := range inners { + if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraVaultDepositDiscriminator[:]) { + if len(innerInstr.Accounts) < 2 { + continue + } + if !baseFound && innerInstr.Accounts[1] == baseVaultAccountIndex { + baseFound = true + } + if !quoteFound && innerInstr.Accounts[1] == quoteVaultAccountIndex { + quoteFound = true + } + if baseFound && quoteFound { + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(innerIndex) + 1 + prefixLen + } + break + } + } + } + } + + return []Swap{ + { + Program: SolProgramMeteoraPools, + Event: "create", + Pool: tx.rawTx.accountList[instruction.Accounts[0]], + BaseMint: tokenAMint, + QuoteMint: tokenBMint, + BaseTokenProgram: baseVaultTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteVaultTokenBalance.ProgramIDAccount, + Creator: tx.rawTx.accountList[0], + BaseMintDecimals: uint8(baseVaultTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteVaultTokenBalance.UITokenAmount.Decimals), + User: tx.rawTx.accountList[instruction.Accounts[18]], + BaseAmount: decimal.NewFromUint64(data.TokenAAmount), + QuoteAmount: decimal.NewFromUint64(data.TokenBAmount), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + EntryContract: entryContract, + }, + }, offset, nil + +} + +// InitializePermissionlessPool +// InitializePermissionlessPoolWithFeeTier +func metaoraPoolInitializePermissionlessPool(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + // discriminator + tokenA amount + tokenB amount + if len(instruction.Data) < 24 { + return nil, increaseOffset(offset), fmt.Errorf("not enough data for initialize instruction") + } + + var data metaoraPoolInitializePoolData + err := agbinary.NewBorshDecoder(instruction.Data[len(instruction.Data)-16:]).Decode(&data) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize initialize pool data: %w", err) + } + + if len(instruction.Accounts) < 20 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction") + } + + tokenAMint := tx.rawTx.accountList[instruction.Accounts[2]] + tokenBMint := tx.rawTx.accountList[instruction.Accounts[3]] + + baseVaultAccountIndex := instruction.Accounts[6] + quoteVaultAccountIndex := instruction.Accounts[7] + baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) + quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) + + if baseVaultTokenBalance == nil || quoteVaultTokenBalance == nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token balances") + } + + baseReserve, err := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err) + } + + quoteReserve, err := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err) + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + + var meteoraVaultProgramId int + for i, acc := range tx.rawTx.accountList { + if acc.Equals(meteoraVaultProgram) { + meteoraVaultProgramId = i + break + } + } + + var baseFound, quoteFound bool + + if meteoraVaultProgramId > 0 { + for innerIndex, innerInstr := range inners { + if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraVaultDepositDiscriminator[:]) { + if len(innerInstr.Accounts) < 2 { + continue + } + if !baseFound && innerInstr.Accounts[1] == baseVaultAccountIndex { + baseFound = true + } + if !quoteFound && innerInstr.Accounts[1] == quoteVaultAccountIndex { + quoteFound = true + } + if baseFound && quoteFound { + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(innerIndex) + 1 + prefixLen + } + break + } + } + } + } + + return []Swap{ + { + Program: SolProgramMeteoraPools, + Event: "create", + Pool: tx.rawTx.accountList[instruction.Accounts[0]], + BaseMint: tokenAMint, + QuoteMint: tokenBMint, + BaseTokenProgram: baseVaultTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteVaultTokenBalance.ProgramIDAccount, + Creator: tx.rawTx.accountList[0], + BaseMintDecimals: uint8(baseVaultTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteVaultTokenBalance.UITokenAmount.Decimals), + User: tx.rawTx.accountList[instruction.Accounts[18]], + BaseAmount: decimal.NewFromUint64(data.TokenAAmount), + QuoteAmount: decimal.NewFromUint64(data.TokenBAmount), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + EntryContract: entryContract, + }, + }, offset, nil +} + +func metaoraPoolInitializePermissionedPool(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + + // discriminator + tokenA amount + tokenB amount + if len(instruction.Data) < 24 { + return nil, increaseOffset(offset), fmt.Errorf("not enough data for initialize instruction") + } + + var data metaoraPoolInitializePoolData + err := agbinary.NewBorshDecoder(instruction.Data[len(instruction.Data)-16:]).Decode(&data) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize initialize pool data: %w", err) + } + + if len(instruction.Accounts) < 20 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction") + } + + tokenAMint := tx.rawTx.accountList[instruction.Accounts[2]] + tokenBMint := tx.rawTx.accountList[instruction.Accounts[3]] + + baseVaultAccountIndex := instruction.Accounts[10] + quoteVaultAccountIndex := instruction.Accounts[11] + baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) + quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) + + if baseVaultTokenBalance == nil || quoteVaultTokenBalance == nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token balances") + } + + baseReserve, err := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err) + } + + quoteReserve, err := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err) + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + + var meteoraVaultProgramId int + for i, acc := range tx.rawTx.accountList { + if acc.Equals(meteoraVaultProgram) { + meteoraVaultProgramId = i + break + } + } + + var baseFound, quoteFound bool + + if meteoraVaultProgramId > 0 { + for innerIndex, innerInstr := range inners { + if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraVaultDepositDiscriminator[:]) { + if len(innerInstr.Accounts) < 2 { + continue + } + if !baseFound && innerInstr.Accounts[1] == baseVaultAccountIndex { + baseFound = true + } + if !quoteFound && innerInstr.Accounts[1] == quoteVaultAccountIndex { + quoteFound = true + } + if baseFound && quoteFound { + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(innerIndex) + 1 + prefixLen + } + break + } + } + } + } + + return []Swap{ + { + Program: SolProgramMeteoraPools, + Event: "create", + Pool: tx.rawTx.accountList[instruction.Accounts[0]], + BaseMint: tokenAMint, + QuoteMint: tokenBMint, + BaseTokenProgram: baseVaultTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteVaultTokenBalance.ProgramIDAccount, + Creator: tx.rawTx.accountList[0], + BaseMintDecimals: uint8(baseVaultTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteVaultTokenBalance.UITokenAmount.Decimals), + User: tx.rawTx.accountList[instruction.Accounts[18]], + BaseAmount: decimal.NewFromUint64(data.TokenAAmount), + QuoteAmount: decimal.NewFromUint64(data.TokenBAmount), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + EntryContract: entryContract, + }, + }, offset, nil +} + +// BootstrapLiquidity +// AddImbalanceLiquidity +// AddBalanceLiquidity +func metaoraPoolAddLiquidity(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 14 { + return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") + } + + pool := tx.rawTx.accountList[instruction.Accounts[0]] + lpMint := tx.rawTx.accountList[instruction.Accounts[1]] + payer := tx.rawTx.accountList[instruction.Accounts[13]] + userPoolLp := tx.rawTx.accountList[instruction.Accounts[2]] + // vault for storing real tokens + // NOTE: because meteora pools will put assets of different pairs together, + // we cannot directly use the vault balance to calculate liquidity + var meteoraVaultProgramId int + for i, acc := range tx.rawTx.accountList { + if acc.Equals(meteoraVaultProgram) { + meteoraVaultProgramId = i + break + } + } + if meteoraVaultProgramId == 0 { + return nil, increaseOffset(offset), fmt.Errorf("meteora vault program not found") + } + + // 7, 8 + baseVaultLpAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[7]) + quoteVaultLpAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[8]) + if baseVaultLpAccountBalance == nil || quoteVaultLpAccountBalance == nil { + return nil, increaseOffset(offset), InstructionIgnoredError //fmt.Errorf("failed to get vault lp account balances") + } + + // 9,10 + baseVaultAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[9]) + quoteVaultAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[10]) + if baseVaultAccountBalance == nil || quoteVaultAccountBalance == nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get vault lp account balances") + } + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var baseFound, quoteFound bool + + var baseAmount, quoteAmount decimal.Decimal + var ( + baseMint = solana.PublicKey{} + quoteMint = solana.PublicKey{} + baseTokenProgram = solana.PublicKey{} + quoteTokenProgram = solana.PublicKey{} + baseDecimals uint8 + quoteDecimals uint8 + baseReserve decimal.Decimal + quoteReserve decimal.Decimal + ) + baseMint = baseVaultAccountBalance.MintAccount + quoteMint = quoteVaultAccountBalance.MintAccount + quoteTokenProgram = quoteVaultAccountBalance.ProgramIDAccount + baseTokenProgram = baseVaultAccountBalance.ProgramIDAccount + + baseDecimals = uint8(baseVaultAccountBalance.UITokenAmount.Decimals) + baseReserve, err = decimal.NewFromString(baseVaultLpAccountBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err) + } + if baseDecimals != uint8(baseVaultLpAccountBalance.UITokenAmount.Decimals) { + decimalDiff := int(baseDecimals) - int(baseVaultLpAccountBalance.UITokenAmount.Decimals) + baseReserve = baseReserve.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(decimalDiff)))) + } + + quoteDecimals = uint8(quoteVaultAccountBalance.UITokenAmount.Decimals) + quoteReserve, err = decimal.NewFromString(quoteVaultLpAccountBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err) + } + if quoteDecimals != uint8(quoteVaultLpAccountBalance.UITokenAmount.Decimals) { + decimalDiff := int(quoteDecimals) - int(quoteVaultLpAccountBalance.UITokenAmount.Decimals) + quoteReserve = quoteReserve.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(decimalDiff)))) + } + + for innerIndex := 0; innerIndex < len(inners); innerIndex++ { + innerInstr := inners[innerIndex] + + if innerInstr.ProgramIDIndex == meteoraVaultProgramId && + len(innerInstr.Data) >= 16 && + bytes.Equal(innerInstr.Data[:8], eventDiscriminator[:]) && + bytes.Equal(innerInstr.Data[8:16], meteoraVaultDepositDiscriminator[:]) { + if len(innerInstr.Accounts) < 6 { + continue + } + if innerIndex+1 >= len(inners) { + continue + } + transferInstr := inners[innerIndex+1] + _, to, amount, err := parseTokenTransfer(tx.rawTx, transferInstr) + if err != nil { + continue + } + + innerIndex++ // skip transfer instruction + if !baseFound && to.Equals(tx.rawTx.accountList[baseVaultAccountBalance.AccountIndex]) { + baseFound = true + baseAmount = decimal.NewFromUint64(amount) + } else if !quoteFound && to.Equals(tx.rawTx.accountList[quoteVaultAccountBalance.AccountIndex]) { + quoteFound = true + quoteAmount = decimal.NewFromUint64(amount) + } + } + if (baseFound || quoteFound) && (tx.rawTx.accountList[innerInstr.ProgramIDIndex] == solana.TokenProgramID || + tx.rawTx.accountList[innerInstr.ProgramIDIndex] == solana.Token2022ProgramID) && innerInstr.Data[0] == 7 { + if len(innerInstr.Accounts) < 3 { + continue + } + // mint lp token + if tx.rawTx.accountList[innerInstr.Accounts[0]] == lpMint && tx.rawTx.accountList[innerInstr.Accounts[1]] == userPoolLp { + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(innerIndex) + 1 + prefixLen + } + break + } + } + } + if !baseFound && !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("failed to find deposit instructions") + } + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + var event = "add_liquidity_one_side" + if baseFound && quoteFound { + // both sides + event = "add_liquidity" + } + swap := Swap{ + Program: SolProgramMeteoraPools, + Event: event, + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + BaseMintDecimals: baseDecimals, + QuoteMintDecimals: quoteDecimals, + User: payer, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + EntryContract: entryContract, + } + + return []Swap{swap}, offset, nil + +} + +// RemoveLiquiditySingleSide +// ClaimFee +// RemoveBalanceLiquidity +func metaoraPoolRemoveLiquidity(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 14 { + return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") + } + + pool := tx.rawTx.accountList[instruction.Accounts[0]] + lpMint := tx.rawTx.accountList[instruction.Accounts[1]] + var ( + userPoolLp solana.PublicKey + baseVaultIdx int + quoteVaultIdx int + baseLpVaultIdx int + quoteLpVaultIdx int + userIdx int + ) + if bytes.Equal(instruction.Data[:8], metaoraPoolRemoveLiquiditySingleSideDiscriminator[:]) { + userPoolLp = tx.rawTx.accountList[instruction.Accounts[2]] + //userBaseAccountIdx = 11 + //userQuoteAccountIdx = 12 + baseVaultIdx = 9 + quoteVaultIdx = 10 + baseLpVaultIdx = 3 + quoteLpVaultIdx = 4 + userIdx = 12 + } else if bytes.Equal(instruction.Data[:8], metaoraPoolClaimFeeDiscriminator[:]) { + if len(instruction.Accounts) < 16 { + return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") + } + userPoolLp = tx.rawTx.accountList[instruction.Accounts[5]] + //userBaseAccountIdx = 15 + //userQuoteAccountIdx = 16 + + baseVaultIdx = 7 + quoteVaultIdx = 8 + baseLpVaultIdx = 11 + quoteLpVaultIdx = 12 + userIdx = 3 + } else if bytes.Equal(instruction.Data[:8], metaoraPoolRemoveBalanceLiquidityDiscriminator[:]) { + userPoolLp = tx.rawTx.accountList[instruction.Accounts[2]] + //userBaseAccountIdx = 11 + //userQuoteAccountIdx = 12 + + baseVaultIdx = 9 + quoteVaultIdx = 10 + baseLpVaultIdx = 3 + quoteLpVaultIdx = 4 + userIdx = 12 + + } else { + return nil, increaseOffset(offset), fmt.Errorf("invalid remove liquidity instruction discriminator") + } + // vault for storing real tokens + // NOTE: because meteora pools will put assets of different pairs together, + // we cannot directly use the vault balance to calculate liquidity + var meteoraVaultProgramId int + for i, acc := range tx.rawTx.accountList { + if acc.Equals(meteoraVaultProgram) { + meteoraVaultProgramId = i + break + } + } + if meteoraVaultProgramId == 0 { + return nil, increaseOffset(offset), fmt.Errorf("meteora vault program not found") + } + + // 7, 8 + baseVaultLpAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[baseLpVaultIdx]) + quoteVaultLpAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[quoteLpVaultIdx]) + if baseVaultLpAccountBalance == nil || quoteVaultLpAccountBalance == nil { + return nil, increaseOffset(offset), InstructionIgnoredError // fmt.Errorf("failed to get vault lp account balances") + } + + // 9,10 + baseVaultAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[baseVaultIdx]) + quoteVaultAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[quoteVaultIdx]) + if baseVaultAccountBalance == nil || quoteVaultAccountBalance == nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get vault lp account balances") + } + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var baseFound, quoteFound bool + + var baseAmount, quoteAmount decimal.Decimal + var ( + baseMint = solana.PublicKey{} + quoteMint = solana.PublicKey{} + baseTokenProgram = solana.PublicKey{} + quoteTokenProgram = solana.PublicKey{} + baseDecimals uint8 + quoteDecimals uint8 + baseReserve decimal.Decimal + quoteReserve decimal.Decimal + ) + + baseMint = baseVaultAccountBalance.MintAccount + quoteMint = quoteVaultAccountBalance.MintAccount + baseTokenProgram = baseVaultAccountBalance.ProgramIDAccount + quoteTokenProgram = quoteVaultAccountBalance.ProgramIDAccount + + baseDecimals = uint8(baseVaultAccountBalance.UITokenAmount.Decimals) + baseReserve, err = decimal.NewFromString(baseVaultLpAccountBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err) + } + if baseDecimals != uint8(baseVaultLpAccountBalance.UITokenAmount.Decimals) { + decimalDiff := int(baseDecimals) - int(baseVaultLpAccountBalance.UITokenAmount.Decimals) + baseReserve = baseReserve.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(decimalDiff)))) + } + + quoteDecimals = uint8(quoteVaultAccountBalance.UITokenAmount.Decimals) + quoteReserve, err = decimal.NewFromString(quoteVaultLpAccountBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err) + } + if quoteDecimals != uint8(quoteVaultLpAccountBalance.UITokenAmount.Decimals) { + decimalDiff := int(quoteDecimals) - int(quoteVaultLpAccountBalance.UITokenAmount.Decimals) + quoteReserve = quoteReserve.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(decimalDiff)))) + } + + for innerIndex := 0; innerIndex < len(inners); innerIndex++ { + innerInstr := inners[innerIndex] + + if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 8 && + bytes.Equal(innerInstr.Data[:8], meteoraVaultWithdrawDiscriminator[:]) { + if len(innerInstr.Accounts) < 6 { + continue + } + if innerIndex+1 >= len(inners) { + continue + } + + transferInstr := inners[innerIndex+1] + + from, _, amount, err := parseTokenTransfer(tx.rawTx, transferInstr) + if err != nil { + fmt.Println("parse tx error:", err, tx.GetTxHash(), transferInstr) + continue + } + + innerIndex++ // skip transfer instruction + + if !baseFound && from.Equals(tx.rawTx.accountList[instruction.Accounts[baseVaultIdx]]) { + //base + baseFound = true + baseAmount = decimal.NewFromUint64(amount) + } else if !quoteFound && from.Equals(tx.rawTx.accountList[instruction.Accounts[quoteVaultIdx]]) { + // quote + quoteFound = true + quoteAmount = decimal.NewFromUint64(amount) + } + } + if (baseFound || quoteFound) && (tx.rawTx.accountList[innerInstr.ProgramIDIndex] == solana.TokenProgramID || + tx.rawTx.accountList[innerInstr.ProgramIDIndex] == solana.Token2022ProgramID) && innerInstr.Data[0] == 8 { + if len(innerInstr.Accounts) < 3 { + continue + } + // mint lp token + if tx.rawTx.accountList[innerInstr.Accounts[1]] == lpMint && tx.rawTx.accountList[innerInstr.Accounts[0]] == userPoolLp { + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(innerIndex) + 1 + prefixLen + } + break + } + } + } + if !baseFound && !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("failed to find withdraw instructions, baseFound: %v, quoteFound: %v", baseFound, quoteFound) + } + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + var event = "remove_liquidity_one_side" + if baseFound && quoteFound { + event = "remove_liquidity" + } + swap := Swap{ + Program: SolProgramMeteoraPools, + Event: event, + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + BaseMintDecimals: baseDecimals, + QuoteMintDecimals: quoteDecimals, + User: tx.rawTx.accountList[userIdx], + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + EntryContract: entryContract, + } + + return []Swap{swap}, offset, nil +} + +func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + pool := tx.rawTx.accountList[instruction.Accounts[0]] + payer := tx.rawTx.accountList[instruction.Accounts[12]] + + sourceAccountIndex := instruction.Accounts[1] + destinationAccountIndex := instruction.Accounts[2] + + // vault for storing real tokens + // NOTE: because meteora pools will put assets of different pairs together, + // we cannot directly use the vault balance to calculate liquidity + + //parse reserves from vault accounts + baseVaultIdx := instruction.Accounts[6] + quoteVaultIdx := instruction.Accounts[5] + baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx) + quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx) + if baseVaultTokenBalance == nil || quoteVaultTokenBalance == nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get vault token balances") + } + + baseVaultLpBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[10]) + quoteVaultLpBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[9]) + if baseVaultLpBalance == nil || quoteVaultLpBalance == nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get vault lp balances") + } + baseTokenProgram := baseVaultTokenBalance.ProgramIDAccount + quoteTokenProgram := quoteVaultTokenBalance.ProgramIDAccount + baseMint := baseVaultTokenBalance.MintAccount + quoteMint := quoteVaultTokenBalance.MintAccount + baseDecimals := uint8(baseVaultTokenBalance.UITokenAmount.Decimals) + quoteDecimals := uint8(quoteVaultTokenBalance.UITokenAmount.Decimals) + + baseReserve := decimal.Zero + quoteReserve := decimal.Zero + if baseVaultLpBalance.UITokenAmount.Decimals == baseVaultTokenBalance.UITokenAmount.Decimals { + baseReserve, _ = decimal.NewFromString(baseVaultLpBalance.UITokenAmount.Amount) + } else { + decimalsDiff := int32(baseVaultTokenBalance.UITokenAmount.Decimals) - int32(baseVaultLpBalance.UITokenAmount.Decimals) + multiplier := decimal.NewFromInt(10).Pow(decimal.NewFromInt32(decimalsDiff)) + baseLpAmount, _ := decimal.NewFromString(baseVaultLpBalance.UITokenAmount.Amount) + baseReserve = baseLpAmount.Mul(multiplier) + } + + if quoteVaultLpBalance.UITokenAmount.Decimals == quoteVaultTokenBalance.UITokenAmount.Decimals { + quoteReserve, _ = decimal.NewFromString(quoteVaultLpBalance.UITokenAmount.Amount) + } else { + decimalsDiff := int32(quoteVaultTokenBalance.UITokenAmount.Decimals) - int32(quoteVaultLpBalance.UITokenAmount.Decimals) + multiplier := decimal.NewFromInt(10).Pow(decimal.NewFromInt32(decimalsDiff)) + quoteLpAmount, _ := decimal.NewFromString(quoteVaultLpBalance.UITokenAmount.Amount) + quoteReserve = quoteLpAmount.Mul(multiplier) + } + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var meteoraVaultProgramId int + for i, acc := range tx.rawTx.accountList { + if acc.Equals(meteoraVaultProgram) { + meteoraVaultProgramId = i + break + } + } + + var baseFound, quoteFound bool + var ( + baseAmount decimal.Decimal + quoteAmount decimal.Decimal + event string + ) + for innerIndex := 0; innerIndex < len(inners); innerIndex++ { + innerInstr := inners[innerIndex] + // + if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 8 && + (bytes.Equal(innerInstr.Data[:8], meteoraVaultWithdrawDiscriminator[:]) || + bytes.Equal(innerInstr.Data[:8], meteoraVaultDepositDiscriminator[:])) { + if len(innerInstr.Accounts) < 6 { + continue + } + if innerIndex+1 >= len(inners) { + continue + } + + transferInstr := inners[innerIndex+1] + if (tx.rawTx.accountList[transferInstr.ProgramIDIndex] != solana.TokenProgramID && + tx.rawTx.accountList[transferInstr.ProgramIDIndex] != solana.Token2022ProgramID) || transferInstr.Data[0] != 3 { + continue + } + innerIndex++ // skip transfer instruction + if len(innerInstr.Accounts) == 7 && + (bytes.Equal(innerInstr.Data[:8], meteoraVaultWithdrawDiscriminator[:]) || bytes.Equal(innerInstr.Data[:8], meteoraVaultDepositDiscriminator[:])) { + if innerInstr.Accounts[1] == baseVaultIdx { + //base + baseFound = true + baseAmount = decimal.NewFromUint64(binary.LittleEndian.Uint64(transferInstr.Data[1:9])) + if bytes.Equal(innerInstr.Data[:8], meteoraVaultWithdrawDiscriminator[:]) { + event = "buy" + } else { + event = "sell" + } + } else if innerInstr.Accounts[1] == quoteVaultIdx { + // quote + quoteFound = true + quoteAmount = decimal.NewFromUint64(binary.LittleEndian.Uint64(transferInstr.Data[1:9])) + } + + } + + } + if baseFound && quoteFound { + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(innerIndex) + 1 + prefixLen + 1 // +1 for mint or withdraw instruction, + } + break + } + } + if !baseFound || !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("failed to find meteora pool event in inner instructions") + } + + userBase := getAccountBalanceAfterTx(tx.rawTx, sourceAccountIndex) + userQuote := getAccountBalanceAfterTx(tx.rawTx, destinationAccountIndex) + + swaps := []Swap{ + { + Program: SolProgramMeteoraPools, + Event: event, + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + Creator: solana.PublicKey{}, + BaseMintDecimals: baseDecimals, + QuoteMintDecimals: quoteDecimals, + User: payer, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + EntryContract: entryContract, + }, + } + return swaps, offset, nil +} diff --git a/meteora_bonding_curve.go b/meteora_bonding_curve.go new file mode 100644 index 0000000..b773a9a --- /dev/null +++ b/meteora_bonding_curve.go @@ -0,0 +1,389 @@ +package pump_parser + +import ( + "bytes" + "encoding/binary" + "fmt" + + agbinary "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/shopspring/decimal" +) + +type MetaoraBcEvtInitializePool struct { + Pool solana.PublicKey + Config solana.PublicKey + Creator solana.PublicKey + BaseMint solana.PublicKey + //PoolType uint8 + //ActivationPoint uint64 +} + +type MetaoraBcSwapEvent struct { + Pool solana.PublicKey `json:"pool"` + Config solana.PublicKey `json:"config"` + TradeDirection uint8 `json:"tradeDirection"` + HasReferral bool `json:"hasReferral"` + Params *struct { + AmountIn uint64 `json:"amountIn"` + MinimumAmountOut uint64 `json:"minimumAmountOut"` + } `json:"params"` + SwapResult *struct { + ActualInputAmount uint64 `json:"actualInputAmount"` + OutputAmount uint64 `json:"outputAmount"` + NextSqrtPrice [16]byte `json:"nextSqrtPrice"` + TradingFee uint64 `json:"tradingFee"` + ProtocolFee uint64 `json:"protocolFee"` + ReferralFee uint64 `json:"referralFee"` + } `json:"swapResult"` + AmountIn uint64 `json:"amountIn"` + CurrentTimestamp uint64 `json:"currentTimestamp"` +} + +func metaoraBcParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(metaoraBcProgramID) { + return nil, increaseOffset(offset), fmt.Errorf("metaora Bonding Curve program instruction not found, offset, %d, %d", offset[0], offset[1]) + } + + decode := instruction.Data + if len(decode) < 8 { + return nil, increaseOffset(offset), fmt.Errorf("metaora Bonding Curve program instruction data too short, offset, %d, %d", offset[0], offset[1]) + } + + discriminator := *(*[8]byte)(decode[:8]) + + switch discriminator { + case metaoraBcInitialize2022PoolDiscriminator, + metaoraBcInitializedPoolDiscriminator: + return metaBcInitializePoolParser(tx, instruction, innerInstructions, offset) + case metaoraBcMigrateMeteoraDammDiscriminator: + return metaBcMigrateParser(tx, instruction, innerInstructions, offset) + case metaoraBcMigrateMeteoraDammV2Discriminator: + return metaBcMigrateV2Parser(tx, instruction, innerInstructions, offset) + case metaoraBcSwapDiscriminator: + return metaBcSwapParser(tx, instruction, innerInstructions, offset) + case metaoraBcSwapV2Discriminator: + return metaBcSwapParser(tx, instruction, innerInstructions, offset) + default: + return nil, increaseOffset(offset), InstructionIgnoredError + } +} + +type MetaoraCreateData struct { + Name string + Symbol string + Uri string +} + +func metaBcInitializePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 14 { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool not enough accounts, offset, %d, %d", offset[0], offset[1]) + } + var createData MetaoraCreateData + err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&createData) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool parse create data error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[6]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool get base token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[7]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool get quote token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + baseReserve, err := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool parse base reserve error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + quoteReserve, err := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool parse quote reserve error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var user solana.PublicKey + if bytes.Equal(instruction.Data[:8], metaoraBcInitialize2022PoolDiscriminator[:]) { + user = tx.rawTx.accountList[instruction.Accounts[8]] + } else if bytes.Equal(instruction.Data[:8], metaoraBcInitializedPoolDiscriminator[:]) { + user = tx.rawTx.accountList[instruction.Accounts[10]] + } else { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool unknown discriminator, offset, %d, %d", offset[0], offset[1]) + } + baseTokenProgram := baseTokenBalance.ProgramIDAccount + quoteTokenProgram := quoteTokenBalance.ProgramIDAccount + baseMintDecimals := uint8(baseTokenBalance.UITokenAmount.Decimals) + quoteMintDecimals := uint8(quoteTokenBalance.UITokenAmount.Decimals) + var ( + pool solana.PublicKey + baseMint solana.PublicKey + creator solana.PublicKey + totalSupply *decimal.Decimal + ) + for innerIndex, innerInstr := range inners { + if tx.rawTx.accountList[innerInstr.ProgramIDIndex].Equals(baseMint) && + len(innerInstr.Data) >= 9 && innerInstr.Data[0] == 7 && + len(innerInstr.Accounts) == 3 && tx.rawTx.accountList[innerInstr.Accounts[0]].Equals(baseMint) && + innerInstr.Accounts[1] == instruction.Accounts[6] { + supply := decimal.NewFromUint64(binary.LittleEndian.Uint64(innerInstr.Data[1:9])) + totalSupply = &supply + } + if innerInstr.ProgramIDIndex == instruction.ProgramIDIndex && + len(innerInstr.Data) >= 16 && + bytes.Equal(innerInstr.Data[0:8], pumpEventDiscriminator[:]) && + bytes.Equal(innerInstr.Data[8:16], metaoraBcEventInitializePoolDiscriminator[:]) { + + var event MetaoraBcEvtInitializePool + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(innerIndex) + 1 + prefixLen + } + err := agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&event) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initialize pool deserialize event error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + pool = event.Pool + baseMint = event.BaseMint + creator = event.Creator + break + } + } + if pool.IsZero() { + return nil, offset, fmt.Errorf("meta Bonding Curve initialize pool event not found, offset, %d, %d", offset[0], offset[1]) + } + quoteMint := tx.rawTx.accountList[instruction.Accounts[4]] + + tx.Token[baseMint] = TokenMeta{ + Mint: baseMint, + TokenProgram: baseTokenProgram, + Decimals: baseMintDecimals, + Name: createData.Name, + Symbol: createData.Symbol, + Url: createData.Uri, + TotalSupply: totalSupply, + } + return []Swap{ + { + Program: SolProgramMeteoraBondingCurve, + Event: "create", + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + Creator: creator, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + User: user, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + EntryContract: entryContract, + }, + }, offset, nil +} + +func metaBcMigrateV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 25 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for migrate instruction") + } + + baseVaultBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[17]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve migrate get base vault balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + quoteVaultBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[18]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve migrate get quote vault balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + swaps := []Swap{ + { + Program: SolProgramMeteoraBondingCurve, + Event: "migrate", + Pool: tx.rawTx.accountList[instruction.Accounts[0]], + BaseMint: tx.rawTx.accountList[instruction.Accounts[13]], + QuoteMint: tx.rawTx.accountList[instruction.Accounts[14]], + BaseTokenProgram: tx.rawTx.accountList[instruction.Accounts[20]], + QuoteTokenProgram: tx.rawTx.accountList[instruction.Accounts[21]], + BaseMintDecimals: uint8(baseVaultBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteVaultBalance.UITokenAmount.Decimals), + User: tx.rawTx.accountList[instruction.Accounts[19]], + //BaseAmount: decimal.Decimal{}, + //QuoteAmount: decimal.Decimal{}, + //BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount), + //QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount), + MigrateTopProgram: meteoraDammV2Program, + MigrateToPool: tx.rawTx.accountList[instruction.Accounts[4]], + EntryContract: entryContract, + }, + } + return swaps, offset, nil +} + +func metaBcMigrateParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 23 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for migrate instruction") + } + + baseVaultBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[17]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve migrate get base vault balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + quoteVaultBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[18]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve migrate get quote vault balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + swaps := []Swap{ + { + Program: SolProgramMeteoraBondingCurve, + Event: "migrate", + Pool: tx.rawTx.accountList[instruction.Accounts[0]], + BaseMint: tx.rawTx.accountList[instruction.Accounts[7]], + QuoteMint: tx.rawTx.accountList[instruction.Accounts[8]], + BaseTokenProgram: baseVaultBalance.ProgramIDAccount, + QuoteTokenProgram: baseVaultBalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseVaultBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteVaultBalance.UITokenAmount.Decimals), + User: tx.rawTx.accountList[instruction.Accounts[22]], + //BaseAmount: decimal.Decimal{}, + //QuoteAmount: decimal.Decimal{}, + //BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount), + //QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount), + MigrateTopProgram: metaoraPoolProgramID, + MigrateToPool: tx.rawTx.accountList[instruction.Accounts[4]], + EntryContract: entryContract, + }, + } + return swaps, offset, nil +} + +func metaBcSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 15 { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve swap not enough accounts, offset, %d, %d", offset[0], offset[1]) + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + userBase := getAccountBalanceAfterTx(tx.rawTx, instruction.Accounts[3]) + userQuote := getAccountBalanceAfterTx(tx.rawTx, instruction.Accounts[4]) + inputToken := tx.rawTx.accountList[instruction.Accounts[3]] + outputToken := tx.rawTx.accountList[instruction.Accounts[4]] + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[5]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve swap get base token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[6]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve swap get quote token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + baseReserve, err := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve swap parse base reserve error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + quoteReserve, err := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve swap parse quote reserve error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + baseTokenProgram := baseTokenBalance.ProgramIDAccount + quoteTokenProgram := quoteTokenBalance.ProgramIDAccount + baseMintDecimals := uint8(baseTokenBalance.UITokenAmount.Decimals) + quoteMintDecimals := uint8(quoteTokenBalance.UITokenAmount.Decimals) + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var ( + swapEvent MetaoraBcSwapEvent + eventLoaded bool + event string + ) + for innerIndex, innerInstr := range inners { + from, to, _, err := parseTokenTransfer(tx.rawTx, innerInstr) + if err == nil { + if from.Equals(inputToken) && to.Equals(tx.rawTx.accountList[quoteTokenBalance.AccountIndex]) { + event = "buy" + } else if from.Equals(tx.rawTx.accountList[quoteTokenBalance.AccountIndex]) && to.Equals(outputToken) { + event = "sell" + } + } + if innerInstr.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstr.Data) >= 16 && + bytes.Equal(innerInstr.Data[0:8], pumpEventDiscriminator[:]) && + bytes.Equal(innerInstr.Data[8:16], metaoraBcEventSwapDiscriminator[:]) { + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(innerIndex) + 1 + prefixLen + } + err := agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&swapEvent) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve pool swap event deserialize event error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + eventLoaded = true + break + } + } + if !eventLoaded { + return nil, offset, fmt.Errorf("meta Bonding Curve swap event not found, offset, %d, %d", offset[0], offset[1]) + } + baseMint := tx.rawTx.accountList[instruction.Accounts[7]] + quoteMint := tx.rawTx.accountList[instruction.Accounts[8]] + user := tx.rawTx.accountList[instruction.Accounts[9]] + pool := tx.rawTx.accountList[instruction.Accounts[2]] + var ( + baseMintAmount decimal.Decimal + quoteMintAmount decimal.Decimal + ) + if swapEvent.TradeDirection == 0 { + // A -> B + if event == "sell" { + baseMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.ActualInputAmount) + quoteMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.OutputAmount) + } else { + baseMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.OutputAmount) + quoteMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.ActualInputAmount) + } + + } else { + // B -> A + if event == "buy" { + baseMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.OutputAmount) + quoteMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.ActualInputAmount) + } else { + baseMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.ActualInputAmount) + quoteMintAmount = decimal.NewFromUint64(swapEvent.SwapResult.OutputAmount) + } + } + + swaps := []Swap{ + { + Program: SolProgramMeteoraBondingCurve, + Event: event, + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + Creator: solana.PublicKey{}, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + User: user, + BaseAmount: baseMintAmount, + QuoteAmount: quoteMintAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + EntryContract: entryContract, + }, + } + return swaps, offset, nil +} diff --git a/meteoradamm.go b/meteoradamm.go new file mode 100644 index 0000000..92af5ac --- /dev/null +++ b/meteoradamm.go @@ -0,0 +1,479 @@ +package pump_parser + +import ( + "bytes" + "fmt" + + agbinary "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/shopspring/decimal" +) + +func metaoraDammParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(meteoraDammV2Program) { + return nil, increaseOffset(offset), fmt.Errorf("metaora damm program instruction not found, offset, %d, %d", offset[0], offset[1]) + } + + decode := instruction.Data + if len(decode) < 8 { + return nil, increaseOffset(offset), fmt.Errorf("metaora damm program instruction data too short, offset, %d, %d", offset[0], offset[1]) + } + + discriminator := *(*[8]byte)(decode[:8]) + + switch discriminator { + case meteoraDammV2InitializeCustomizablePoolDiscriminator, + meteoraDammV2InitializePoolWithDynamicConfig, + meteoraDammV2InitializePoolDiscriminator: + return meteoraDammV2InitializePoolParser(tx, instruction, innerInstructions, offset) + case meteoraDammV2SwapDiscriminator, meteoraDammV2SwapV2Discriminator: + return meteoraDammV2Swap(tx, instruction, innerInstructions, offset) + case meteoraDammV2AddLiquidityDiscriminator: + return meteoraDammV2AddLiquidityParser(tx, instruction, innerInstructions, offset) + case meteoraDammV2RemoveLiquidityDiscriminator, meteoraDammV2RemoveAllLiquidityDiscriminator: + return meteoraDammV2RemoveLiquidityParser(tx, instruction, innerInstructions, offset) + default: + return nil, increaseOffset(offset), InstructionIgnoredError + } +} + +var ( + metaoraDammInitializePoolDiscriminator = []byte{228, 69, 165, 46, 81, 203, 154, 29, 228, 50, 246, 85, 203, 66, 134, 37} + meteoraDammSwapDiscriminator = []byte{228, 69, 165, 46, 81, 203, 154, 29, 189, 66, 51, 168, 38, 80, 117, 153} + // EvtLiquidityChange + meteoraDammAddLiquidityDiscriminator = []byte{228, 69, 165, 46, 81, 203, 154, 29, 197, 171, 78, 127, 224, 211, 87, 13} + meteoraDammRemoveLiquidityDiscriminator = []byte{228, 69, 165, 46, 81, 203, 154, 29, 197, 171, 78, 127, 224, 211, 87, 13} +) + +type MetaoraDammDynamicFeeParameters struct { + BinStep uint16 + BinStepU128 [16]byte + FilterPeriod uint16 + DecayPeriod uint16 + ReductionFactor uint16 + MaxVolatilityAccumulator uint32 + VariableFeeControl uint32 +} + +type MetaoraDammInitializePoolEvent struct { + Pool solana.PublicKey `json:"pool"` + TokenAMint solana.PublicKey `json:"tokenAMint"` + TokenBMint solana.PublicKey `json:"tokenBMint"` + Creator solana.PublicKey `json:"creator"` + Payer solana.PublicKey `json:"payer"` + AlphaVault solana.PublicKey `json:"alphaVault"` + //PoolFees *struct { + // BaseFee [30]byte + // DynamicFee *MetaoraDammDynamicFeeParameters `json:"dynamicFee"` + //} `json:"poolFees"` + //SqrtMinPrice [16]byte `json:"sqrtMinPrice"` + //SqrtMaxPrice [16]byte `json:"sqrtMaxPrice"` + //ActivationType uint8 `json:"activationType"` + //CollectFeeMode uint8 `json:"collectFeeMode"` + //Liquidity [16]byte `json:"liquidity"` + //SqrtPrice [16]byte `json:"sqrtPrice"` + //ActivationPoint uint64 `json:"activationPoint"` + //TokenAFlag uint8 `json:"tokenAFlag"` + //TokenBFlag uint8 `json:"tokenBFlag"` + //TokenAAmount uint64 `json:"tokenAAmount"` + //TokenBAmount uint64 `json:"tokenBAmount"` + //TotalAmountA uint64 `json:"totalAmountA"` + //TotalAmountB uint64 `json:"totalAmountB"` + //PoolType uint8 `json:"poolType"` +} + +func meteoraDammV2InitializePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 12 { + return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var loadedEvent bool + var initializePoolEvent MetaoraDammInitializePoolEvent + for i, innerInstruction := range inners { + if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && bytes.Equal(innerInstruction.Data[:16], metaoraDammInitializePoolDiscriminator) { + err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&initializePoolEvent) + if err != nil { + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(i) + 1 + prefixLen + } + return nil, offset, fmt.Errorf("failed to deserialize initialize pool event: %w", err) + } + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(i) + 1 + prefixLen + } + loadedEvent = true + break + } + } + if !loadedEvent { + return nil, increaseOffset(offset), fmt.Errorf("failed to get initialize pool event") + } + baseVaultAccountIndex := instruction.Accounts[10] + quoteVaultAccountIndex := instruction.Accounts[11] + + baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("metaora damm get base vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("metaora damm get quote vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + baseReserve, _ := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) + + swap := Swap{ + Program: SolProgramMeteoraAmmV2, + Event: "create", + Pool: initializePoolEvent.Pool, + BaseMint: initializePoolEvent.TokenAMint, + QuoteMint: initializePoolEvent.TokenBMint, + BaseTokenProgram: baseVaultTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteVaultTokenBalance.ProgramIDAccount, + Creator: initializePoolEvent.Creator, + BaseMintDecimals: uint8(baseVaultTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteVaultTokenBalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + User: tx.rawTx.accountList[instruction.Accounts[0]], + LpMint: tx.rawTx.accountList[instruction.Accounts[1]], + EntryContract: entryContract, + } + return []Swap{swap}, offset, nil +} + +type meteoraDammSwapEvent struct { + Pool solana.PublicKey + TradeDirection uint8 + CollectFeeMode uint8 + HasReferral bool + + Params *struct { + Amount0 uint64 + Amount1 uint64 + SwapMode uint8 + } + SwapResult *struct { + IncludedFeeInputAmount uint64 + ExcludedFeeInputAmount uint64 + AmountLeft uint64 + OutputAmount uint64 + NextSqrtPrice [16]byte + TradingFee uint64 + ProtocolFee uint64 + PartnerFee uint64 + ReferralFee uint64 + } + IncludedTransferFeeAmountIn uint64 + IncludedTransferFeeAmountOut uint64 + ExcludedTransferFeeAmountOut uint64 + CurrentTimestamp uint64 + ReserveAAmount uint64 + ReserveBAmount uint64 +} + +func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 9 { + return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") + } + + sourceAccountIndex := instruction.Accounts[2] + destinationAccountIndex := instruction.Accounts[3] + baseVaultAccountIndex := instruction.Accounts[4] + quoteVaultAccountIndex := instruction.Accounts[5] + tokenAMint := tx.rawTx.accountList[instruction.Accounts[6]] + tokenBMint := tx.rawTx.accountList[instruction.Accounts[7]] + payer := tx.rawTx.accountList[instruction.Accounts[8]] + + baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("metaora damm get base vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("metaora damm get quote vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + + baseReserve, _ := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) + + baseMint := tokenAMint + quoteMint := tokenBMint + baseTokenProgram := baseVaultTokenBalance.ProgramIDAccount + quoteTokenProgram := quoteVaultTokenBalance.ProgramIDAccount + baseMintDecimals := uint8(baseVaultTokenBalance.UITokenAmount.Decimals) + quoteMintDecimals := uint8(quoteVaultTokenBalance.UITokenAmount.Decimals) + + userInputTokenBalance := getAccountBalanceAfterTx(tx.rawTx, sourceAccountIndex) + userOutputTokenBalance := getAccountBalanceAfterTx(tx.rawTx, destinationAccountIndex) + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var loadedEvent bool + var swapEvent meteoraDammSwapEvent + for i, innerInstruction := range inners { + if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && bytes.Equal(innerInstruction.Data[:16], meteoraDammSwapDiscriminator) { + err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&swapEvent) + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(i) + 1 + prefixLen + } + if err != nil { + return nil, offset, fmt.Errorf("failed to deserialize swap event: %w", err) + } + + loadedEvent = true + + break + } + } + if !loadedEvent { + return nil, increaseOffset(offset), fmt.Errorf("failed to get swap event") + } + var baseAmount decimal.Decimal + var quoteAmount decimal.Decimal + var userBase decimal.Decimal + var userQuote decimal.Decimal + event := "buy" + if swapEvent.TradeDirection == 0 { + // A -> B + // sell base/A; buy quote/B + event = "sell" + userBase = userInputTokenBalance + userQuote = userOutputTokenBalance + baseAmount = decimal.NewFromUint64(swapEvent.SwapResult.IncludedFeeInputAmount) + quoteAmount = decimal.NewFromUint64(swapEvent.ExcludedTransferFeeAmountOut) + + } else if swapEvent.TradeDirection == 1 { + // B -> A + // sell quote/B; buy base/A + userBase = userOutputTokenBalance + userQuote = userInputTokenBalance + baseAmount = decimal.NewFromUint64(swapEvent.ExcludedTransferFeeAmountOut) + quoteAmount = decimal.NewFromUint64(swapEvent.SwapResult.IncludedFeeInputAmount) + } else { + return nil, offset, fmt.Errorf("invalid trade direction") + } + + return []Swap{ + { + Program: SolProgramMeteoraAmmV2, + Event: event, + Pool: swapEvent.Pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + Creator: solana.PublicKey{}, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + User: payer, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + EntryContract: entryContract, + }, + }, offset, nil + +} + +type MeteoraDammV2LiquidityData struct { + LiquidityDelta [16]byte `json:"liquidityDelta"` + TokenAAmounthreshold uint64 `json:"tokenAAmounthreshold"` + TokenBAmounthreshold uint64 `json:"tokenBAmounthreshold"` +} +type MeteoraDammV2AddLiquidityEvent struct { + Pool solana.PublicKey `json:"pool"` + Position solana.PublicKey `json:"position"` + Owner solana.PublicKey `json:"owner"` + Params *MeteoraDammV2LiquidityData `json:"params"` + TokenAAmount uint64 `json:"tokenAAmount"` + TokenBAmount uint64 `json:"tokenBAmount"` + TotalAmountA uint64 `json:"totalAmountA"` + TotalAmountB uint64 `json:"totalAmountB"` +} + +type MeteoraDammV2RemoveLiquidityEvent struct { + Pool solana.PublicKey `json:"pool"` + Position solana.PublicKey `json:"position"` + Owner solana.PublicKey `json:"owner"` + Params *MeteoraDammV2LiquidityData `json:"params"` + TokenAAmount uint64 `json:"tokenAAmount"` + TokenBAmount uint64 `json:"tokenBAmount"` +} + +func meteoraDammV2AddLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + + if len(instruction.Accounts) < 8 { + return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") + } + tokenAMint := tx.rawTx.accountList[instruction.Accounts[6]] + tokenBMint := tx.rawTx.accountList[instruction.Accounts[7]] + + baseVaultAccountIndex := instruction.Accounts[4] + quoteVaultAccountIndex := instruction.Accounts[5] + + baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("metaora damm get base vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("metaora damm get quote vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + baseReserve, _ := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) + + baseTokenProgram := baseVaultTokenBalance.ProgramIDAccount + quoteTokenProgram := quoteVaultTokenBalance.ProgramIDAccount + baseMintDecimals := uint8(baseVaultTokenBalance.UITokenAmount.Decimals) + quoteMintDecimals := uint8(quoteVaultTokenBalance.UITokenAmount.Decimals) + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var loadedEvent bool + var liquidityEvent MeteoraDammV2AddLiquidityEvent + for i, innerInstruction := range inners { + if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && bytes.Equal(innerInstruction.Data[:16], meteoraDammAddLiquidityDiscriminator[:]) { + err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&liquidityEvent) + if err != nil { + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(i) + 1 + prefixLen + } + return nil, offset, fmt.Errorf("failed to deserialize add liquidity event: %w", err) + } + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(i) + 1 + prefixLen + } + loadedEvent = true + break + } + } + + if !loadedEvent { + return nil, increaseOffset(offset), fmt.Errorf("failed to get add liquidity event") + } + swap := Swap{ + Program: SolProgramMeteoraDLMM, + Event: "add_liquidity", + Pool: liquidityEvent.Pool, + BaseMint: tokenAMint, + QuoteMint: tokenBMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + User: liquidityEvent.Owner, + BaseAmount: decimal.NewFromUint64(liquidityEvent.TokenAAmount), + QuoteAmount: decimal.NewFromUint64(liquidityEvent.TokenBAmount), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + EntryContract: entryContract, + } + + return []Swap{swap}, offset, nil +} + +func meteoraDammV2RemoveLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 8 { + return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") + } + tokenAMint := tx.rawTx.accountList[instruction.Accounts[7]] + tokenBMint := tx.rawTx.accountList[instruction.Accounts[8]] + + baseVaultAccountIndex := instruction.Accounts[5] + quoteVaultAccountIndex := instruction.Accounts[6] + + baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("metaora damm get base vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("metaora damm get quote vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + baseReserve, _ := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) + + baseTokenProgram := baseVaultTokenBalance.ProgramIDAccount + quoteTokenProgram := quoteVaultTokenBalance.ProgramIDAccount + baseMintDecimals := uint8(baseVaultTokenBalance.UITokenAmount.Decimals) + quoteMintDecimals := uint8(quoteVaultTokenBalance.UITokenAmount.Decimals) + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meta damm get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var loadedEvent bool + var liquidityEvent MeteoraDammV2RemoveLiquidityEvent + for i, innerInstruction := range inners { + if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && bytes.Equal(innerInstruction.Data[:16], meteoraDammRemoveLiquidityDiscriminator[:]) { + err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&liquidityEvent) + if err != nil { + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(i) + 1 + prefixLen + } + return nil, offset, fmt.Errorf("failed to deserialize remove liquidity event: %w", err) + } + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(i) + 1 + prefixLen + } + loadedEvent = true + break + } + } + + if !loadedEvent { + return nil, increaseOffset(offset), fmt.Errorf("failed to get remove liquidity event") + } + swap := Swap{ + Program: SolProgramMeteoraDLMM, + Event: "remove_liquidity", + Pool: liquidityEvent.Pool, + BaseMint: tokenAMint, + QuoteMint: tokenBMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + User: liquidityEvent.Owner, + BaseAmount: decimal.NewFromUint64(liquidityEvent.TokenAAmount), + QuoteAmount: decimal.NewFromUint64(liquidityEvent.TokenBAmount), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + EntryContract: entryContract, + } + + return []Swap{swap}, offset, nil +} diff --git a/orcawhirpool.go b/orcawhirpool.go new file mode 100644 index 0000000..c15b785 --- /dev/null +++ b/orcawhirpool.go @@ -0,0 +1,1262 @@ +package pump_parser + +import ( + "fmt" + + "github.com/gagliardetto/solana-go" + "github.com/shopspring/decimal" +) + +func orcaWhirPoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(orcaProgramID) { + return nil, increaseOffset(offset), fmt.Errorf("orcawhirpoolprogram instruction not found, offset, %d, %d", offset[0], offset[1]) + } + + decode := instruction.Data + + discriminator := *(*[8]byte)(decode[:8]) + + switch discriminator { + case orcaInitializePoolDiscriminator: + return orcaWhirPoolInitialParser(tx, instruction, innerInstructions, offset) + case orcaInitializePoolWithAdaptiveFeeDiscriminator: + return orcaWhirPoolInitialWithAdaptiveFeeParser(tx, instruction, innerInstructions, offset) + case orcaInitializePoolV2Discriminator: + return orcaWhirPoolInitialV2Parser(tx, instruction, innerInstructions, offset) + case orcaIncreaseLiquidityDiscriminator, orcaDecreaseLiquidityDiscriminator: + return orcaWhirPoolLiquidityParser(tx, instruction, innerInstructions, offset) + case orcaIncreaseLiquidityV2Discriminator, orcaDecreaseLiquidityV2Discriminator: + return orcaWhirPoolLiquidityV2Parser(tx, instruction, innerInstructions, offset) + case orcaCollectFeesDiscriminator: + return orcaWhirPoolCollectFeeParser(tx, instruction, innerInstructions, offset) + case orcaCollectFeesV2Discriminator: + return orcaWhirPoolCollectFeeV2Parser(tx, instruction, innerInstructions, offset) + case orcaCollectProtocolFeesV2Discriminator: + return orcaWhirPoolCollectProtocolFeeV2Parser(tx, instruction, innerInstructions, offset) + case orcaSwapDiscriminator: + return orcaWhirPoolSwapParser(tx, instruction, innerInstructions, offset) + case orcaSwapV2Discriminator: + return orcaWhirPoolSwapV2Parser(tx, instruction, innerInstructions, offset) + case orcaTwoHopSwapDiscriminator: + return orcaWhirPoolTwoHopSwapParser(tx, instruction, innerInstructions, offset) + case orcaTwoHopSwapV2Discriminator: + return orcaWhirPoolTwoHopSwapV2Parser(tx, instruction, innerInstructions, offset) + default: + return nil, increaseOffset(offset), InstructionIgnoredError + } +} + +func orcaWhirPoolInitialParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 11 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize pool instruction") + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + // Get accounts from instruction + pool := tx.rawTx.accountList[instruction.Accounts[4]] + signer := tx.rawTx.accountList[0] + + vault0 := instruction.Accounts[5] + vault1 := instruction.Accounts[6] + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) + } + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + + offset[1] += 5 + return []Swap{ + { + Program: SolProgramOrcaWhirPool, + Event: "create", + Pool: pool, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + Creator: signer, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func orcaWhirPoolInitialWithAdaptiveFeeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 16 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize pool instruction") + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + pool := tx.rawTx.accountList[instruction.Accounts[7]] + signer := tx.rawTx.accountList[0] + + vault0 := instruction.Accounts[9] + vault1 := instruction.Accounts[10] + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) + } + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + offset[1] += 9 + return []Swap{ + { + Program: SolProgramOrcaWhirPool, + Event: "create", + Pool: pool, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + Creator: signer, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func orcaWhirPoolInitialV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 14 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize pool instruction") + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + pool := tx.rawTx.accountList[instruction.Accounts[6]] + signer := tx.rawTx.accountList[0] + + vault0 := instruction.Accounts[7] + vault1 := instruction.Accounts[8] + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) + } + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + offset[1] += 7 + return []Swap{ + { + Program: SolProgramOrcaWhirPool, + Event: "create", + Pool: pool, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + Creator: signer, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func orcaWhirPoolLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 11 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for instruction") + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + signer := tx.rawTx.accountList[0] + pool := tx.rawTx.accountList[instruction.Accounts[0]] + + vault0 := instruction.Accounts[7] + vault1 := instruction.Accounts[8] + + baseMint := tx.rawTx.accountList[instruction.Accounts[1]] + quoteMint := tx.rawTx.accountList[instruction.Accounts[2]] + + baseTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault0) + quoteTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault1) + if baseTokenBalance == nil && quoteTokenBalance == nil { + return nil, increaseOffset(offset), InstructionIgnoredError + } + var ( + baseReserve decimal.Decimal + quoteReserve decimal.Decimal + + baseMintDecimals uint8 + quoteMintDecimals uint8 + + baseProgram solana.PublicKey + quoteProgram solana.PublicKey + ) + if baseTokenBalance != nil { + baseReserve, _ = decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + baseMintDecimals = uint8(baseTokenBalance.UITokenAmount.Decimals) + baseProgram = baseTokenBalance.ProgramIDAccount + } + if quoteTokenBalance != nil { + quoteReserve, _ = decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + quoteMintDecimals = uint8(quoteTokenBalance.UITokenAmount.Decimals) + quoteProgram = quoteTokenBalance.ProgramIDAccount + } + + discriminator := *(*[8]byte)(instruction.Data[:8]) + var instructionName string + if discriminator == orcaDecreaseLiquidityDiscriminator { + instructionName = "remove_liquidity" + } else if discriminator == orcaIncreaseLiquidityDiscriminator { + instructionName = "add_liquidity" + } + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if len(inners) < 2 { + return nil, increaseOffset(offset), fmt.Errorf("not enough inner instructions for protocol fee collection") + } + baseAmount := decimal.Zero + quoteAmount := decimal.Zero + var baseFound, quoteFound bool + for i := 0; i < 2; i++ { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inners[i]) + if err != nil { // maybe momo? + continue + //return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) { + baseAmount = decimal.NewFromInt(int64(amount)) + baseFound = true + } else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) { + quoteAmount = decimal.NewFromInt(int64(amount)) + quoteFound = true + } + if baseFound && quoteFound { + break + } + } + if !baseFound && !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("liquidity change failed to find token transfer for both vaults in inner instructions") + } + offset[1] += 2 + if baseAmount.Equal(decimal.Zero) && quoteAmount.Equal(decimal.Zero) { + return nil, increaseOffset(offset), InstructionIgnoredError + } + if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) { + instructionName += "_on_side" + } + if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) { + return nil, offset, fmt.Errorf("token balance is nil but amount is not zero") + } + return []Swap{ + { + Program: SolProgramOrcaWhirPool, + Event: instructionName, + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseProgram, + QuoteTokenProgram: quoteProgram, + Creator: signer, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func orcaWhirPoolLiquidityV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 15 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for instruction") + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + signer := tx.rawTx.accountList[0] + pool := tx.rawTx.accountList[instruction.Accounts[0]] + baseMint := tx.rawTx.accountList[instruction.Accounts[1]] + quoteMint := tx.rawTx.accountList[instruction.Accounts[2]] + vault0 := instruction.Accounts[11] + vault1 := instruction.Accounts[12] + + baseTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault0) + quoteTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault1) + if baseTokenBalance == nil && quoteTokenBalance == nil { + return nil, increaseOffset(offset), InstructionIgnoredError + } + var ( + baseReserve decimal.Decimal + quoteReserve decimal.Decimal + + baseMintDecimals uint8 + quoteMintDecimals uint8 + + baseProgram solana.PublicKey + quoteProgram solana.PublicKey + ) + if baseTokenBalance != nil { + baseReserve, _ = decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + baseMintDecimals = uint8(baseTokenBalance.UITokenAmount.Decimals) + baseProgram = baseTokenBalance.ProgramIDAccount + } + if quoteTokenBalance != nil { + quoteReserve, _ = decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + quoteMintDecimals = uint8(quoteTokenBalance.UITokenAmount.Decimals) + quoteProgram = quoteTokenBalance.ProgramIDAccount + } + + discriminator := *(*[8]byte)(instruction.Data[:8]) + var instructionName string + if discriminator == orcaDecreaseLiquidityV2Discriminator { + instructionName = "remove_liquidity" + } else if discriminator == orcaIncreaseLiquidityV2Discriminator { + instructionName = "add_liquidity" + } + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if len(inners) < 2 { + return nil, increaseOffset(offset), fmt.Errorf("not enough inner instructions for protocol fee collection") + } + baseAmount := decimal.Zero + quoteAmount := decimal.Zero + var baseFound, quoteFound bool + for i := 0; i < 3; i++ { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inners[i]) + if err != nil { // maybe momo? + continue + } + if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) { + baseAmount = decimal.NewFromInt(int64(amount)) + baseFound = true + } else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) { + quoteAmount = decimal.NewFromInt(int64(amount)) + quoteFound = true + } + if baseFound && quoteFound { + break + } + } + if !baseFound && !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("liquidity change failed to find token transfer for both vaults in inner instructions") + } + offset[1] += 2 + if baseAmount.Equal(decimal.Zero) && quoteAmount.Equal(decimal.Zero) { + return nil, offset, InstructionIgnoredError + } + if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) { + instructionName += "_on_side" + } + if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) { + return nil, offset, fmt.Errorf("token balance is nil but amount is not zero") + } + return []Swap{ + { + Program: SolProgramOrcaWhirPool, + Event: instructionName, + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseProgram, + QuoteTokenProgram: quoteProgram, + Creator: signer, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func orcaWhirPoolCollectFeeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 9 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize pool instruction") + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + signer := tx.rawTx.accountList[0] + pool := tx.rawTx.accountList[instruction.Accounts[0]] + vault0 := instruction.Accounts[5] + vault1 := instruction.Accounts[7] + + baseTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault0) + quoteTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault1) + if baseTokenBalance == nil && quoteTokenBalance == nil { + return nil, increaseOffset(offset), InstructionIgnoredError + } + var ( + baseReserve decimal.Decimal + quoteReserve decimal.Decimal + + baseMintDecimals uint8 + quoteMintDecimals uint8 + + baseMint solana.PublicKey + quoteMint solana.PublicKey + + baseProgram solana.PublicKey + quoteProgram solana.PublicKey + ) + if baseTokenBalance != nil { + baseReserve, _ = decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + baseMintDecimals = uint8(baseTokenBalance.UITokenAmount.Decimals) + baseProgram = baseTokenBalance.ProgramIDAccount + baseMint = baseTokenBalance.MintAccount + } + if quoteTokenBalance != nil { + quoteReserve, _ = decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + quoteMintDecimals = uint8(quoteTokenBalance.UITokenAmount.Decimals) + quoteProgram = quoteTokenBalance.ProgramIDAccount + quoteMint = quoteTokenBalance.MintAccount + } + + var instructionName = "remove_liquidity" + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if len(inners) < 2 { + return nil, increaseOffset(offset), fmt.Errorf("not enough inner instructions for protocol fee collection") + } + baseAmount := decimal.Zero + quoteAmount := decimal.Zero + var baseFound, quoteFound bool + for i, inner := range inners { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + continue + //return nil, increaseOffset(offset), fmt.Errorf("orca whirpool parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) { + baseAmount = decimal.NewFromInt(int64(amount)) + baseFound = true + } else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) { + quoteAmount = decimal.NewFromInt(int64(amount)) + quoteFound = true + } + if (baseFound && quoteFound) || i >= 6 { + break + } + } + if !baseFound && !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("collect fee failed to find token transfer for both vaults in inner instructions") + } + offset[1] += 2 + if baseAmount.Equal(decimal.Zero) && quoteAmount.Equal(decimal.Zero) { + return nil, offset, InstructionIgnoredError + } + if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) { + instructionName += "_on_side" + } + if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) { + return nil, offset, fmt.Errorf("token balance is nil but amount is not zero") + } + return []Swap{ + { + Program: SolProgramOrcaWhirPool, + Event: instructionName, + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseProgram, + QuoteTokenProgram: quoteProgram, + Creator: signer, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func orcaWhirPoolCollectFeeV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 12 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize pool instruction") + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + signer := tx.rawTx.accountList[0] + pool := tx.rawTx.accountList[instruction.Accounts[0]] + baseMint := tx.rawTx.accountList[instruction.Accounts[4]] + quoteMint := tx.rawTx.accountList[instruction.Accounts[5]] + vault0 := instruction.Accounts[7] + vault1 := instruction.Accounts[9] + + baseTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault0) + quoteTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault1) + if baseTokenBalance == nil && quoteTokenBalance == nil { + return nil, increaseOffset(offset), InstructionIgnoredError + } + var ( + baseReserve decimal.Decimal + quoteReserve decimal.Decimal + + baseMintDecimals uint8 + quoteMintDecimals uint8 + + baseProgram solana.PublicKey + quoteProgram solana.PublicKey + ) + if baseTokenBalance != nil { + baseReserve, _ = decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + baseMintDecimals = uint8(baseTokenBalance.UITokenAmount.Decimals) + baseProgram = baseTokenBalance.ProgramIDAccount + } + if quoteTokenBalance != nil { + quoteReserve, _ = decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + quoteMintDecimals = uint8(quoteTokenBalance.UITokenAmount.Decimals) + quoteProgram = quoteTokenBalance.ProgramIDAccount + } + + var instructionName = "remove_liquidity" + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if len(inners) < 2 { + return nil, increaseOffset(offset), fmt.Errorf("not enough inner instructions for protocol fee collection") + } + baseAmount := decimal.Zero + quoteAmount := decimal.Zero + var baseFound, quoteFound bool + for i, inner := range inners { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + continue + //return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) { + baseAmount = decimal.NewFromInt(int64(amount)) + baseFound = true + } else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) { + quoteAmount = decimal.NewFromInt(int64(amount)) + quoteFound = true + } + if (baseFound && quoteFound) || i >= 6 { + break + } + } + if !baseFound && !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf(" collect fee v2 failed to find token transfer for both vaults in inner instructions") + } + offset[1] += 2 + if baseAmount.Equal(decimal.Zero) && quoteAmount.Equal(decimal.Zero) { + return nil, offset, InstructionIgnoredError + } + if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) { + instructionName += "_on_side" + } + if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) { + return nil, offset, fmt.Errorf("token balance is nil but amount is not zero") + } + return []Swap{ + { + Program: SolProgramOrcaWhirPool, + Event: instructionName, + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseProgram, + QuoteTokenProgram: quoteProgram, + Creator: signer, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func orcaWhirPoolCollectProtocolFeeV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 12 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize pool instruction") + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + signer := tx.rawTx.accountList[0] + pool := tx.rawTx.accountList[instruction.Accounts[1]] + baseMint := tx.rawTx.accountList[instruction.Accounts[3]] + quoteMint := tx.rawTx.accountList[instruction.Accounts[4]] + vault0 := instruction.Accounts[5] + vault1 := instruction.Accounts[6] + + baseTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault0) + quoteTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault1) + if baseTokenBalance == nil && quoteTokenBalance == nil { + return nil, increaseOffset(offset), InstructionIgnoredError + } + var ( + baseReserve decimal.Decimal + quoteReserve decimal.Decimal + + baseMintDecimals uint8 + quoteMintDecimals uint8 + + baseProgram solana.PublicKey + quoteProgram solana.PublicKey + ) + if baseTokenBalance != nil { + baseReserve, _ = decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + baseMintDecimals = uint8(baseTokenBalance.UITokenAmount.Decimals) + baseProgram = baseTokenBalance.ProgramIDAccount + } + if quoteTokenBalance != nil { + quoteReserve, _ = decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + quoteMintDecimals = uint8(quoteTokenBalance.UITokenAmount.Decimals) + quoteProgram = quoteTokenBalance.ProgramIDAccount + } + + var instructionName = "remove_liquidity" + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if len(inners) < 2 { + return nil, increaseOffset(offset), fmt.Errorf("not enough inner instructions for protocol fee collection") + } + baseAmount := decimal.Zero + quoteAmount := decimal.Zero + var baseFound, quoteFound bool + for i, inner := range inners { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + continue + // return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) { + baseAmount = decimal.NewFromInt(int64(amount)) + baseFound = true + } else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) { + quoteAmount = decimal.NewFromInt(int64(amount)) + quoteFound = true + } + if (baseFound && quoteFound) || i >= 6 { + break + } + } + if !baseFound && !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("collect protocol fee failed to find token transfer for both vaults in inner instructions") + } + offset[1] += 2 + if baseAmount.Equal(decimal.Zero) && quoteAmount.Equal(decimal.Zero) { + return nil, offset, InstructionIgnoredError + } + if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) { + instructionName += "_on_side" + } + if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) { + return nil, offset, fmt.Errorf("token balance is nil but amount is not zero") + } + return []Swap{ + { + Program: SolProgramOrcaWhirPool, + Event: instructionName, + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseProgram, + QuoteTokenProgram: quoteProgram, + Creator: signer, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 11 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction") + } + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + user := tx.rawTx.accountList[instruction.Accounts[1]] + pool := tx.rawTx.accountList[instruction.Accounts[2]] + + token0Account := tx.rawTx.accountList[instruction.Accounts[3]] + token1Account := tx.rawTx.accountList[instruction.Accounts[5]] + + user0 := instruction.Accounts[3] + user1 := instruction.Accounts[5] + vault0 := instruction.Accounts[4] + vault1 := instruction.Accounts[6] + + token0Account = tx.rawTx.accountList[user0] + token1Account = tx.rawTx.accountList[user1] + vault0Account := tx.rawTx.accountList[vault0] + vault1Account := tx.rawTx.accountList[vault1] + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) + } + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + userBase := getAccountBalanceAfterTx(tx.rawTx, user0) + userQuote := getAccountBalanceAfterTx(tx.rawTx, user1) + + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swap get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var baseAmount = decimal.Zero + var quoteAmount = decimal.Zero + var event string + var baseFound, quoteFound bool + for i, inner := range inners { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) { + baseAmount = decimal.NewFromInt(int64(amount)) + if from.Equals(vault0Account) && to.Equals(token0Account) { + event = "buy" + } else if from.Equals(token0Account) && to.Equals(vault0Account) { + event = "sell" + } + baseFound = true + } else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) { + quoteAmount = decimal.NewFromInt(int64(amount)) + if from.Equals(vault1Account) && to.Equals(token1Account) { + event = "sell" + } else if from.Equals(token1Account) && to.Equals(vault1Account) { + event = "buy" + } + quoteFound = true + } + + if i >= 1 || (baseFound && quoteFound) { + break + } + } + offset[1] += 2 + if !baseFound || !quoteFound { + return nil, offset, fmt.Errorf("orca whirpool swap failed to find both base and quote token transfer in inner instructions") + } + + return []Swap{ + { + Program: SolProgramOrcaWhirPool, + Event: event, + Pool: pool, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + User: user, + EntryContract: entryContract, + }, + }, offset, nil +} + +func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 15 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction") + } + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + user := tx.rawTx.accountList[instruction.Accounts[3]] + pool := tx.rawTx.accountList[instruction.Accounts[4]] + + user0 := instruction.Accounts[7] + user1 := instruction.Accounts[9] + vault0 := instruction.Accounts[8] + vault1 := instruction.Accounts[10] + + token0Account := tx.rawTx.accountList[user0] + token1Account := tx.rawTx.accountList[user1] + vault0Account := tx.rawTx.accountList[vault0] + vault1Account := tx.rawTx.accountList[vault1] + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) + } + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + userBase := getAccountBalanceAfterTx(tx.rawTx, user0) + userQuote := getAccountBalanceAfterTx(tx.rawTx, user1) + + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swap get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var baseAmount = decimal.Zero + var quoteAmount = decimal.Zero + var event string + var baseFound, quoteFound bool + var skipOffset = 0 + for i, inner := range inners { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + if i <= 1 { //maybe momo?? + continue + } + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swapv2 parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) { + baseAmount = decimal.NewFromInt(int64(amount)) + if from.Equals(vault0Account) && to.Equals(token0Account) { + event = "buy" + } else if from.Equals(token0Account) && to.Equals(vault0Account) { + event = "sell" + } + baseFound = true + } else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) { + quoteAmount = decimal.NewFromInt(int64(amount)) + if from.Equals(vault1Account) && to.Equals(token1Account) { + event = "sell" + } else if from.Equals(token1Account) && to.Equals(vault1Account) { + event = "buy" + } + quoteFound = true + } + if baseFound && quoteFound { + skipOffset = i + break + } + } + + if !baseFound || !quoteFound { + return nil, offset, fmt.Errorf("orca whirpool swapV2 failed to find both base and quote token transfer in inner instructions") + } + offset[1] += uint(skipOffset + 1) + + return []Swap{ + { + Program: SolProgramOrcaWhirPool, + Event: event, + Pool: pool, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + User: user, + EntryContract: entryContract, + }, + }, offset, nil +} + +func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 12 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for twoHopSwap instruction") + } + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + user := tx.rawTx.accountList[instruction.Accounts[1]] + pool1 := tx.rawTx.accountList[instruction.Accounts[2]] + pool2 := tx.rawTx.accountList[instruction.Accounts[3]] + + pool1UserBase := instruction.Accounts[4] + pool1VaultBase := instruction.Accounts[5] + + pool1UserQuote := instruction.Accounts[6] + pool1VaultQuote := instruction.Accounts[7] + + pool2UserBase := instruction.Accounts[8] + pool2VaultBase := instruction.Accounts[9] + + pool2UserQuote := instruction.Accounts[10] + pool2VaultQuote := instruction.Accounts[11] + + swaps := make([]Swap, 2) + { + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool1VaultBase) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token0 vault balance after tx: %v", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool1VaultQuote) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token1 vault balance after tx: %v", err) + } + + userBase := getAccountBalanceAfterTx(tx.rawTx, pool1UserBase) + userQuote := getAccountBalanceAfterTx(tx.rawTx, pool1UserQuote) + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + + var baseAmount = decimal.Zero + var quoteAmount = decimal.Zero + var event string + inners, err := getInnerInstructions(innerInstructions, offset[1]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var nextInstructionIndex = uint(0) + var baseFound, quoteFound bool + for i, inner := range inners { + if !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.Token2022ProgramID) && !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.TokenProgramID) { + continue + } + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) { + baseAmount = decimal.NewFromInt(int64(amount)) + if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) { + event = "buy" + } else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) { + event = "sell" + } + baseFound = true + } else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) { + quoteAmount = decimal.NewFromInt(int64(amount)) + if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool1UserQuote]) { + event = "sell" + } else if from.Equals(tx.rawTx.accountList[pool1UserQuote]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) { + event = "buy" + } + quoteFound = true + } + nextInstructionIndex = uint(i + 1) + if baseFound && quoteFound { + break + } + } + offset[1] += nextInstructionIndex + swaps[0] = Swap{ + Program: SolProgramOrcaWhirPool, + Event: event, + Pool: pool1, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + User: user, + EntryContract: entryContract, + } + } + + { + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultBase) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token0 vault balance after tx: %v", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultQuote) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token1 vault balance after tx: %v", err) + } + + userBase := getAccountBalanceAfterTx(tx.rawTx, pool2UserBase) + userQuote := getAccountBalanceAfterTx(tx.rawTx, pool2UserQuote) + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + var baseAmount = decimal.Zero + var quoteAmount = decimal.Zero + var event string + inners, err := getInnerInstructions(innerInstructions, offset[1]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var baseFound, quoteFound bool + var nextInstructionIndex = uint(0) + for i, inner := range inners { + if !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.Token2022ProgramID) && !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.TokenProgramID) { + continue + } + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) { + baseAmount = decimal.NewFromInt(int64(amount)) + if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool2UserBase]) { + event = "buy" + } else if from.Equals(tx.rawTx.accountList[pool2UserBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) { + event = "sell" + } + baseFound = true + } else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) { + quoteAmount = decimal.NewFromInt(int64(amount)) + if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) { + event = "sell" + } else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) { + event = "buy" + } + quoteFound = true + } + nextInstructionIndex = uint(i + 1) + if baseFound && quoteFound { + break + } + } + offset[1] += nextInstructionIndex + swaps[1] = Swap{ + Program: SolProgramOrcaWhirPool, + Event: event, + Pool: pool2, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + User: user, + EntryContract: entryContract, + } + } + return swaps, offset, nil +} + +func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 15 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for twoHopSwapV2 instruction") + } + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + user := tx.rawTx.accountList[instruction.Accounts[14]] + pool1 := tx.rawTx.accountList[instruction.Accounts[0]] + pool2 := tx.rawTx.accountList[instruction.Accounts[1]] + + pool1UserBase := instruction.Accounts[8] + pool1VaultBase := instruction.Accounts[9] + + //pool1UserQuote := instruction.Accounts[6] + pool1VaultQuote := instruction.Accounts[10] + + //pool2UserBase := instruction.Accounts[8] + pool2VaultBase := instruction.Accounts[11] + + pool2UserQuote := instruction.Accounts[12] + pool2VaultQuote := instruction.Accounts[13] + + swaps := make([]Swap, 2) + { + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool1VaultBase) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token0 vault balance after tx: %v", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool1VaultQuote) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token1 vault balance after tx: %v", err) + } + + userBase := getAccountBalanceAfterTx(tx.rawTx, pool1UserBase) + userQuote := decimal.Zero + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + + var baseAmount = decimal.Zero + var quoteAmount = decimal.Zero + var event string + inners, err := getInnerInstructions(innerInstructions, offset[1]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var baseFound, quoteFound bool + for _, inner := range inners { + if !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.Token2022ProgramID) && !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.TokenProgramID) { + continue + } + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) { + baseAmount = decimal.NewFromInt(int64(amount)) + if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) { + event = "buy" + } else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) { + event = "sell" + } + baseFound = true + } else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) { + quoteAmount = decimal.NewFromInt(int64(amount)) + if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) { + event = "sell" + } else if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) { + event = "buy" + } + quoteFound = true + } + if baseFound && quoteFound { + break + } + } + swaps[0] = Swap{ + Program: SolProgramOrcaWhirPool, + Event: event, + Pool: pool1, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + User: user, + EntryContract: entryContract, + } + } + + { + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultBase) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token0 vault balance after tx: %v", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultQuote) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token1 vault balance after tx: %v", err) + } + + userBase := decimal.Zero + userQuote := getAccountBalanceAfterTx(tx.rawTx, pool2UserQuote) + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + var baseAmount = decimal.Zero + var quoteAmount = decimal.Zero + var event string + inners, err := getInnerInstructions(innerInstructions, offset[1]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var baseFound, quoteFound bool + var nextInstructionIndex = uint(0) + for i, inner := range inners { + if !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.Token2022ProgramID) && !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.TokenProgramID) { + continue + } + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) { + baseAmount = decimal.NewFromInt(int64(amount)) + if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) { + event = "buy" + } else if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) { + event = "sell" + } + baseFound = true + } else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) { + quoteAmount = decimal.NewFromInt(int64(amount)) + if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) { + event = "sell" + } else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) { + event = "buy" + } + quoteFound = true + } + nextInstructionIndex = uint(i + 1) + if baseFound && quoteFound { + break + } + } + offset[1] += nextInstructionIndex + swaps[1] = Swap{ + Program: SolProgramOrcaWhirPool, + Event: event, + Pool: pool2, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + User: user, + EntryContract: entryContract, + } + } + return swaps, offset, nil +} diff --git a/parser.go b/parser.go index d0db04d..bc7fbc5 100644 --- a/parser.go +++ b/parser.go @@ -3,6 +3,7 @@ package pump_parser import ( "errors" "log" + "slices" "github.com/gagliardetto/solana-go" "github.com/shopspring/decimal" @@ -21,6 +22,20 @@ type parserConfig struct { enableMeteoraDlmm bool } +func EnableAllParsers() { + programs := cloneSwapPrograms(defaultSwapPrograms) + programs[meteoraDlmmProgram] = metaoradlmmParser + programs[metaoraPoolProgramID] = metaoraPoolParser + programs[metaoraBcProgramID] = metaoraBcParser + programs[meteoraDammV2Program] = metaoraDammParser + programs[orcaProgramID] = orcaWhirPoolParser + programs[raydiumV4Program] = raydiumV4Parser + programs[raydiumClmmProgramID] = raydiumClmmParser + programs[raydiumCPmmProgramID] = raydiumCPmmParser + programs[raydiumLaunchLabProgramID] = raydiumLaunchLabParser + swapPrograms = programs +} + func InitParser(opts ...ParserOption) { cfg := parserConfig{} for _, opt := range opts { @@ -85,7 +100,12 @@ func (tx *Tx) Parser() error { for _, inner := range tx.rawTx.Meta.InnerInstructions { innersMap[inner.Index] = inner } + txIndex := 0 for i, instr := range tx.rawTx.Transaction.Message.Instructions { + txIndex += 1 + if i > 0 { + txIndex += len(innersMap[i-1].Instructions) + } programAccount := accountList[instr.ProgramIDIndex] if p, exists := swapPrograms[programAccount]; exists { swaps, _, err := p(tx, instr, innersMap[i], [2]uint{uint(i), uint(0)}) @@ -95,7 +115,20 @@ func (tx *Tx) Parser() error { } return err } - tx.Swaps = append(tx.Swaps, swaps...) + for k, swap := range swaps { + swap.TxIndex = txIndex + k + if !swap.User.IsOnCurve() { + swap.AfterSOLBalance = tx.AfterSOLBalance + swap.User = tx.rawTx.accountList[0] + } else { + userIdx := slices.Index(tx.rawTx.accountList, swap.User) + if userIdx >= 0 { + swap.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[userIdx]).Div(decimal.NewFromInt(1e9)) + } + } + tx.Swaps = append(tx.Swaps, swap) + } + } else if p, exists := actionPrograms[programAccount]; exists { _, err := p(tx, instr, innersMap[i], [2]uint{uint(i), uint(0)}) if err != nil { @@ -125,7 +158,24 @@ func (tx *Tx) Parser() error { } return err } - tx.Swaps = append(tx.Swaps, swaps...) + for k, swap := range swaps { + swap.TxIndex = txIndex + k + j + // identify okxDexRoutersV2 and okxAggregatorV2 is user + //if !swap.User.IsOnCurve() && (swap.EntryContract.Equals(okxDexRoutersV2) || swap.EntryContract.Equals(okxAggregatorV2)) { + // swap.User = tx.rawTx.accountList[0] + //} + if !swap.User.IsOnCurve() { + swap.AfterSOLBalance = tx.AfterSOLBalance + swap.User = tx.rawTx.accountList[0] + } else { + userIdx := slices.Index(tx.rawTx.accountList, swap.User) + if userIdx >= 0 { + swap.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[userIdx]).Div(decimal.NewFromInt(1e9)) + } + } + tx.Swaps = append(tx.Swaps, swap) + } + // tx.Swaps = append(tx.Swaps, swaps...) j = int(offset[1]) ii = int(offset[0]) } else if p, exists := actionPrograms[innerProgramAccount]; exists { @@ -148,10 +198,54 @@ func (tx *Tx) Parser() error { } } } + // update swaps same program+pair with last reserve balance + if len(tx.Swaps) > 1 { + pairKey := func(s Swap) solana.PublicKey { + // Match pair selection used by downstream consumers. + if s.Program == SolProgramPump { + return s.BaseMint + } + return s.Pool + } + + lastReserve := make(map[solana.PublicKey]reserveSnapshot, len(tx.Swaps)) + for _, swap := range tx.Swaps { + lastReserve[pairKey(swap)] = reserveSnapshot{ + baseMint: swap.BaseMint, + quoteMint: swap.QuoteMint, + baseReserve: swap.BaseReserve, + quoteReserve: swap.QuoteReserve, + } + } + + for i := range tx.Swaps { + key := pairKey(tx.Swaps[i]) + if v, ok := lastReserve[key]; ok { + if tx.Swaps[i].BaseMint == v.baseMint && tx.Swaps[i].QuoteMint == v.quoteMint { + tx.Swaps[i].BaseReserve = v.baseReserve + tx.Swaps[i].QuoteReserve = v.quoteReserve + } else if tx.Swaps[i].BaseMint == v.quoteMint && tx.Swaps[i].QuoteMint == v.baseMint { + tx.Swaps[i].BaseReserve = v.quoteReserve + tx.Swaps[i].QuoteReserve = v.baseReserve + } + //else { + // tx.Swaps[i].BaseReserve = v.baseReserve + // tx.Swaps[i].QuoteReserve = v.quoteReserve + //} + } + } + } return nil } +type reserveSnapshot struct { + baseMint solana.PublicKey + quoteMint solana.PublicKey + baseReserve decimal.Decimal + quoteReserve decimal.Decimal +} + func cloneSwapPrograms(src map[solana.PublicKey]swapParser) map[solana.PublicKey]swapParser { dst := make(map[solana.PublicKey]swapParser, len(src)) for k, v := range src { diff --git a/pump.go b/pump.go index 0b3f5e0..5d79d36 100644 --- a/pump.go +++ b/pump.go @@ -27,7 +27,6 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct decode := instruction.Data if len(decode) < 8 { - offset[1] += 1 return nil, increaseOffset(offset), fmt.Errorf("pump program instruction data too short, offset, %d, %d", offset[0], offset[1]) } @@ -101,7 +100,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions if offset[1] == 0 { offset[0] += 1 } else { - offset[1] += uint(innerIndex) + 1 + prefixLen + offset[1] = uint(innerIndex) + 1 + prefixLen } if err != nil { return nil, offset, fmt.Errorf("pump create event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) @@ -470,12 +469,14 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction User: migrateEvent.User, //BaseAmount: decimal.Decimal{}, //QuoteAmount: decimal.Decimal{}, - BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount), - QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount), - Mayhem: createEvent.IsMayhemMode, - UserBaseBalance: userBase, - UserQuoteBalance: decimal.NewFromUint64(userQuote), - EntryContract: entryContract, + BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount), + QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount), + Mayhem: createEvent.IsMayhemMode, + MigrateTopProgram: pumpAmmProgram, + MigrateToPool: migrateEvent.Pool, + UserBaseBalance: userBase, + UserQuoteBalance: decimal.NewFromUint64(userQuote), + EntryContract: entryContract, }, } swaps = append(swaps, Swap{ diff --git a/pump_test.go b/pump_test.go index 0b5f261..ff5ad80 100644 --- a/pump_test.go +++ b/pump_test.go @@ -1,11 +1,13 @@ package pump_parser import ( + "encoding/binary" "encoding/hex" "fmt" "testing" agbinary "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" "github.com/mr-tron/base58" ) @@ -32,6 +34,12 @@ func TestTradeEvent(t *testing.T) { func TestCal(t *testing.T) { //e445a52e51cb9a1db94afc7d1bd7bc6f5e99e54b // . b94afc7d1bd7bc6f - s := calculateDiscriminator("event:LbPairCreate") + s := calculateDiscriminator("global:initialize_with_permission") fmt.Println(hex.EncodeToString(s[:])) + + s2, _ := base58.Decode("6ApXSNCamGdm") + s3 := binary.LittleEndian.Uint64(s2[1:]) + fmt.Println(s2, s3) + + fmt.Println(solana.MustPublicKeyFromBase58("BM9CcyErJcu2mjrFvUsRRrD3snGeHDDVirJLvL6EjvMN").IsOnCurve()) } diff --git a/pumpamm.go b/pumpamm.go index b3f5052..18356fe 100644 --- a/pumpamm.go +++ b/pumpamm.go @@ -180,7 +180,7 @@ func ammCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions Inne if offset[1] == 0 { offset[0] += 1 } else { - offset[1] += uint(innerIndex) + 1 + prefixLen + offset[1] = uint(innerIndex) + 1 + prefixLen } if err != nil { return nil, offset, fmt.Errorf("pump amm create pool event decode error: %v, offset: %d, %d", err, offset[0], prefixLen) @@ -262,7 +262,7 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru if offset[1] == 0 { offset[0] += 1 } else { - offset[1] += uint(innerIndex) + 1 + prefixLen + offset[1] = uint(innerIndex) + 1 + prefixLen } if err != nil { return nil, offset, fmt.Errorf("pump amm buy pool event decode error: %v, offset: %d, %d", err, offset[0], prefixLen) @@ -380,7 +380,7 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr if offset[1] == 0 { offset[0] += 1 } else { - offset[1] += uint(innerIndex) + 1 + prefixLen + offset[1] = uint(innerIndex) + 1 + prefixLen } if err != nil { return nil, offset, fmt.Errorf("pump amm sell pool event decode error: %v, offset: %d, %d", err, offset[0], prefixLen) @@ -491,7 +491,7 @@ func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstru if offset[1] == 0 { offset[0] += 1 } else { - offset[1] += uint(innerIndex) + 1 + prefixLen + offset[1] = uint(innerIndex) + 1 + prefixLen } if err != nil { return nil, offset, fmt.Errorf("pump amm deposit pool event decode error: %v, offset: %d, %d", err, offset[0], prefixLen) @@ -589,7 +589,7 @@ func withdrawParse(tx *Tx, instruction Instruction, innerInstructions InnerInstr if offset[1] == 0 { offset[0] += 1 } else { - offset[1] += uint(innerIndex) + 1 + prefixLen + offset[1] = uint(innerIndex) + 1 + prefixLen } if err != nil { return nil, offset, fmt.Errorf("pump amm withdraw pool event decode error: %v, offset: %d, %d", err, offset[0], prefixLen) diff --git a/raydiumclmm.go b/raydiumclmm.go new file mode 100644 index 0000000..43e2b03 --- /dev/null +++ b/raydiumclmm.go @@ -0,0 +1,375 @@ +package pump_parser + +import ( + "fmt" + + "github.com/gagliardetto/solana-go" + "github.com/shopspring/decimal" +) + +func raydiumClmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumClmmProgramID) { + return nil, increaseOffset(offset), fmt.Errorf("raydiumClmm instruction not found, offset, %d, %d", offset[0], offset[1]) + } + + decode := instruction.Data + if len(decode) < 8 { + return nil, increaseOffset(offset), fmt.Errorf("raydiumClmm program instruction data too short, offset, %d, %d", offset[0], offset[1]) + } + + discriminator := *(*[8]byte)(decode[:8]) + + switch discriminator { + case raydiumClmmCreatePoolDiscriminator: + return raydiumClmmCreatePoolParser(tx, instruction, innerInstructions, offset) + case raydiumClmmIncreaseLiquidityDiscriminator, raydiumClmmIncreaseLiquidityV2Discriminator, raydiumClmmOpenPositionDiscriminator, raydiumClmmOpenPositionV2Discriminator, raydiumClmmOpenPositionWithToken22NftDiscriminator: + return raydiumClmmAddLiquidityParser(tx, instruction, innerInstructions, offset) + case raydiumClmmDecreaseLiquidityDiscriminator, raydiumClmmDecreaseLiquidityV2Discriminator: + return raydiumClmmDecreaseLiquidityParser(tx, instruction, innerInstructions, offset) + case raydiumClmmCollectFundFeeDiscriminator, raydiumClmmCollectProtocolFeeDiscriminator: + return raydiumClmmCollectFeeParser(tx, instruction, innerInstructions, offset) + case raydiumClmmSwapDiscriminator, raydiumClmmSwapV2Discriminator: + return raydiumClmmSwapParser(tx, instruction, innerInstructions, offset) + default: + return nil, increaseOffset(offset), InstructionIgnoredError + } +} + +func raydiumClmmCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 13 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for raydiumClmm create pool instruction, offset, %d, %d", offset[0], offset[1]) + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + pool := tx.rawTx.accountList[instruction.Accounts[2]] + creator := tx.rawTx.accountList[instruction.Accounts[0]] + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[5]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) + } + + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[6]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) + } + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + offset[1] += 9 + return []Swap{ + { + Program: SolProgramRaydiumCLMM, + Event: "create", + Pool: pool, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + Creator: creator, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func raydiumClmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + discriminator := *(*[8]byte)(instruction.Data[:8]) + var ( + accountMin int + market solana.PublicKey + //token0 solana.PublicKey + //token1 solana.PublicKey + lpToken solana.PublicKey + vault0 int + vault1 int + ) + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + switch discriminator { + case raydiumClmmIncreaseLiquidityDiscriminator: + accountMin = 12 + market = tx.rawTx.accountList[instruction.Accounts[2]] + vault0 = instruction.Accounts[9] + vault1 = instruction.Accounts[10] + case raydiumClmmIncreaseLiquidityV2Discriminator: + accountMin = 15 + market = tx.rawTx.accountList[instruction.Accounts[2]] + vault0 = instruction.Accounts[9] + vault1 = instruction.Accounts[10] + //token0 = tx.rawTx.accountList[instruction.Accounts[13]] + //token1 = tx.rawTx.accountList[instruction.Accounts[14]] + case raydiumClmmOpenPositionDiscriminator: + accountMin = 19 + market = tx.rawTx.accountList[instruction.Accounts[5]] + vault0 = instruction.Accounts[12] + vault1 = instruction.Accounts[13] + lpToken = tx.rawTx.accountList[instruction.Accounts[2]] + case raydiumClmmOpenPositionV2Discriminator: + accountMin = 22 + market = tx.rawTx.accountList[instruction.Accounts[5]] + vault0 = instruction.Accounts[12] + vault1 = instruction.Accounts[13] + lpToken = tx.rawTx.accountList[instruction.Accounts[2]] + //token0 = tx.rawTx.accountList[instruction.Accounts[20]] + //token1 = tx.rawTx.accountList[instruction.Accounts[21]] + case raydiumClmmOpenPositionWithToken22NftDiscriminator: + accountMin = 20 + market = tx.rawTx.accountList[instruction.Accounts[4]] + vault0 = instruction.Accounts[11] + vault1 = instruction.Accounts[12] + lpToken = tx.rawTx.accountList[instruction.Accounts[2]] + //token0 = tx.rawTx.accountList[instruction.Accounts[18]] + //token1 = tx.rawTx.accountList[instruction.Accounts[19]] + default: + return nil, increaseOffset(offset), fmt.Errorf("invalid discriminator") + } + + if len(instruction.Accounts) < accountMin { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for raydiumClmm add liquidity instruction, offset, %d, %d", offset[0], offset[1]) + } + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) + } + + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) + } + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + + offset[1] += 2 + + return []Swap{ + { + Program: SolProgramRaydiumCLMM, + Event: "add_liquidity", + Pool: market, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + LpMint: lpToken, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func raydiumClmmDecreaseLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + discriminator := *(*[8]byte)(instruction.Data[:8]) + var ( + accountMin int + market solana.PublicKey + vault0 int + vault1 int + ) + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + if discriminator == raydiumClmmDecreaseLiquidityDiscriminator { + accountMin = 14 + } else if discriminator == raydiumClmmDecreaseLiquidityV2Discriminator { + accountMin = 16 + } + if len(instruction.Accounts) < accountMin { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for decrease liquidity instruction") + } + market = tx.rawTx.accountList[instruction.Accounts[3]] + vault0 = instruction.Accounts[5] + vault1 = instruction.Accounts[6] + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) + } + + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) + } + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + + offset[1] += 2 + + return []Swap{ + { + Program: SolProgramRaydiumCLMM, + Event: "remove_liquidity", + Pool: market, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func raydiumClmmCollectFeeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 11 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for CollectFeeParser instruction") + } + pool := tx.rawTx.accountList[instruction.Accounts[1]] + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + vault0 := instruction.Accounts[3] + vault1 := instruction.Accounts[4] + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) + } + + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) + } + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + + offset[1] += 2 + + return []Swap{ + { + Program: SolProgramRaydiumCLMM, + Event: "remove_liquidity", + Pool: pool, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + discriminator := *(*[8]byte)(instruction.Data[:8]) + var ( + pool solana.PublicKey + + accountMin int + tokenInVault int + tokenOutVault int + userTokenInAccount int + userTokenOutAccount int + ) + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + if discriminator == raydiumClmmSwapDiscriminator { + accountMin = 9 + pool = tx.rawTx.accountList[instruction.Accounts[2]] + userTokenInAccount = instruction.Accounts[3] + userTokenOutAccount = instruction.Accounts[4] + tokenInVault = instruction.Accounts[5] + tokenOutVault = instruction.Accounts[6] + } else if discriminator == raydiumClmmSwapV2Discriminator { + accountMin = 13 + pool = tx.rawTx.accountList[instruction.Accounts[2]] + userTokenInAccount = instruction.Accounts[3] + userTokenOutAccount = instruction.Accounts[4] + tokenInVault = instruction.Accounts[5] + tokenOutVault = instruction.Accounts[6] + } + if len(instruction.Accounts) < accountMin { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction") + } + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, tokenInVault) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get tokenIn vault balance after tx: %w", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, tokenOutVault) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get tokenOut vault balance after tx: %w", err) + } + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + baseTokenProgram := baseTokenBalance.ProgramIDAccount + quoteTokenProgram := quoteTokenBalance.ProgramIDAccount + baseMint := baseTokenBalance.MintAccount + quoteMint := quoteTokenBalance.MintAccount + baseMintDecimals := uint8(baseTokenBalance.UITokenAmount.Decimals) + quoteMintDecimals := uint8(quoteTokenBalance.UITokenAmount.Decimals) + + userBase := getAccountBalanceAfterTx(tx.rawTx, userTokenInAccount) + userQuote := getAccountBalanceAfterTx(tx.rawTx, userTokenOutAccount) + + inners, err := getInnerInstructions(innerInstructions, offset[1]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get inner instructions: %w", err) + } + if len(inners) < 2 { + return nil, increaseOffset(offset), fmt.Errorf("not enough inner instructions for swap instruction") + } + baseVaultAccount := tx.rawTx.accountList[tokenInVault] + quoteVaultAccount := tx.rawTx.accountList[tokenOutVault] + userBaseAccount := tx.rawTx.accountList[userTokenInAccount] + userQuoteAccount := tx.rawTx.accountList[userTokenOutAccount] + var baseAmount, quoteAmount decimal.Decimal + var baseFound, quoteFound bool + for i := 0; i < 2; i++ { + inner := inners[i] + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to parse token transfer: %w", err) + } + if from.Equals(userBaseAccount) && to.Equals(baseVaultAccount) && !baseFound { + baseAmount = decimal.NewFromUint64(amount) + baseFound = true + } else if from.Equals(quoteVaultAccount) && to.Equals(userQuoteAccount) && !quoteFound { + quoteAmount = decimal.NewFromUint64(amount) + quoteFound = true + } + } + if !baseFound || !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions") + } + + offset[1] += 2 + + return []Swap{ + { + Program: SolProgramRaydiumCLMM, + Event: "sell", + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + User: tx.rawTx.accountList[instruction.Accounts[0]], + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + EntryContract: entryContract, + }, + }, offset, nil + +} diff --git a/raydiumcpmm.go b/raydiumcpmm.go new file mode 100644 index 0000000..d12af05 --- /dev/null +++ b/raydiumcpmm.go @@ -0,0 +1,408 @@ +package pump_parser + +import ( + "bytes" + "fmt" + + "github.com/shopspring/decimal" +) + +func raydiumCPmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumCPmmProgramID) { + return nil, increaseOffset(offset), fmt.Errorf("raydiumCPmm instruction not found, offset, %d, %d", offset[0], offset[1]) + } + + decode := instruction.Data + if len(decode) < 8 { + return nil, increaseOffset(offset), fmt.Errorf("raydiumCPmm program instruction data too short, offset, %d, %d", offset[0], offset[1]) + } + + discriminator := *(*[8]byte)(decode[:8]) + + switch discriminator { + case raydiumCPmmInitializeDiscriminator, raydiumCPmmInitializeWithPermissionDiscriminator: + return raydiumCPmmCreatePoolParser(tx, instruction, innerInstructions, offset) + case raydiumCPmmDepositDiscriminator: + return raydiumCPmmDepositParser(tx, instruction, innerInstructions, offset) + case raydiumCPmmWithdrawDiscriminator: + return raydiumCPmmWithdrawParser(tx, instruction, innerInstructions, offset) + case raydiumCPmmCollectProtocolFeeDiscriminator, raydiumCPmmCollectFundFeeDiscriminator: + return raydiumCPmmCollectParser(tx, instruction, innerInstructions, offset) + case raydiumCPmmSwapBaseInputDiscriminator, raydiumCPmmSwapBaseOutputDiscriminator: + return raydiumCPmmSwapParser(tx, instruction, innerInstructions, offset) + default: + return nil, increaseOffset(offset), InstructionIgnoredError + } +} + +func raydiumCPmmCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 20 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction") + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + pool := tx.rawTx.accountList[instruction.Accounts[3]] + lpMint := tx.rawTx.accountList[instruction.Accounts[6]] + creator := tx.rawTx.accountList[instruction.Accounts[0]] + vault0 := instruction.Accounts[10] + vault1 := instruction.Accounts[11] + if bytes.Equal(instruction.Data[:8], raydiumCPmmInitializeWithPermissionDiscriminator[:]) { + pool = tx.rawTx.accountList[instruction.Accounts[4]] + lpMint = tx.rawTx.accountList[instruction.Accounts[7]] + creator = tx.rawTx.accountList[instruction.Accounts[1]] + vault0 = instruction.Accounts[11] + vault1 = instruction.Accounts[12] + } + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[vault0]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) + } + + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[vault1]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) + } + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + offset[1] += 13 + return []Swap{ + { + Program: SolProgramRaydiumCPMM, + Event: "create", + Pool: pool, + BaseMint: baseTokenBalance.MintAccount, + QuoteMint: quoteTokenBalance.MintAccount, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + Creator: creator, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + LpMint: lpMint, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, increaseOffset(offset), nil +} + +func raydiumCPmmDepositParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 13 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for deposit instruction") + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + market := tx.rawTx.accountList[instruction.Accounts[2]] + token0User := tx.rawTx.accountList[instruction.Accounts[4]] + token1User := tx.rawTx.accountList[instruction.Accounts[5]] + + token0Vault := tx.rawTx.accountList[instruction.Accounts[6]] + token1Vault := tx.rawTx.accountList[instruction.Accounts[7]] + + token0 := tx.rawTx.accountList[instruction.Accounts[10]] + token1 := tx.rawTx.accountList[instruction.Accounts[11]] + lpTokenMint := tx.rawTx.accountList[instruction.Accounts[12]] + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[6]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) + } + + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[7]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) + } + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + + inners, err := getInnerInstructions(innerInstructions, offset[1]) + + var baseFound, quoteFound bool + var baseAmount, quoteAmount decimal.Decimal + for _, inner := range inners { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + continue + } + if from.Equals(token0User) && to.Equals(token0Vault) && !baseFound { + baseFound = true + baseAmount = decimal.NewFromUint64(amount) + } else if from.Equals(token1User) && to.Equals(token1Vault) && !quoteFound { + quoteFound = true + quoteAmount = decimal.NewFromUint64(amount) + } + if baseFound && quoteFound { + break + } + } + if !baseFound || !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions") + } + offset[1] += 3 + return []Swap{ + { + Program: SolProgramRaydiumCPMM, + Event: "add_liquidity", + Pool: market, + BaseMint: token0, + QuoteMint: token1, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + LpMint: lpTokenMint, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func raydiumCPmmWithdrawParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 13 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for deposit instruction") + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + market := tx.rawTx.accountList[instruction.Accounts[2]] + token0User := tx.rawTx.accountList[instruction.Accounts[4]] + token1User := tx.rawTx.accountList[instruction.Accounts[5]] + + token0Vault := tx.rawTx.accountList[instruction.Accounts[6]] + token1Vault := tx.rawTx.accountList[instruction.Accounts[7]] + + token0 := tx.rawTx.accountList[instruction.Accounts[10]] + token1 := tx.rawTx.accountList[instruction.Accounts[11]] + lpTokenMint := tx.rawTx.accountList[instruction.Accounts[12]] + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[6]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) + } + + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[7]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) + } + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + + inners, err := getInnerInstructions(innerInstructions, offset[1]) + + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get inner instructions: %v", err) + } + var baseFound, quoteFound bool + var baseAmount, quoteAmount decimal.Decimal + for _, inner := range inners { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + continue + } + if to.Equals(token0User) && from.Equals(token0Vault) && !baseFound { + baseFound = true + baseAmount = decimal.NewFromUint64(amount) + } else if to.Equals(token1User) && from.Equals(token1Vault) && !quoteFound { + quoteFound = true + quoteAmount = decimal.NewFromUint64(amount) + } + if baseFound && quoteFound { + break + } + } + if !baseFound || !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions") + } + offset[1] += 3 + return []Swap{ + { + Program: SolProgramRaydiumCPMM, + Event: "remove_liquidity", + Pool: market, + BaseMint: token0, + QuoteMint: token1, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + LpMint: lpTokenMint, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func raydiumCPmmCollectParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 12 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for deposit instruction") + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + market := tx.rawTx.accountList[instruction.Accounts[2]] + token0User := tx.rawTx.accountList[instruction.Accounts[8]] + token1User := tx.rawTx.accountList[instruction.Accounts[9]] + + token0Vault := tx.rawTx.accountList[instruction.Accounts[4]] + token1Vault := tx.rawTx.accountList[instruction.Accounts[5]] + + token0 := tx.rawTx.accountList[instruction.Accounts[6]] + token1 := tx.rawTx.accountList[instruction.Accounts[7]] + + inners, err := getInnerInstructions(innerInstructions, offset[1]) + + var baseFound, quoteFound bool + var baseAmount, quoteAmount decimal.Decimal + for _, inner := range inners { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + continue + } + if to.Equals(token0User) && from.Equals(token0Vault) && !baseFound { + baseFound = true + baseAmount = decimal.NewFromUint64(amount) + } else if to.Equals(token1User) && from.Equals(token1Vault) && !quoteFound { + quoteFound = true + quoteAmount = decimal.NewFromUint64(amount) + } + if baseFound && quoteFound { + break + } + } + if !baseFound && !quoteFound { + return nil, increaseOffset(offset), InstructionIgnoredError + } + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[4]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) + } + + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[5]) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) + } + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + + event := "remove_liquidity" + if !baseFound || !quoteFound { + offset[1] += 1 + event = "remove_liquidity_one_side" + } else { + offset[1] += 2 + } + return []Swap{ + { + Program: SolProgramRaydiumCPMM, + Event: event, + Pool: market, + BaseMint: token0, + QuoteMint: token1, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + User: tx.rawTx.accountList[instruction.Accounts[0]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func raydiumCPmmSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 13 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for SwapBaseInputParser instruction") + } + + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + market := tx.rawTx.accountList[instruction.Accounts[3]] + // Get token accounts from instruction + tokenIn := tx.rawTx.accountList[instruction.Accounts[4]] + tokenOut := tx.rawTx.accountList[instruction.Accounts[5]] + user := tx.rawTx.accountList[instruction.Accounts[0]] + + user0 := instruction.Accounts[4] + user1 := instruction.Accounts[5] + + inputVault := tx.rawTx.accountList[instruction.Accounts[6]] + outputVault := tx.rawTx.accountList[instruction.Accounts[7]] + + vault0 := instruction.Accounts[6] + vault1 := instruction.Accounts[7] + + inputTokenMint := tx.rawTx.accountList[instruction.Accounts[10]] + outputTokenMint := tx.rawTx.accountList[instruction.Accounts[11]] + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get input amount: %v", err) + } + + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get output amount: %v", err) + } + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + userBase := getAccountBalanceAfterTx(tx.rawTx, user0) + userQuote := getAccountBalanceAfterTx(tx.rawTx, user1) + + inners, err := getInnerInstructions(innerInstructions, offset[1]) + + var baseFound, quoteFound bool + var baseAmount, quoteAmount decimal.Decimal + for _, inner := range inners { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + continue + } + if from.Equals(tokenIn) && to.Equals(inputVault) && !baseFound { + baseFound = true + baseAmount = decimal.NewFromUint64(amount) + } else if from.Equals(outputVault) && to.Equals(tokenOut) && !quoteFound { + quoteFound = true + quoteAmount = decimal.NewFromUint64(amount) + } + if baseFound && quoteFound { + break + } + } + if !baseFound || !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions") + } + offset[1] += 2 + return []Swap{ + { + Program: SolProgramRaydiumCPMM, + Event: "sell", + Pool: market, + BaseMint: inputTokenMint, + QuoteMint: outputTokenMint, + BaseTokenProgram: baseTokenBalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + User: user, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + EntryContract: entryContract, + }, + }, offset, nil +} diff --git a/raydiumlaunchlab.go b/raydiumlaunchlab.go new file mode 100644 index 0000000..e521379 --- /dev/null +++ b/raydiumlaunchlab.go @@ -0,0 +1,470 @@ +package pump_parser + +import ( + "bytes" + "fmt" + + agbinary "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/shopspring/decimal" +) + +func raydiumLaunchLabParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumLaunchLabProgramID) { + return nil, increaseOffset(offset), fmt.Errorf("raydiumLaunchLab instruction not found, offset, %d, %d", offset[0], offset[1]) + } + + decode := instruction.Data + if len(decode) < 8 { + return nil, increaseOffset(offset), fmt.Errorf("raydiumLaunchLab program instruction data too short, offset, %d, %d", offset[0], offset[1]) + } + discriminator := *(*[8]byte)(decode[:8]) + + switch discriminator { + case raydiumLaunchLabInitializeWithToken2022PoolDiscriminator, raydiumLaunchLabInitializeV2PoolDiscriminator: + return raydiumLaunchLabInitializeParser(tx, instruction, innerInstructions, offset) + case raydiumLaunchLabMigrateToAmmDiscriminator: + return raydiumLaunchLabMigrateToAmmParser(tx, instruction, innerInstructions, offset) + case raydiumLaunchLabMigrateToCpmmDiscriminator: + return raydiumLaunchLabMigrateToCpmmParser(tx, instruction, innerInstructions, offset) + case raydiumLaunchLabSellExactInDiscriminator, + raydiumLaunchLabSellExactOutDiscriminator, + raydiumLaunchLabBuyExactInDiscriminator, + raydiumLaunchLabBuyExactOutDiscriminator: + return raydiumLaunchLabSwapParser(tx, instruction, innerInstructions, offset) + default: + return nil, increaseOffset(offset), InstructionIgnoredError + } +} + +type VestingParam struct { + TotalLockedAmount uint64 + CliffPeriod uint64 + UnlockPeriod uint64 +} + +type CurveParamKind uint8 + +const ( + CurveParamConstant CurveParamKind = 0 + CurveParamFixed CurveParamKind = 1 + CurveParamLinear CurveParamKind = 2 +) + +type CurveParam struct { + // rust enum ConstantCurve/FixedCurve/LinearCurve + Kind CurveParamKind + Constant *ConstantCurve + Fixed *FixedCurve + Linear *LinearCurve +} + +func (c *CurveParam) TotalSupply() uint64 { + switch c.Kind { + case CurveParamConstant: + return c.Constant.TotalSupply + case CurveParamFixed: + return c.Fixed.TotalSupply + case CurveParamLinear: + return c.Linear.TotalSupply + default: + return 0 + } +} + +// UnmarshalWithDecoder 让 agbinary/borsh 解码时走自定义逻辑 +func (c *CurveParam) UnmarshalWithDecoder(dec *agbinary.Decoder) error { + var tag uint8 + if err := dec.Decode(&tag); err != nil { + return fmt.Errorf("decode CurveParam tag: %w", err) + } + + c.Kind = CurveParamKind(tag) + c.Constant, c.Fixed, c.Linear = nil, nil, nil + + switch c.Kind { + case CurveParamConstant: + var v ConstantCurve + if err := dec.Decode(&v); err != nil { + return fmt.Errorf("decode ConstantCurve: %w", err) + } + c.Constant = &v + case CurveParamFixed: + var v FixedCurve + if err := dec.Decode(&v); err != nil { + return fmt.Errorf("decode FixedCurve: %w", err) + } + c.Fixed = &v + case CurveParamLinear: + var v LinearCurve + if err := dec.Decode(&v); err != nil { + return fmt.Errorf("decode LinearCurve: %w", err) + } + c.Linear = &v + default: + return fmt.Errorf("unknown CurveParam tag: %d", tag) + } + return nil +} + +type ConstantCurve struct { + TotalSupply uint64 + TotalBaseSell uint64 + TotalQuoteFundRaising uint64 + MigrateType uint8 +} + +type FixedCurve struct { + TotalSupply uint64 + TotalQuoteFundRaising uint64 + MigrateType uint8 +} + +type LinearCurve struct { + TotalSupply uint64 + TotalQuoteFundRaising uint64 + MigrateType uint8 +} + +type BaseMintParam struct { + Decimals uint8 + Name string + Symbol string + Uri string +} +type RaydiumLaunchLabCreateEvent struct { + Pool solana.PublicKey + Creator solana.PublicKey + Config solana.PublicKey + BaseMintParam BaseMintParam + CurveParam CurveParam + VestingParam VestingParam + ammFeeOn uint8 // 0 or 1, QuoteToken/BaseToken fee on amm swap +} + +func raydiumLaunchLabInitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 16 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction") + } + user := tx.rawTx.accountList[instruction.Accounts[0]] + creator := tx.rawTx.accountList[instruction.Accounts[1]] + pool := tx.rawTx.accountList[instruction.Accounts[5]] + platformConfig := tx.rawTx.accountList[instruction.Accounts[3]] + baseMint := tx.rawTx.accountList[instruction.Accounts[6]] + quoteMint := tx.rawTx.accountList[instruction.Accounts[7]] + baseVaultIdx := instruction.Accounts[8] + quoteVaultIdx := instruction.Accounts[9] + var ( + baseTokenProgram solana.PublicKey + quoteTokenProgram solana.PublicKey + ) + if bytes.Equal(instruction.Data[:8], raydiumLaunchLabInitializeWithToken2022PoolDiscriminator[:]) { + baseTokenProgram = tx.rawTx.accountList[instruction.Accounts[10]] + quoteTokenProgram = tx.rawTx.accountList[instruction.Accounts[11]] + } else if bytes.Equal(instruction.Data[:8], raydiumLaunchLabInitializeV2PoolDiscriminator[:]) { + baseTokenProgram = tx.rawTx.accountList[instruction.Accounts[11]] + quoteTokenProgram = tx.rawTx.accountList[instruction.Accounts[12]] + } + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err) + } + var programName string + if platformConfig.Equals(bonkPlatformConfig) { + programName = SolProgramRaydiumLaunchLabBonk + } else { + programName = SolProgramRaydiumLaunchLab + } + + baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount) + baseDecimals := uint8(baseTokenBalance.UITokenAmount.Decimals) + var createEvent RaydiumLaunchLabCreateEvent + inners, err := getInnerInstructions(innerInstructions, offset[1]) + + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get inner instructions: %v", err) + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + loadedEvent := false + var prefixLen uint = offset[1] + for innerIndex, innerInstruction := range inners { + if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && + bytes.Equal(innerInstruction.Data[:8], pumpEventDiscriminator[:]) && + bytes.Equal(innerInstruction.Data[8:16], raydiumLaunchLabCreatePoolEvnet[:]) && + len(innerInstruction.Accounts) == 1 { + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(innerIndex) + 1 + prefixLen + } + err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&createEvent) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize create event: %w", err) + } + + loadedEvent = true + + break + } + } + + if !loadedEvent { + return nil, increaseOffset(offset), fmt.Errorf("failed to get create event") + } + totalSupply := decimal.NewFromUint64(createEvent.CurveParam.TotalSupply()).Div(decimal.New(1, int32(baseDecimals))) + tx.Token[baseMint] = TokenMeta{ + Mint: baseMint, + TokenProgram: baseTokenProgram, + Decimals: baseDecimals, + Name: createEvent.BaseMintParam.Name, + Symbol: createEvent.BaseMintParam.Symbol, + Url: createEvent.BaseMintParam.Uri, + TotalSupply: &totalSupply, + } + return []Swap{{ + Program: programName, + Event: "create", + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + Creator: creator, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + User: user, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + EntryContract: entryContract, + }}, offset, nil +} + +func raydiumLaunchLabMigrateToCpmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 27 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for migrate instruction") + } + var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + platformConfig := tx.rawTx.accountList[instruction.Accounts[3]] + var programName string + if platformConfig.Equals(bonkPlatformConfig) { + programName = SolProgramRaydiumLaunchLabBonk + } else { + programName = SolProgramRaydiumLaunchLab + } + baseTokenProgram := tx.rawTx.accountList[instruction.Accounts[22]] + quoteTokenProgram := tx.rawTx.accountList[instruction.Accounts[23]] + + baseMint := tx.rawTx.accountList[instruction.Accounts[1]] + quoteMint := tx.rawTx.accountList[instruction.Accounts[2]] + pool := tx.rawTx.accountList[instruction.Accounts[17]] + + baseVaultIdx := instruction.Accounts[19] + quoteVaultIdx := instruction.Accounts[20] + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err) + } + offset[1] += 1 + return []Swap{ + { + Program: programName, + Event: "migrate", + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + User: tx.rawTx.accountList[instruction.Accounts[0]], + //BaseAmount: decimal.Decimal{}, + //QuoteAmount: decimal.Decimal{}, + MigrateTopProgram: tx.rawTx.accountList[instruction.Accounts[4]], + MigrateToPool: tx.rawTx.accountList[instruction.Accounts[5]], + EntryContract: entryContract, + }, + }, offset, nil +} + +func raydiumLaunchLabMigrateToAmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if len(instruction.Accounts) < 27 { + return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for migrate instruction") + } + platformConfig := tx.rawTx.accountList[instruction.Accounts[3]] + var programName string + if platformConfig.Equals(bonkPlatformConfig) { + programName = SolProgramRaydiumLaunchLabBonk + } else { + programName = SolProgramRaydiumLaunchLab + } + var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + baseMint := tx.rawTx.accountList[instruction.Accounts[1]] + quoteMint := tx.rawTx.accountList[instruction.Accounts[2]] + pool := tx.rawTx.accountList[instruction.Accounts[23]] + + baseVaultIdx := instruction.Accounts[25] + quoteVaultIdx := instruction.Accounts[26] + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err) + } + baseTokenProgram := baseTokenBalance.ProgramIDAccount + quoteTokenProgram := quoteTokenBalance.ProgramIDAccount + offset[1] += 1 + return []Swap{ + { + Program: programName, + Event: "migrate", + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), + User: tx.rawTx.accountList[instruction.Accounts[0]], + //BaseAmount: decimal.Decimal{}, + //QuoteAmount: decimal.Decimal{}, + MigrateTopProgram: tx.rawTx.accountList[instruction.Accounts[12]], + MigrateToPool: tx.rawTx.accountList[instruction.Accounts[13]], + EntryContract: entryContract, + }, + }, offset, nil +} + +type RaydiumLaunchLabSwapEvent struct { + PoolState solana.PublicKey + TotalBaseSell uint64 + VirtualBase uint64 + VirtualQuote uint64 + RealBaseBefore uint64 + RealQuoteBefore uint64 + RealBaseAfter uint64 + RealQuoteAfter uint64 + AmountIn uint64 + AmountOut uint64 + ProtocolFee uint64 + PlatformFee uint64 + CreatorFee uint64 + ShareFee uint64 + TradeDirection uint8 // 0: buy 1: sell + PoolStatus uint8 // 0 Fund, 1 Migrate, 2 Trade + +} + +func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + platformConfig := tx.rawTx.accountList[instruction.Accounts[3]] + var programName string + if platformConfig.Equals(bonkPlatformConfig) { + programName = SolProgramRaydiumLaunchLabBonk + } else { + programName = SolProgramRaydiumLaunchLab + } + var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + user := tx.rawTx.accountList[instruction.Accounts[0]] + pool := tx.rawTx.accountList[instruction.Accounts[4]] + userBaseIdx := instruction.Accounts[5] + userQuoteIdx := instruction.Accounts[6] + baseVaultIdx := instruction.Accounts[7] + quoteVaultIdx := instruction.Accounts[8] + baseMint := tx.rawTx.accountList[instruction.Accounts[9]] + quoteMint := tx.rawTx.accountList[instruction.Accounts[10]] + baseTokenProgram := tx.rawTx.accountList[instruction.Accounts[11]] + quoteTokenProgram := tx.rawTx.accountList[instruction.Accounts[12]] + + baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err) + } + quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err) + } + baseMintDecimals := uint8(baseTokenBalance.UITokenAmount.Decimals) + quoteMintDecimals := uint8(quoteTokenBalance.UITokenAmount.Decimals) + inners, err := getInnerInstructions(innerInstructions, offset[1]) + + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get inner instructions: %v", err) + } + + var swapEvent RaydiumLaunchLabSwapEvent + loadedEvent := false + var prefixLen uint = offset[1] + for innerIndex, innerInstruction := range inners { + if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && + bytes.Equal(innerInstruction.Data[:8], pumpEventDiscriminator[:]) && + bytes.Equal(innerInstruction.Data[8:16], raydiumLaunchLabTradeEvnet[:]) && + len(innerInstruction.Accounts) == 1 { + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(innerIndex) + 1 + prefixLen + } + err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&swapEvent) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize swap event: %w", err) + } + + loadedEvent = true + + break + } + } + + if !loadedEvent { + return nil, increaseOffset(offset), fmt.Errorf("failed to get swap event") + } + + var event string + var baseAmount, quoteAmount decimal.Decimal + if swapEvent.TradeDirection == 0 { + event = "buy" + baseAmount = decimal.NewFromInt(int64(swapEvent.AmountOut)) + quoteAmount = decimal.NewFromInt(int64(swapEvent.AmountIn)) + } else { + event = "sell" + baseAmount = decimal.NewFromInt(int64(swapEvent.AmountIn)) + quoteAmount = decimal.NewFromInt(int64(swapEvent.AmountOut)) + } + baseReserve := decimal.NewFromInt(int64(swapEvent.RealBaseAfter)) + quoteReserve := decimal.NewFromInt(int64(swapEvent.RealQuoteAfter)) + userBase := getAccountBalanceAfterTx(tx.rawTx, userBaseIdx) + userQuote := getAccountBalanceAfterTx(tx.rawTx, userQuoteIdx) + + return []Swap{{ + Program: programName, + Event: event, + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + User: user, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + Mayhem: false, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + EntryContract: entryContract, + }}, offset, nil +} diff --git a/raydiumv4.go b/raydiumv4.go new file mode 100644 index 0000000..9dfe885 --- /dev/null +++ b/raydiumv4.go @@ -0,0 +1,399 @@ +package pump_parser + +import ( + "fmt" + + "github.com/shopspring/decimal" +) + +func raydiumV4Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumV4Program) { + return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 instruction not found, offset, %d, %d", offset[0], offset[1]) + } + + decode := instruction.Data + if len(decode) < 1 { + return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 program instruction data too short, offset, %d, %d", offset[0], offset[1]) + } + + discriminator := decode[0] + + switch discriminator { + case raydiumV4InitializePoolDiscriminator: + return raydiumv4InitializeParser(tx, instruction, innerInstructions, offset) + case raydiumV4AddLiquidityDiscriminator: + return raydiumv4AddLiquidityParser(tx, instruction, innerInstructions, offset) + case raydiumV4RemoveLiquidityDiscriminator: + return raydiumv4RemoveLiquidityParser(tx, instruction, innerInstructions, offset) + case raydiumV4WithdrawPNLDiscriminator: + return raydiumv4WithdrawPNLParser(tx, instruction, innerInstructions, offset) + case raydiumV4SwapBaseInDiscriminator, raydiumV4SwapBaseOutDiscriminator: + return raydiumv4SwapParser(tx, instruction, innerInstructions, offset) + + default: + return nil, increaseOffset(offset), InstructionIgnoredError + } +} + +func raydiumv4InitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + accountsLen := len(instruction.Accounts) + if accountsLen != 21 { + return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 initialize instruction, offset %d, %d", offset[0], offset[1]) + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + //who := tx.rawTx.accountList[instruction.Accounts[accountsLen-1]] + user := tx.rawTx.accountList[instruction.Accounts[accountsLen-4]] + baseVaultIdx := instruction.Accounts[10] + quoteVaultIdx := instruction.Accounts[11] + + baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err) + } + quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err) + } + baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount) + offset[1] += 30 + return []Swap{ + { + Program: SolProgramRaydiumV4, + Event: "create", + Pool: tx.rawTx.accountList[instruction.Accounts[4]], + BaseMint: baseTokenbalance.MintAccount, + QuoteMint: quoteTokenbalance.MintAccount, + BaseTokenProgram: baseTokenbalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount, + Creator: user, + BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals), + User: user, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + EntryContract: entryContract, + }, + }, offset, nil +} + +func raydiumv4AddLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + accountsLen := len(instruction.Accounts) + if accountsLen != 14 && accountsLen != 15 { + return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 add liquidity instruction, offset %d, %d", offset[0], offset[1]) + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + baseVaultAccountIndex := instruction.Accounts[6] + quoteVaultAccountIndex := instruction.Accounts[7] + + userBaseVaultAccountIndex := instruction.Accounts[9] + userQuoteVaultAccountIndex := instruction.Accounts[10] + + baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err) + } + quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err) + } + inners, err := getInnerInstructions(innerInstructions, offset[1]) + if err != nil { + return nil, increaseOffset(offset), err + } + var nextIndex int + var baseFound, quoteFound bool + var baseAmount, quoteAmount decimal.Decimal + for i, inner := range inners { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + continue + } + if from.Equals(tx.rawTx.accountList[userBaseVaultAccountIndex]) && to.Equals(tx.rawTx.accountList[baseVaultAccountIndex]) && !baseFound { + baseAmount = decimal.NewFromUint64(amount) + baseFound = true + } else if from.Equals(tx.rawTx.accountList[userQuoteVaultAccountIndex]) && to.Equals(tx.rawTx.accountList[quoteVaultAccountIndex]) && !quoteFound { + quoteAmount = decimal.NewFromUint64(amount) + quoteFound = true + } + if baseFound && quoteFound { + nextIndex = i + 1 + break + } + } + if !baseFound || !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer inner instruction for add liquidity, offset %d, %d", offset[0], offset[1]) + } + offset[1] += uint(nextIndex + 1) + baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount) + return []Swap{ + { + Program: SolProgramRaydiumV4, + Event: "remove_liquidity", + Pool: tx.rawTx.accountList[instruction.Accounts[1]], + BaseMint: baseTokenbalance.MintAccount, + QuoteMint: quoteTokenbalance.MintAccount, + BaseTokenProgram: baseTokenbalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount, + User: tx.rawTx.accountList[instruction.Accounts[12]], + BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + EntryContract: entryContract, + }, + }, offset, nil +} + +func raydiumv4RemoveLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + accountsLen := len(instruction.Accounts) + const AccountLen = 20 + if accountsLen != AccountLen && accountsLen != AccountLen+1 && accountsLen != AccountLen+2 && accountsLen != AccountLen+3 { + return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 add liquidity instruction, offset %d, %d", offset[0], offset[1]) + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + baseVaultAccountIndex := instruction.Accounts[6] + quoteVaultAccountIndex := instruction.Accounts[7] + userBaseVaultAccountIndex := instruction.Accounts[14] + userQuoteVaultAccountIndex := instruction.Accounts[16] + + if accountsLen == AccountLen+2 || accountsLen == AccountLen+3 { + userBaseVaultAccountIndex = instruction.Accounts[16] + userQuoteVaultAccountIndex = instruction.Accounts[17] + } + + baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err) + } + quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err) + } + inners, err := getInnerInstructions(innerInstructions, offset[1]) + if err != nil { + return nil, increaseOffset(offset), err + } + var nextIndex int + var baseFound, quoteFound bool + var baseAmount, quoteAmount decimal.Decimal + for i, inner := range inners { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + continue + } + if to.Equals(tx.rawTx.accountList[userBaseVaultAccountIndex]) && from.Equals(tx.rawTx.accountList[baseVaultAccountIndex]) && !baseFound { + baseAmount = decimal.NewFromUint64(amount) + baseFound = true + } else if to.Equals(tx.rawTx.accountList[userQuoteVaultAccountIndex]) && from.Equals(tx.rawTx.accountList[quoteVaultAccountIndex]) && !quoteFound { + quoteAmount = decimal.NewFromUint64(amount) + quoteFound = true + } + if baseFound && quoteFound { + nextIndex = i + 1 + break + } + } + if !baseFound || !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer inner instruction for add liquidity, offset %d, %d", offset[0], offset[1]) + } + offset[1] += uint(nextIndex + 1) + baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount) + return []Swap{ + { + Program: SolProgramRaydiumV4, + Event: "remove_liquidity", + Pool: tx.rawTx.accountList[instruction.Accounts[1]], + BaseMint: baseTokenbalance.MintAccount, + QuoteMint: quoteTokenbalance.MintAccount, + BaseTokenProgram: baseTokenbalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount, + User: tx.rawTx.accountList[0], + BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + EntryContract: entryContract, + }, + }, offset, nil +} + +func raydiumv4WithdrawPNLParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + accountsLen := len(instruction.Accounts) + if accountsLen != 17 && accountsLen != 18 { + return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 WithdrawPNL instruction, offset %d, %d", offset[0], offset[1]) + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + baseVaultAccountIndex := instruction.Accounts[5] + quoteVaultAccountIndex := instruction.Accounts[6] + userBaseVaultAccountIndex := instruction.Accounts[7] + userQuoteVaultAccountIndex := instruction.Accounts[8] + + baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err) + } + quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err) + } + inners, err := getInnerInstructions(innerInstructions, offset[1]) + if err != nil { + return nil, increaseOffset(offset), err + } + var nextIndex int + var baseFound, quoteFound bool + var baseAmount, quoteAmount decimal.Decimal + for i, inner := range inners { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + continue + } + if to.Equals(tx.rawTx.accountList[userBaseVaultAccountIndex]) && from.Equals(tx.rawTx.accountList[baseVaultAccountIndex]) && !baseFound { + baseAmount = decimal.NewFromUint64(amount) + baseFound = true + } else if to.Equals(tx.rawTx.accountList[userQuoteVaultAccountIndex]) && from.Equals(tx.rawTx.accountList[quoteVaultAccountIndex]) && !quoteFound { + quoteAmount = decimal.NewFromUint64(amount) + quoteFound = true + } + if baseFound && quoteFound { + nextIndex = i + 1 + break + } + } + if !baseFound || !quoteFound { + return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer inner instruction for with pnl, offset %d, %d", offset[0], offset[1]) + } + offset[1] += uint(nextIndex + 1) + + baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount) + return []Swap{ + { + Program: SolProgramRaydiumV4, + Event: "remove_liquidity", + Pool: tx.rawTx.accountList[instruction.Accounts[1]], + BaseMint: baseTokenbalance.MintAccount, + QuoteMint: quoteTokenbalance.MintAccount, + BaseTokenProgram: baseTokenbalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount, + User: tx.rawTx.accountList[instruction.Accounts[9]], + BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + EntryContract: entryContract, + }, + }, offset, nil +} + +func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + accountsLen := len(instruction.Accounts) + if accountsLen != 17 && accountsLen != 18 { + return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 swap instruction, offset %d, %d", offset[0], offset[1]) + } + user := tx.rawTx.accountList[instruction.Accounts[accountsLen-1]] + userSrcIdx := instruction.Accounts[accountsLen-3] + userDestIdx := instruction.Accounts[accountsLen-2] + vaultBaseIdx := instruction.Accounts[4] + vaultQuoteIdx := instruction.Accounts[5] + if accountsLen == 18 { + vaultBaseIdx = instruction.Accounts[5] + vaultQuoteIdx = instruction.Accounts[6] + } + var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + ammAccount := tx.rawTx.accountList[instruction.Accounts[1]] + + userSourceTokenAccount := tx.rawTx.accountList[userSrcIdx] + userDestinationTokenAccount := tx.rawTx.accountList[userDestIdx] + baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, vaultBaseIdx) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err) + } + quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, vaultQuoteIdx) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err) + } + inners, err := getInnerInstructions(innerInstructions, offset[1]) + if err != nil { + return nil, increaseOffset(offset), err + } + var nextIndex int + var srcFound, destFound bool + var baseAmount, quoteAmount decimal.Decimal + var event string + for i, inner := range inners { + from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) + if err != nil { + continue + } + if from.Equals(userSourceTokenAccount) && !srcFound { + if to.Equals(tx.rawTx.accountList[vaultBaseIdx]) { + event = "sell" + baseAmount = decimal.NewFromUint64(amount) + srcFound = true + } else if to.Equals(tx.rawTx.accountList[vaultQuoteIdx]) { + event = "buy" + quoteAmount = decimal.NewFromUint64(amount) + srcFound = true + } + } else if to.Equals(userDestinationTokenAccount) && !destFound { + if from.Equals(tx.rawTx.accountList[vaultQuoteIdx]) { + event = "sell" + quoteAmount = decimal.NewFromUint64(amount) + destFound = true + } else if from.Equals(tx.rawTx.accountList[vaultBaseIdx]) { + event = "buy" + baseAmount = decimal.NewFromUint64(amount) + destFound = true + } + + } + if srcFound && destFound { + nextIndex = i + 1 + break + } + } + if !srcFound || !destFound { + return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 failed to find token transfer inner instruction for swap, offset %d, %d", offset[0], offset[1]) + } + offset[1] += uint(nextIndex + 1) + userBase := getAccountBalanceAfterTx(tx.rawTx, userSrcIdx) + userQuote := getAccountBalanceAfterTx(tx.rawTx, userDestIdx) + baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount) + quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount) + + return []Swap{ + { + Program: SolProgramRaydiumV4, + Event: event, + Pool: ammAccount, + BaseMint: baseTokenbalance.MintAccount, + QuoteMint: quoteTokenbalance.MintAccount, + BaseTokenProgram: baseTokenbalance.ProgramIDAccount, + QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount, + BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals), + QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals), + User: user, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + Mayhem: false, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + EntryContract: entryContract, + }, + }, offset, nil +} diff --git a/tx.go b/tx.go index 2fd134e..fa93a6f 100644 --- a/tx.go +++ b/tx.go @@ -10,6 +10,8 @@ type Swap struct { Program string Event string + TxIndex int + Pool solana.PublicKey BaseMint solana.PublicKey QuoteMint solana.PublicKey @@ -34,6 +36,13 @@ type Swap struct { UserQuoteBalance decimal.Decimal EntryContract solana.PublicKey + MigrateToPool solana.PublicKey + MigrateTopProgram solana.PublicKey + + LpMint solana.PublicKey + + AfterSOLBalance decimal.Decimal + //For meteora dlmm StartBinId int32 EndBinId int32 @@ -129,7 +138,6 @@ func (tx *Tx) GetTxHash() string { func (tx *Tx) CheckPlatform(swap Swap) (string, decimal.Decimal) { // hasSolProgramRaydiumLaunchLabBonk - rawTx := tx.rawTx var platform string var platformFee decimal.Decimal if len(tx.Platform) == 0 { @@ -141,49 +149,14 @@ func (tx *Tx) CheckPlatform(swap Swap) (string, decimal.Decimal) { platformFee = info.PlatformFee break } - if swap.Event == "buy" { - switch swap.Program { - case SolProgramRaydiumLaunchLabBonk: - for _, p := range tx.Platform { - switch p.Platform { - case PlatformAxiom: - if !checkBonkAxiomBuy(rawTx) { - platform = PlatformFake - } - case PlatformGMGN: - if !checkBonkGmgnBuy(rawTx) { - platform = PlatformFake - } - } - } - } - if swap.Program == SolProgramRaydiumLaunchLabBonk { - for _, p := range tx.Platform { - switch p.Platform { - case PlatformAxiom: - if !checkPumpFunAxiomBuy(rawTx) { - platform = PlatformFake - } - case PlatformGMGN: - if !checkPumpFunGmgnBuy(rawTx) { - platform = PlatformFake - } - } - } - } - + quoteAmount := swap.QuoteAmount + if swap.BaseMint.Equals(solana.WrappedSol) { + quoteAmount = swap.BaseAmount } - if platform != "" && - platform != PlatformFake { - if (swap.QuoteMint.Equals(wSolMint) || swap.QuoteMint.IsZero()) && - platformFee.LessThan(swap.QuoteAmount.Div(decimal.New(1, int32(swap.QuoteMintDecimals))).Div(decimal.NewFromInt(10000)).Mul(decimal.NewFromInt(9))) { - platform = PlatformFake - } else if swap.BaseMint.Equals(wSolMint) && - platformFee.LessThan(swap.QuoteAmount.Div(decimal.New(1, int32(swap.QuoteMintDecimals))).Div(decimal.NewFromInt(10000)).Mul(decimal.NewFromInt(9))) { - platform = PlatformFake - } - + platform != PlatformFake && + platformFee.LessThan(quoteAmount.Mul(decimal.NewFromInt(9)).Div(decimal.New(10000, 9))) { + platform = PlatformFake } if platform == "" { platform = PlatformNone