From 6820430fe47ccdeb6e33133d805d3a64d641b0d1 Mon Sep 17 00:00:00 2001 From: hundredark Date: Mon, 24 Aug 2026 10:17:28 +0800 Subject: [PATCH 01/10] fix user check --- solana/mvm.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/solana/mvm.go b/solana/mvm.go index 2ac37d3..11db961 100644 --- a/solana/mvm.go +++ b/solana/mvm.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "fmt" "math/big" - "slices" "strings" "github.com/MixinNetwork/bot-api-go-client/v3" @@ -199,10 +198,8 @@ func (node *Node) processSystemCall(ctx context.Context, req *store.Request) ([] if err != nil { panic(err) } - if !slices.ContainsFunc(mix.Members(), func(m string) bool { - return slices.Contains(req.Output.Senders, m) - }) && !common.CheckTestEnvironment(ctx) { - // TODO use better and general authentication without MM api + if !common.CheckTestEnvironment(ctx) && + (mix.Threshold != byte(req.Output.SendersThreshold) || bot.HashMembers(mix.Members()) != req.Output.SendersHash) { return node.failRequest(ctx, req, "") } From 9fb27686e3103131c448da65a32053be200f525d Mon Sep 17 00:00:00 2001 From: hundredark Date: Mon, 24 Aug 2026 10:20:52 +0800 Subject: [PATCH 02/10] fail request when duplicate call id --- solana/mvm.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/solana/mvm.go b/solana/mvm.go index 11db961..dcb6dfe 100644 --- a/solana/mvm.go +++ b/solana/mvm.go @@ -231,6 +231,15 @@ func (node *Node) processSystemCall(ctx context.Context, req *store.Request) ([] return node.failRequest(ctx, req, "") } + old, err := node.store.ReadSystemCallByRequestId(ctx, cid, 0) + if err != nil { + panic(err) + } + if old != nil { + logger.Printf("store.ReadSystemCallByRequestId(%s) => %s", cid, old) + return node.failRequest(ctx, req, "") + } + rb := node.readStorageExtraFromObserver(ctx, *storage) call, tx, err := node.buildSystemCallFromBytes(ctx, req, cid, rb, false) if err != nil { @@ -241,7 +250,7 @@ func (node *Node) processSystemCall(ctx context.Context, req *store.Request) ([] call.Public = hex.EncodeToString(user.FingerprintWithPath()) call.SkipPostProcess = skipPostProcess - old, err := node.store.ReadSystemCallByMessage(ctx, call.MessageHash) + old, err = node.store.ReadSystemCallByMessage(ctx, call.MessageHash) if err != nil { panic(err) } From 494a88302f722e3d7b67dd09bb5fcb9d958d7537 Mon Sep 17 00:00:00 2001 From: hundredark Date: Mon, 24 Aug 2026 10:34:01 +0800 Subject: [PATCH 03/10] fail request when invalid user --- solana/mvm.go | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/solana/mvm.go b/solana/mvm.go index dcb6dfe..314369c 100644 --- a/solana/mvm.go +++ b/solana/mvm.go @@ -52,16 +52,19 @@ func (node *Node) processAddUser(ctx context.Context, req *store.Request) ([]*mt } mix := string(req.ExtraBytes()) - _, err = bot.NewMixAddressFromString(mix) - logger.Printf("common.NewAddressFromString(%s) => %v", mix, err) + mmix, err := bot.NewMixAddressFromString(mix) + logger.Printf("bot.NewMixAddressFromString(%s) => %v", mix, err) if err != nil { return node.failRequest(ctx, req, "") } + if !checkUser(ctx, req, mmix) { + return node.failRequest(ctx, req, "") + } old, err := node.store.ReadUserByMixAddress(ctx, mix) - logger.Printf("store.ReadUserByAddress(%s) => %v %v", mix, old, err) + logger.Printf("store.ReadUserByMixAddress(%s) => %v %v", mix, old, err) if err != nil { - panic(fmt.Errorf("store.ReadUserByAddress(%s) => %v", mix, err)) + panic(fmt.Errorf("store.ReadUserByMixAddress(%s) => %v", mix, err)) } else if old != nil { return node.failRequest(ctx, req, "") } @@ -107,6 +110,13 @@ func (node *Node) processUserDeposit(ctx context.Context, req *store.Request) ([ } else if user == nil { return node.failRequest(ctx, req, "") } + mix, err := bot.NewMixAddressFromString(user.MixAddress) + if err != nil { + panic(err) + } + if !checkUser(ctx, req, mix) { + return node.failRequest(ctx, req, "") + } asset, err := common.SafeReadAssetUntilSufficient(ctx, req.AssetId) if err != nil || asset == nil { @@ -198,8 +208,7 @@ func (node *Node) processSystemCall(ctx context.Context, req *store.Request) ([] if err != nil { panic(err) } - if !common.CheckTestEnvironment(ctx) && - (mix.Threshold != byte(req.Output.SendersThreshold) || bot.HashMembers(mix.Members()) != req.Output.SendersHash) { + if !checkUser(ctx, req, mix) { return node.failRequest(ctx, req, "") } @@ -236,7 +245,7 @@ func (node *Node) processSystemCall(ctx context.Context, req *store.Request) ([] panic(err) } if old != nil { - logger.Printf("store.ReadSystemCallByRequestId(%s) => %s", cid, old) + logger.Printf("store.ReadSystemCallByRequestId(%s) => %v", cid, old) return node.failRequest(ctx, req, "") } @@ -1103,3 +1112,11 @@ func (node *Node) confirmBurnRelatedSystemCall(ctx context.Context, req *store.R } return txs, "" } + +func checkUser(ctx context.Context, req *store.Request, mix *bot.MixAddress) bool { + if common.CheckTestEnvironment(ctx) { + return true + } + + return mix.Threshold == byte(req.Output.SendersThreshold) && bot.HashMembers(mix.Members()) == req.Output.SendersHash +} From b14c113591ad4434c405ff369f2f72c9767c105d Mon Sep 17 00:00:00 2001 From: hundredark Date: Mon, 24 Aug 2026 11:15:15 +0800 Subject: [PATCH 04/10] handle alt errors --- apps/solana/common.go | 3 +++ solana/solana.go | 11 +++++++---- solana/system_call.go | 6 +++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/solana/common.go b/apps/solana/common.go index d5ce791..b422994 100644 --- a/apps/solana/common.go +++ b/apps/solana/common.go @@ -414,6 +414,9 @@ func DecodeNonceAdvance(accounts solana.AccountMetaSlice, data []byte) (*system. } func NonceAccountFromTx(tx *solana.Transaction) (*system.AdvanceNonceAccount, error) { + if len(tx.Message.Instructions) == 0 { + return nil, fmt.Errorf("transaction has no instructions") + } ins := tx.Message.Instructions[0] accounts, err := ins.ResolveInstructionAccounts(&tx.Message) if err != nil { diff --git a/solana/solana.go b/solana/solana.go index 25b3938..b77bcca 100644 --- a/solana/solana.go +++ b/solana/solana.go @@ -3,6 +3,7 @@ package solana import ( "context" "encoding/hex" + "errors" "fmt" "maps" "math/big" @@ -32,6 +33,8 @@ const ( SolanaTxRetry = 10 ) +var errInvalidAddressLookup = errors.New("invalid address lookup") + func (node *Node) addressLookupTableLoop(ctx context.Context) { for { time.Sleep(time.Minute) @@ -703,23 +706,23 @@ func (node *Node) processTransactionWithAddressLookups(ctx context.Context, txx } for index, info := range infos.Value { if info == nil { - return fmt.Errorf("get account info: not found") + return fmt.Errorf("%w: get account info: not found", errInvalidAddressLookup) } key := tblKeys[index] tableContent, err := lookup.DecodeAddressLookupTableState(info.Data.GetBinary()) if err != nil { - return fmt.Errorf("decode address lookup table state: %s %w", key, err) + return fmt.Errorf("%w: decode address lookup table state: %s %v", errInvalidAddressLookup, key, err) } resolutions[key] = tableContent.Addresses } if err := txx.Message.SetAddressTables(resolutions); err != nil { - return fmt.Errorf("set address tables: %w", err) + return fmt.Errorf("%w: set address tables: %v", errInvalidAddressLookup, err) } if err := txx.Message.ResolveLookups(); err != nil { - return fmt.Errorf("resolve lookups: %w ", err) + return fmt.Errorf("%w: resolve lookups: %v", errInvalidAddressLookup, err) } return nil diff --git a/solana/system_call.go b/solana/system_call.go index fca5c29..8a657d4 100644 --- a/solana/system_call.go +++ b/solana/system_call.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/base64" + "errors" "fmt" "math/big" "slices" @@ -298,7 +299,7 @@ func (node *Node) getSubSystemCallFromExtra(ctx context.Context, req *store.Requ return node.buildSystemCallFromBytes(ctx, req, id, raw, true) } -// should only return error when fail to parse nonce advance instruction; +// should only return error when fail to resolve address lookups or parse nonce advance instruction; // without fields of superior, type, public, skip_postprocess func (node *Node) buildSystemCallFromBytes(ctx context.Context, req *store.Request, id string, raw []byte, withdrawn bool) (*store.SystemCall, *solana.Transaction, error) { tx, err := solana.TransactionFromBytes(raw) @@ -308,6 +309,9 @@ func (node *Node) buildSystemCallFromBytes(ctx context.Context, req *store.Reque } err = node.processTransactionWithAddressLookups(ctx, tx) if err != nil { + if errors.Is(err, errInvalidAddressLookup) { + return nil, nil, err + } panic(err) } advance, err := solanaApp.NonceAccountFromTx(tx) From 9942061c41623245a3609800c6f94ad69b31caf4 Mon Sep 17 00:00:00 2001 From: hundredark Date: Mon, 24 Aug 2026 11:51:55 +0800 Subject: [PATCH 05/10] restrict the amount of nonce account per user --- solana/http.go | 5 +++++ store/nonce.go | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/solana/http.go b/solana/http.go index e377a56..b084af5 100644 --- a/solana/http.go +++ b/solana/http.go @@ -5,6 +5,7 @@ package solana import ( _ "embed" "encoding/json" + "errors" "fmt" "net/http" "time" @@ -270,6 +271,10 @@ func (node *Node) httpLockNonce(w http.ResponseWriter, r *http.Request, params m err = node.store.LockNonceAccountWithMix(ctx, nonce.Address, body.Mix) if err != nil { + if errors.Is(err, store.ErrNonceAccountLimit) { + common.RenderJSON(w, r, http.StatusTooManyRequests, map[string]any{"error": "nonce limit"}) + return + } common.RenderError(w, r, err) return } diff --git a/store/nonce.go b/store/nonce.go index 27cfed2..4bcf8aa 100644 --- a/store/nonce.go +++ b/store/nonce.go @@ -3,6 +3,7 @@ package store import ( "context" "database/sql" + "errors" "fmt" "strings" "time" @@ -22,6 +23,10 @@ type NonceAccount struct { UpdatedAt time.Time } +const MaxNonceAccountsPerMix = 5 + +var ErrNonceAccountLimit = errors.New("nonce account limit reached") + var nonceAccountCols = []string{"address", "hash", "mix", "call_id", "updated_by", "created_at", "updated_at"} func nonceAccountFromRow(row Row) (*NonceAccount, error) { @@ -102,6 +107,15 @@ func (s *SQLite3Store) LockNonceAccountWithMix(ctx context.Context, address, mix } defer common.Rollback(tx) + var count int + err = tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM nonce_accounts WHERE mix=?", mix).Scan(&count) + if err != nil { + return fmt.Errorf("SELECT nonce_accounts %v", err) + } + if count >= MaxNonceAccountsPerMix { + return fmt.Errorf("%w: %s", ErrNonceAccountLimit, mix) + } + err = s.execOne(ctx, tx, "UPDATE nonce_accounts SET mix=?, updated_at=? WHERE address=? AND mix IS NULL AND call_id IS NULL", mix, time.Now().UTC(), address) if err != nil { From b25b85d435b71ffa257c5d10fec858fe85cab2f0 Mon Sep 17 00:00:00 2001 From: hundredark Date: Mon, 24 Aug 2026 13:09:09 +0800 Subject: [PATCH 06/10] should check the length of all kinds of extra --- solana/mvm.go | 49 ++++++++++++++++++++++++++++++++++++++++++++---- solana/signer.go | 43 +++++++++++++++++++++++++++++++++--------- 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/solana/mvm.go b/solana/mvm.go index 314369c..e242e67 100644 --- a/solana/mvm.go +++ b/solana/mvm.go @@ -291,6 +291,10 @@ func (node *Node) processConfirmNonce(ctx context.Context, req *store.Request) ( } extra := req.ExtraBytes() + if len(extra) < 1+uuid.Size { + logger.Printf("invalid extra length for confirm nonce: %d", len(extra)) + return node.failRequest(ctx, req, "") + } flag, extra := extra[0], extra[1:] callId := uuid.Must(uuid.FromBytes(extra[0:16])).String() @@ -428,6 +432,15 @@ func (node *Node) processDeployExternalAssetsCall(ctx context.Context, req *stor var as []*solanaApp.DeployedAsset extra := req.ExtraBytes() + if len(extra) < 1 { + logger.Printf("invalid extra length for deploy external assets: %d", len(extra)) + return node.failRequest(ctx, req, "") + } + assetSize := uuid.Size + solana.PublicKeyLength + if len(extra) != 1+int(extra[0])*assetSize { + logger.Printf("invalid extra length for deploy external assets: %d", len(extra)) + return node.failRequest(ctx, req, "") + } n, extra := extra[0], extra[1:] offset := 0 for len(as) < int(n) { @@ -489,15 +502,27 @@ func (node *Node) processConfirmCall(ctx context.Context, req *store.Request) ([ } extra := req.ExtraBytes() + if len(extra) < 1 { + logger.Printf("invalid extra length for confirm call: %d", len(extra)) + return node.failRequest(ctx, req, "") + } flag, extra := extra[0], extra[1:] switch flag { case FlagConfirmCallSuccess: + if len(extra) < 1 { + logger.Printf("invalid extra length for successful confirm call: %d", len(extra)) + return node.failRequest(ctx, req, "") + } n, extra := int(extra[0]), extra[1:] if n == 0 || n > 2 { logger.Printf("invalid length of signature: %d", n) return node.failRequest(ctx, req, "") } + if len(extra) < n*solana.SignatureLength { + logger.Printf("invalid signature payload length: %d %d", len(extra), n) + return node.failRequest(ctx, req, "") + } var calls []*store.SystemCall @@ -572,6 +597,10 @@ func (node *Node) processConfirmCall(ctx context.Context, req *store.Request) ([ } return nil, "" case FlagConfirmCallFail: + if len(extra) < uuid.Size { + logger.Printf("invalid extra length for failed confirm call: %d", len(extra)) + return node.failRequest(ctx, req, "") + } callId := uuid.Must(uuid.FromBytes(extra[:16])).String() call, err := node.store.ReadSystemCallByRequestId(ctx, callId, 0) logger.Printf("store.ReadSystemCallByRequestId(%s) => %v %v", callId, call, err) @@ -594,6 +623,10 @@ func (node *Node) processObserverRequestSign(ctx context.Context, req *store.Req } extra := req.ExtraBytes() + if len(extra) != uuid.Size { + logger.Printf("invalid extra length for sign request: %d", len(extra)) + return node.failRequest(ctx, req, "") + } callId := uuid.Must(uuid.FromBytes(extra[:16])).String() call, err := node.store.ReadSystemCallByRequestId(ctx, callId, common.RequestStatePending) logger.Printf("store.ReadSystemCallByRequestId(%s) => %v %v", callId, call, err) @@ -645,6 +678,10 @@ func (node *Node) processObserverCreateDepositCall(ctx context.Context, req *sto } extra := req.ExtraBytes() + if len(extra) < solana.PublicKeyLength+solana.SignatureLength { + logger.Printf("invalid extra length for deposit call: %d", len(extra)) + return node.failRequest(ctx, req, "") + } userAddress := solana.PublicKeyFromBytes(extra[:32]) signature := solana.SignatureFromBytes(extra[32:96]) @@ -873,19 +910,23 @@ func (node *Node) refundAndFailRequest(ctx context.Context, req *store.Request, } func (node *Node) failSystemCall(ctx context.Context, req *store.Request, call *store.SystemCall) ([]*mtg.Transaction, string) { + if call == nil { + return node.failRequest(ctx, req, "") + } + extra := req.ExtraBytes() + if len(extra) < 1+uuid.Size { + logger.Printf("invalid extra length for failed system call: %d", len(extra)) + return node.failRequest(ctx, req, "") + } switch call.State { case common.RequestStatePending, common.RequestStateFailed: default: return node.failRequest(ctx, req, "") } - extra := req.ExtraBytes() flag, extra := extra[0], extra[1:] storage := extra[16:] - if call == nil { - panic(req) - } if flag == FlagConfirmCallSuccess { storage = nil } diff --git a/solana/signer.go b/solana/signer.go index 03122d8..977bea2 100644 --- a/solana/signer.go +++ b/solana/signer.go @@ -204,20 +204,21 @@ func (node *Node) loopPendingSessions(ctx context.Context) { func (node *Node) acceptIncomingMessages(ctx context.Context) { for { mm, err := node.network.ReceiveMessage(ctx) - logger.Debugf("network.ReceiveMessage() => %s %x %s %v", mm.Peer, mm.Data, mm.CreatedAt, err) if err != nil { panic(err) } + logger.Debugf("network.ReceiveMessage() => %s %x %s %v", mm.Peer, mm.Data, mm.CreatedAt, err) err = node.writeRequestTime(ctx, store.MPCMessageTimeKey, mm.CreatedAt) if err != nil { panic(err) } sessionId, msg, err := unmarshalSessionMessage(mm.Data) - logger.Verbosef("node.acceptIncomingMessages(%x, %d) => %s %s %x", sessionId, msg.RoundNumber, mm.Peer, mm.CreatedAt, msg.SSID) if err != nil { + logger.Printf("node.unmarshalSessionMessage(%x) => %v", mm.Data, err) continue } + logger.Verbosef("node.acceptIncomingMessages(%x, %d) => %s %s %x", sessionId, msg.RoundNumber, mm.Peer, mm.CreatedAt, msg.SSID) if msg.SSID == nil { continue } @@ -414,7 +415,7 @@ func (node *Node) getSession(sessionId []byte) *MultiPartySession { } func marshalSessionMessage(sessionId []byte, msg *protocol.Message) []byte { - if len(sessionId) > 32 { + if len(sessionId) != uuid.Size { panic(hex.EncodeToString(sessionId)) } msb := []byte{byte(len(sessionId))} @@ -423,15 +424,15 @@ func marshalSessionMessage(sessionId []byte, msg *protocol.Message) []byte { } func unmarshalSessionMessage(b []byte) ([]byte, *protocol.Message, error) { - if len(b) < 16 { + if len(b) < 1 || int(b[0]) != uuid.Size { return nil, nil, fmt.Errorf("unmarshalSessionMessage(%x) short", b) } - if len(b[1:]) <= int(b[0]) { + if len(b) <= 1+uuid.Size { return nil, nil, fmt.Errorf("unmarshalSessionMessage(%x) short", b) } - sessionId := b[1 : 1+b[0]] + sessionId := b[1 : 1+uuid.Size] var msg protocol.Message - err := msg.UnmarshalBinary(b[1+b[0]:]) + err := msg.UnmarshalBinary(b[1+uuid.Size:]) return sessionId, &msg, err } @@ -591,6 +592,10 @@ func (node *Node) processSignerKeygenResults(ctx context.Context, req *store.Req } extra := req.ExtraBytes() + if len(extra) != uuid.Size+ed25519.PublicKeySize { + logger.Printf("invalid extra length for keygen result: %d", len(extra)) + return node.failRequest(ctx, req, "") + } sid := uuid.FromBytesOrNil(extra[:16]).String() public := extra[16:] @@ -599,6 +604,10 @@ func (node *Node) processSignerKeygenResults(ctx context.Context, req *store.Req if err != nil { panic(err) } + if s == nil || s.Operation != OperationTypeKeygenInput { + logger.Printf("invalid keygen session: %v", s) + return node.failRequest(ctx, req, "") + } sender := req.Output.Senders[0] err = node.store.WriteSessionSignerIfNotExist(ctx, s.Id, sender, public, req.Output.SequencerCreatedAt, sender == string(node.id)) @@ -690,6 +699,10 @@ func (node *Node) processSignerPrepare(ctx context.Context, req *store.Request) } extra := req.ExtraBytes() + if len(extra) != uuid.Size+len(PrepareExtra) { + logger.Printf("invalid extra length for signer prepare: %d", len(extra)) + return node.failRequest(ctx, req, "") + } session := uuid.Must(uuid.FromBytes(extra[:16])).String() extra = extra[16:] if !bytes.Equal(extra, PrepareExtra) { @@ -736,16 +749,28 @@ func (node *Node) processSignerSignatureResponse(ctx context.Context, req *store panic(req.Action) } extra := req.ExtraBytes() + if len(extra) != uuid.Size && len(extra) != uuid.Size+ed25519.SignatureSize { + logger.Printf("invalid extra length for signature response: %d", len(extra)) + return node.failRequest(ctx, req, "") + } sid := uuid.FromBytesOrNil(extra[:16]).String() signature := extra[16:] s, err := node.store.ReadSession(ctx, sid) - if err != nil || s == nil { + if err != nil { panic(fmt.Errorf("store.ReadSession(%s) => %v %v", sid, s, err)) } + if s == nil || s.Operation != OperationTypeSignInput { + logger.Printf("invalid sign session: %v", s) + return node.failRequest(ctx, req, "") + } call, err := node.store.ReadSystemCallByRequestId(ctx, s.RequestId, 0) - if err != nil || call == nil { + if err != nil { panic(fmt.Errorf("store.ReadSystemCallByRequestId(%s) => %v %v", s.RequestId, call, err)) } + if call == nil { + logger.Printf("invalid call for sign session: %v", s) + return node.failRequest(ctx, req, "") + } if call.Signature.Valid || call.State != common.RequestStatePending { logger.Printf("invalid call %s: %d %s", call.RequestId, call.State, call.Signature.String) return node.failRequest(ctx, req, "") From f315e94938f392948e4bec055bebe727a089dea3 Mon Sep 17 00:00:00 2001 From: hundredark Date: Mon, 24 Aug 2026 14:10:49 +0800 Subject: [PATCH 07/10] fix priority fee --- apps/solana/transaction.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/solana/transaction.go b/apps/solana/transaction.go index 8984145..9452ef5 100644 --- a/apps/solana/transaction.go +++ b/apps/solana/transaction.go @@ -388,12 +388,19 @@ func (c *Client) getPriorityFeeInstruction(ctx context.Context) *computebudget.I if err != nil { panic(err) } + fee := getAveragePriorityFee(recentFees) + return computebudget.NewSetComputeUnitPriceInstruction(fee).Build() +} + +func getAveragePriorityFee(recentFees []rpc.PriorizationFeeResult) uint64 { + if len(recentFees) == 0 { + return 1000 + } total := decimal.NewFromInt(0) for _, fee := range recentFees { total = total.Add(decimal.NewFromUint64(fee.PrioritizationFee)) } - fee := total.Div(decimal.NewFromInt(int64(len(recentFees)))).BigInt().Uint64() - return computebudget.NewSetComputeUnitPriceInstruction(fee).Build() + return total.Div(decimal.NewFromInt(int64(len(recentFees)))).BigInt().Uint64() } func ExtractTransfersFromTransaction(ctx context.Context, tx *solana.Transaction, meta *rpc.TransactionMeta, exception *solana.PublicKey) ([]*Transfer, error) { From 4d275257ae4c24ac1f29b2552f8f10dfb325a6a2 Mon Sep 17 00:00:00 2001 From: hundredark Date: Mon, 24 Aug 2026 14:19:19 +0800 Subject: [PATCH 08/10] slihgt fix --- solana/rpc.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/solana/rpc.go b/solana/rpc.go index 0886413..6778f8b 100644 --- a/solana/rpc.go +++ b/solana/rpc.go @@ -228,8 +228,16 @@ func (node *Node) RPCCheckNFT(ctx context.Context, account string) (bool, error) if err != nil { return false, err } + return isNFTAccount(acc) +} + +func isNFTAccount(acc *rpc.GetAccountInfoResult) (bool, error) { + data := acc.GetBinary() + if len(data) == 0 { + return false, nil + } var tm token.Mint - err = bin.NewBinDecoder(acc.Value.Data.GetBinary()).Decode(&tm) + err := bin.NewBinDecoder(data).Decode(&tm) if err != nil { return false, fmt.Errorf("solana.NewBinDecoder() => %v", err) } From 0ef7eb6b4e1f00aa7e0a8e003d2feb4c86e442e6 Mon Sep 17 00:00:00 2001 From: hundredark Date: Mon, 24 Aug 2026 14:50:37 +0800 Subject: [PATCH 09/10] improve ExtractTransfersFromTransaction --- apps/solana/transaction.go | 55 +++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/apps/solana/transaction.go b/apps/solana/transaction.go index 9452ef5..f7cbc12 100644 --- a/apps/solana/transaction.go +++ b/apps/solana/transaction.go @@ -20,6 +20,8 @@ import ( "github.com/shopspring/decimal" ) +const solanaInnerIndexBase = int64(1_000_000_000) + func (c *Client) CreateNonceAccount(ctx context.Context, key, nonce string, rent uint64) (*solana.Transaction, error) { payer, err := solana.PrivateKeyFromBase58(key) if err != nil { @@ -447,13 +449,12 @@ func ExtractTransfersFromTransaction(ctx context.Context, tx *solana.Transaction } for index, ix := range msg.Instructions { - baseIndex := int64(index+1) * 10000 if transfer := extractTransfersFromInstruction(&msg, ix, tokenAccounts, owners, transfers); transfer != nil { if exception != nil && exception.String() == transfer.Receiver { continue } transfer.Signature = hash - transfer.Index = baseIndex + transfer.Index = int64(index) transfers = append(transfers, transfer) } @@ -463,7 +464,7 @@ func ExtractTransfersFromTransaction(ctx context.Context, tx *solana.Transaction continue } transfer.Signature = hash - transfer.Index = baseIndex + int64(innerIndex) + 1 + transfer.Index = (int64(index)+1)*solanaInnerIndexBase + int64(innerIndex) transfers = append(transfers, transfer) } } @@ -479,10 +480,23 @@ func ExtractTransferFromTransactionByIndex(ctx context.Context, tx *solana.Trans msg := tx.Message var ( - tokenAccounts = map[solana.PublicKey]token.Account{} - owners = []*solana.PublicKey{} + innerInstructions = map[uint16][]solana.CompiledInstruction{} + tokenAccounts = map[solana.PublicKey]token.Account{} + owners = []*solana.PublicKey{} ) + for _, inner := range meta.InnerInstructions { + sis := make([]solana.CompiledInstruction, len(inner.Instructions)) + for idx, ii := range inner.Instructions { + sis[idx] = solana.CompiledInstruction{ + ProgramIDIndex: ii.ProgramIDIndex, + Accounts: ii.Accounts, + Data: ii.Data, + } + } + innerInstructions[inner.Index] = sis + } + bs := meta.PreTokenBalances bs = append(bs, meta.PostTokenBalances...) for _, balance := range bs { @@ -499,7 +513,36 @@ func ExtractTransferFromTransactionByIndex(ctx context.Context, tx *solana.Trans } } - return extractTransfersFromInstruction(&msg, msg.Instructions[index], tokenAccounts, owners, nil) + ix, ok := instructionByTransferIndex(&msg, innerInstructions, index) + if !ok { + return nil + } + return extractTransfersFromInstruction(&msg, ix, tokenAccounts, owners, nil) +} + +func instructionByTransferIndex(msg *solana.Message, innerInstructions map[uint16][]solana.CompiledInstruction, index int64) (solana.CompiledInstruction, bool) { + if index < 0 { + return solana.CompiledInstruction{}, false + } + + if index < solanaInnerIndexBase { + if index >= int64(len(msg.Instructions)) { + return solana.CompiledInstruction{}, false + } + return msg.Instructions[index], true + } + + outerIndex := index/solanaInnerIndexBase - 1 + if outerIndex < 0 || outerIndex >= int64(len(msg.Instructions)) { + return solana.CompiledInstruction{}, false + } + + innerIndex := index % solanaInnerIndexBase + inners := innerInstructions[uint16(outerIndex)] + if innerIndex < 0 || innerIndex >= int64(len(inners)) { + return solana.CompiledInstruction{}, false + } + return inners[innerIndex], true } func ExtractMintsFromTransaction(tx *solana.Transaction) []string { From 10e8719da7cd88491a2a99f309358710334b1d7e Mon Sep 17 00:00:00 2001 From: hundredark Date: Mon, 24 Aug 2026 14:57:48 +0800 Subject: [PATCH 10/10] improve refund check --- solana/mvm.go | 2 +- solana/solana.go | 2 +- solana/system_call.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/solana/mvm.go b/solana/mvm.go index e242e67..00eacf4 100644 --- a/solana/mvm.go +++ b/solana/mvm.go @@ -954,7 +954,7 @@ func (node *Node) failSystemCall(ctx context.Context, req *store.Request, call * } } - os, _, err := node.GetSystemCallReferenceOutputs(ctx, main.UserIdFromPublicPath(), main.RequestHash, 0) + os, _, err := node.GetSystemCallReferenceOutputs(ctx, main.UserIdFromPublicPath(), main.RequestHash, common.RequestStatePending) if err != nil { panic(err) } diff --git a/solana/solana.go b/solana/solana.go index b77bcca..efaf6b6 100644 --- a/solana/solana.go +++ b/solana/solana.go @@ -618,7 +618,7 @@ func (node *Node) CreateRefundWithdrawalTransaction(ctx context.Context, prepare return nil } - os, _, err := node.GetSystemCallReferenceOutputs(ctx, call.UserIdFromPublicPath(), call.RequestHash, 0) + os, _, err := node.GetSystemCallReferenceOutputs(ctx, call.UserIdFromPublicPath(), call.RequestHash, common.RequestStatePending) if err != nil { panic(fmt.Errorf("node.GetSystemCallReferenceTxs(%s) => %v", call.RequestId, err)) } diff --git a/solana/system_call.go b/solana/system_call.go index 8a657d4..ab48ad7 100644 --- a/solana/system_call.go +++ b/solana/system_call.go @@ -272,7 +272,7 @@ func (node *Node) getPostProcessCall(ctx context.Context, req *store.Request, fl return nil, err } - os, _, err := node.GetSystemCallReferenceOutputs(ctx, main.UserIdFromPublicPath(), main.RequestHash, 0) + os, _, err := node.GetSystemCallReferenceOutputs(ctx, main.UserIdFromPublicPath(), main.RequestHash, common.RequestStatePending) if err != nil { panic(fmt.Errorf("node.GetSystemCallReferenceTxs(%s) => %v", main.RequestId, err)) }