From fc8ac1cae87602789ab3f436e5d316400e857462 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Thu, 26 Feb 2026 11:32:17 -0300 Subject: [PATCH 01/15] fix: adnl timeout --- pkg/ton/codec/debug/explorer/explorer.go | 123 +++++++++++++++++------ 1 file changed, 93 insertions(+), 30 deletions(-) diff --git a/pkg/ton/codec/debug/explorer/explorer.go b/pkg/ton/codec/debug/explorer/explorer.go index 23267a1d6..317c547a5 100644 --- a/pkg/ton/codec/debug/explorer/explorer.go +++ b/pkg/ton/codec/debug/explorer/explorer.go @@ -291,6 +291,8 @@ type client struct { maxPages uint32 } +func (c *client) resilientAPI() ton.APIClientWrapped { return c.connection.WithRetry(5) } + type Format int const ( @@ -299,6 +301,20 @@ const ( FormatSequenceRaw ) +type toncenterTxResult struct { + Account string `json:"account"` + LT string `json:"lt"` + BlockRef struct { + Workchain int32 `json:"workchain"` + Shard string `json:"shard"` + SeqNo uint32 `json:"seqno"` + } `json:"block_ref"` +} + +type toncenterAPIResponse struct { + Transactions []toncenterTxResult `json:"transactions"` +} + // PrintTrace connects to the specified TON network, retrieves the transaction // by the given source address and transaction hash, and prints the full execution // trace of the transaction, including all outgoing messages and their subsequent @@ -309,6 +325,8 @@ const ( // - txHashStr: The transaction hash in hexadecimal format. // - srcAddrStr: The source address of the transaction in string format. func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr string, format Format, knownActors map[string]debug.TypeAndVersion) error { + api := c.resilientAPI() + var senderAddr *address.Address var err error if srcAddrStr == "" { @@ -329,7 +347,7 @@ func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr st return fmt.Errorf("failed to decode tx hash: %w", err) } - tx, err := c.findTx(ctx, c.connection, senderAddr, txHash) + tx, err := c.findTx(ctx, api, senderAddr, txHashStr, txHash) if err != nil { return err } @@ -343,14 +361,14 @@ func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr st c.lggr.Info("waiting for full trace...") - err = recvMsg.WaitForTrace(ctx, c.connection) + err = recvMsg.WaitForTrace(ctx, api) if err != nil { return fmt.Errorf("failed to wait for trace: %w", err) } c.lggr.Debug("actors before query:\n", knownActors) c.lggr.Info("querying actors") - err = c.queryActors(ctx, &recvMsg, knownActors) + err = c.queryActors(ctx, api, &recvMsg, knownActors) if err != nil { return fmt.Errorf("failed to query actors: %w", err) } @@ -374,51 +392,51 @@ func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr st return nil } -func (c *client) queryActors(ctx context.Context, message *tracetracking.ReceivedMessage, knownActors map[string]debug.TypeAndVersion) error { +func (c *client) queryActors(ctx context.Context, api ton.APIClientWrapped, message *tracetracking.ReceivedMessage, knownActors map[string]debug.TypeAndVersion) error { visited := make(map[string]bool) - block, err := c.connection.CurrentMasterchainInfo(ctx) + block, err := api.CurrentMasterchainInfo(ctx) if err != nil { return fmt.Errorf("failed to get masterchain info: %w", err) } - return c.queryActorsReceivedRec(ctx, block, message, knownActors, visited) + return c.queryActorsReceivedRec(ctx, api, block, message, knownActors, visited) } -func (c *client) queryActorsReceivedRec(ctx context.Context, block *ton.BlockIDExt, message *tracetracking.ReceivedMessage, knownActors map[string]debug.TypeAndVersion, visited map[string]bool) error { +func (c *client) queryActorsReceivedRec(ctx context.Context, api ton.APIClientWrapped, block *ton.BlockIDExt, message *tracetracking.ReceivedMessage, knownActors map[string]debug.TypeAndVersion, visited map[string]bool) error { if message.InternalMsg != nil { - err := c.queryActorIfNotVisited(ctx, block, message.InternalMsg.SrcAddr, knownActors, visited) + err := c.queryActorIfNotVisited(ctx, api, block, message.InternalMsg.SrcAddr, knownActors, visited) if err != nil { return err } - err = c.queryActorIfNotVisited(ctx, block, message.InternalMsg.DstAddr, knownActors, visited) + err = c.queryActorIfNotVisited(ctx, api, block, message.InternalMsg.DstAddr, knownActors, visited) if err != nil { return err } - err = c.queryOutgoingMessages(ctx, block, message.OutgoingInternalSentMessages, message.OutgoingInternalReceivedMessages, knownActors, visited) + err = c.queryOutgoingMessages(ctx, api, block, message.OutgoingInternalSentMessages, message.OutgoingInternalReceivedMessages, knownActors, visited) return err } else if message.ExternalMsg != nil { - err := c.queryActorIfNotVisited(ctx, block, message.ExternalMsg.DstAddr, knownActors, visited) + err := c.queryActorIfNotVisited(ctx, api, block, message.ExternalMsg.DstAddr, knownActors, visited) if err != nil { return err } - err = c.queryOutgoingMessages(ctx, block, message.OutgoingInternalSentMessages, message.OutgoingInternalReceivedMessages, knownActors, visited) + err = c.queryOutgoingMessages(ctx, api, block, message.OutgoingInternalSentMessages, message.OutgoingInternalReceivedMessages, knownActors, visited) return err } return fmt.Errorf("unknown message type: %+v", message) } -func (c *client) queryOutgoingMessages(ctx context.Context, block *ton.BlockIDExt, outgoingSentMessages []*tracetracking.SentMessage, outgoingReceivedMessages []*tracetracking.ReceivedMessage, knownActors map[string]debug.TypeAndVersion, visited map[string]bool) error { +func (c *client) queryOutgoingMessages(ctx context.Context, api ton.APIClientWrapped, block *ton.BlockIDExt, outgoingSentMessages []*tracetracking.SentMessage, outgoingReceivedMessages []*tracetracking.ReceivedMessage, knownActors map[string]debug.TypeAndVersion, visited map[string]bool) error { for _, outMsg := range outgoingSentMessages { - err := c.queryActorIfNotVisited(ctx, block, outMsg.InternalMsg.SrcAddr, knownActors, visited) + err := c.queryActorIfNotVisited(ctx, api, block, outMsg.InternalMsg.SrcAddr, knownActors, visited) if err != nil { return err } - err = c.queryActorIfNotVisited(ctx, block, outMsg.InternalMsg.DstAddr, knownActors, visited) + err = c.queryActorIfNotVisited(ctx, api, block, outMsg.InternalMsg.DstAddr, knownActors, visited) if err != nil { return err } } for _, outMsg := range outgoingReceivedMessages { - err := c.queryActorsReceivedRec(ctx, block, outMsg, knownActors, visited) + err := c.queryActorsReceivedRec(ctx, api, block, outMsg, knownActors, visited) if err != nil { return err } @@ -426,7 +444,7 @@ func (c *client) queryOutgoingMessages(ctx context.Context, block *ton.BlockIDEx return nil } -func (c *client) queryActorIfNotVisited(ctx context.Context, block *ton.BlockIDExt, addr *address.Address, knownActors map[string]debug.TypeAndVersion, visited map[string]bool) error { +func (c *client) queryActorIfNotVisited(ctx context.Context, api ton.APIClientWrapped, block *ton.BlockIDExt, addr *address.Address, knownActors map[string]debug.TypeAndVersion, visited map[string]bool) error { c.lggr.Debug("queryActorIfNotVisited", addr.String()) c.lggr.Debug("visited:", visited) c.lggr.Debug("knownActors:", knownActors) @@ -441,7 +459,7 @@ func (c *client) queryActorIfNotVisited(ctx context.Context, block *ton.BlockIDE } c.lggr.Debug("actor not known") var typeVersion common.TypeAndVersion - result, err := c.connection.RunGetMethod(ctx, block, addr, "typeAndVersion") + result, err := api.RunGetMethod(ctx, block, addr, "typeAndVersion") if err != nil { // We don't fail here because many contracts don't implement typeAndVersion return nil // TODO try deducing from code? @@ -463,6 +481,19 @@ func (c *client) queryActorIfNotVisited(ctx context.Context, block *ton.BlockIDE } func (c *client) GetSenderAddressFromTxHash(ctx context.Context, txHashStr string) (*address.Address, error) { + res, err := c.getToncenterTxByHash(ctx, txHashStr) + if err != nil { + return nil, err + } + + addr, err := address.ParseRawAddr(res.Account) + if err != nil { + return nil, fmt.Errorf("failed to parse source address from toncenter response: %w", err) + } + return addr, nil +} + +func (c *client) getToncenterTxByHash(ctx context.Context, txHashStr string) (*toncenterTxResult, error) { // fetch from https://testnet.toncenter.com/api/v3/transactions?hash=txHashStr var baseURL string switch c.net { @@ -473,12 +504,6 @@ func (c *client) GetSenderAddressFromTxHash(ctx context.Context, txHashStr strin default: return nil, fmt.Errorf("unsupported network: %s", c.net) } - type txResult struct { - Account string `json:"account"` - } - type apiResponse struct { - Transactions []txResult `json:"transactions"` - } // Use url.URL for safer URL construction u, err := url.Parse(baseURL) if err != nil { @@ -505,7 +530,7 @@ func (c *client) GetSenderAddressFromTxHash(ctx context.Context, txHashStr strin if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code from toncenter: %d", resp.StatusCode) } - var respData apiResponse + var respData toncenterAPIResponse err = json.NewDecoder(resp.Body).Decode(&respData) if err != nil { return nil, fmt.Errorf("failed to decode toncenter response: %w", err) @@ -513,14 +538,44 @@ func (c *client) GetSenderAddressFromTxHash(ctx context.Context, txHashStr strin if len(respData.Transactions) != 1 { return nil, errors.New("transaction not found in toncenter response") } - addr, err := address.ParseRawAddr(respData.Transactions[0].Account) + + return &respData.Transactions[0], nil +} + +func (c *client) findTxByToncenterMetadata(ctx context.Context, api ton.APIClientWrapped, txHashStr string, txHash []byte, srcAddr *address.Address) (*tlb.Transaction, error) { + res, err := c.getToncenterTxByHash(ctx, txHashStr) + if err != nil { + return nil, err + } + + lt, err := strconv.ParseUint(res.LT, 10, 64) if err != nil { - return nil, fmt.Errorf("failed to parse source address from toncenter response: %w", err) + return nil, fmt.Errorf("failed to parse lt from toncenter response: %w", err) } - return addr, nil + + shard, err := strconv.ParseUint(res.BlockRef.Shard, 16, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse shard from toncenter response: %w", err) + } + + block, err := api.LookupBlock(ctx, res.BlockRef.Workchain, int64(shard), res.BlockRef.SeqNo) + if err != nil { + return nil, fmt.Errorf("failed to lookup block from toncenter metadata: %w", err) + } + + tx, err := api.GetTransaction(ctx, block, srcAddr, lt) + if err != nil { + return nil, fmt.Errorf("failed to fetch transaction from toncenter metadata: %w", err) + } + + if !equalHash(tx.Hash, txHash) { + return nil, errors.New("toncenter metadata lookup returned a different transaction hash") + } + + return tx, nil } -func (c *client) findTx(ctx context.Context, api *ton.APIClient, srcAddr *address.Address, txHash []byte) (*tlb.Transaction, error) { +func (c *client) findTx(ctx context.Context, api ton.APIClientWrapped, srcAddr *address.Address, txHashStr string, txHash []byte) (*tlb.Transaction, error) { block, err := api.GetMasterchainInfo(ctx) if err != nil { return nil, fmt.Errorf("get masterchain info: %w", err) @@ -538,6 +593,9 @@ func (c *client) findTx(ctx context.Context, api *ton.APIClient, srcAddr *addres if err != nil { return nil, fmt.Errorf("get transaction: %w", err) } + if len(txs) == 0 { + return nil, errors.New("transaction not found in searched range. Try increasing --page-size and --max-pages") + } for _, tx := range txs { if equalHash(tx.Hash, txHash) { return tx, nil @@ -548,7 +606,12 @@ func (c *client) findTx(ctx context.Context, api *ton.APIClient, srcAddr *addres maxLT = last.PrevTxLT maxHash = last.PrevTxHash } - return nil, errors.New("transaction not found in searched range. Try increasing --page-size and --max-pages") + tx, err := c.findTxByToncenterMetadata(ctx, api, txHashStr, txHash, srcAddr) + if err == nil { + return tx, nil + } + + return nil, fmt.Errorf("transaction not found in searched range. Try increasing --page-size and --max-pages (fallback failed: %w)", err) } func equalHash(a, b []byte) bool { From 7b6137d99cd351cb5c341064529af5b740dbeeda Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Thu, 26 Feb 2026 11:44:33 -0300 Subject: [PATCH 02/15] feat: open mermaid urls in browser --- pkg/ton/codec/debug/explorer/explorer.go | 35 +++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/pkg/ton/codec/debug/explorer/explorer.go b/pkg/ton/codec/debug/explorer/explorer.go index 317c547a5..19ec75e79 100644 --- a/pkg/ton/codec/debug/explorer/explorer.go +++ b/pkg/ton/codec/debug/explorer/explorer.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "os/exec" + "runtime" "strconv" "strings" "time" @@ -387,7 +388,39 @@ func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr st default: return errors.New("unknown format") } - c.lggr.Info(debugger.DumpReceived(&recvMsg, c.verbose)) + + output := debugger.DumpReceived(&recvMsg, c.verbose) + if format == FormatSequenceURL { + if err := openInBrowser(ctx, output); err != nil { + return fmt.Errorf("failed to open mermaid url in browser: %w", err) + } + c.lggr.Info("opened mermaid visualization in browser") + return nil + } + + c.lggr.Info(output) + + return nil +} + +func openInBrowser(ctx context.Context, targetURL string) error { + if strings.TrimSpace(targetURL) == "" { + return errors.New("empty url") + } + + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.CommandContext(ctx, "open", targetURL) + case "windows": + cmd = exec.CommandContext(ctx, "rundll32", "url.dll,FileProtocolHandler", targetURL) + default: + cmd = exec.CommandContext(ctx, "xdg-open", targetURL) + } + + if err := cmd.Start(); err != nil { + return err + } return nil } From 4df959b15dcaa10fb3fcfb8a83ed8b183a85d7f0 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Thu, 26 Feb 2026 11:59:55 -0300 Subject: [PATCH 03/15] feat: any tx hash in a trace will return whole trace. --- pkg/ton/codec/debug/explorer/explorer.go | 114 ++++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/pkg/ton/codec/debug/explorer/explorer.go b/pkg/ton/codec/debug/explorer/explorer.go index 19ec75e79..5a3d94f45 100644 --- a/pkg/ton/codec/debug/explorer/explorer.go +++ b/pkg/ton/codec/debug/explorer/explorer.go @@ -2,6 +2,7 @@ package explorer import ( "context" + "encoding/base64" "encoding/hex" "encoding/json" "errors" @@ -316,6 +317,15 @@ type toncenterAPIResponse struct { Transactions []toncenterTxResult `json:"transactions"` } +type toncenterTraceResponse struct { + Traces []struct { + Trace struct { + TxHash string `json:"tx_hash"` + } `json:"trace"` + TransactionsOrder []string `json:"transactions_order"` + } `json:"traces"` +} + // PrintTrace connects to the specified TON network, retrieves the transaction // by the given source address and transaction hash, and prints the full execution // trace of the transaction, including all outgoing messages and their subsequent @@ -327,28 +337,45 @@ type toncenterAPIResponse struct { // - srcAddrStr: The source address of the transaction in string format. func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr string, format Format, knownActors map[string]debug.TypeAndVersion) error { api := c.resilientAPI() + effectiveTxHash := txHashStr + rootTxHash, rootErr := c.getTraceRootTxHash(ctx, txHashStr) + if rootErr == nil && rootTxHash != "" { + effectiveTxHash = rootTxHash + if rootTxHash != txHashStr { + c.lggr.Info("resolved input transaction to trace root", "input_tx_hash", txHashStr, "root_tx_hash", rootTxHash) + } + } else if rootErr != nil { + c.lggr.Debug("failed to resolve trace root tx hash, continuing with provided tx", "tx_hash", txHashStr, "error", rootErr) + } var senderAddr *address.Address var err error if srcAddrStr == "" { c.lggr.Debug("source address not provided, attempting to fetch from toncenter by hash...") - senderAddr, err = c.GetSenderAddressFromTxHash(ctx, txHashStr) + senderAddr, err = c.GetSenderAddressFromTxHash(ctx, effectiveTxHash) if err != nil { return fmt.Errorf("failed to get sender address from tx hash: %w", err) } c.lggr.Debug("source address found:", senderAddr.String()) + } else if effectiveTxHash != txHashStr { + // User-provided address may correspond to a non-root tx. Prefer root tx account. + senderAddr, err = c.GetSenderAddressFromTxHash(ctx, effectiveTxHash) + if err != nil { + return fmt.Errorf("failed to get root sender address from tx hash: %w", err) + } + c.lggr.Debug("overriding provided source address with trace root account", senderAddr.String()) } else { senderAddr, err = address.ParseAddr(srcAddrStr) if err != nil { return fmt.Errorf("failed to parse transaction address: %w", err) } } - txHash, err := hex.DecodeString(txHashStr) + txHash, err := decodeTxHash(effectiveTxHash) if err != nil { return fmt.Errorf("failed to decode tx hash: %w", err) } - tx, err := c.findTx(ctx, api, senderAddr, txHashStr, txHash) + tx, err := c.findTx(ctx, api, senderAddr, effectiveTxHash, txHash) if err != nil { return err } @@ -403,6 +430,87 @@ func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr st return nil } +func decodeTxHash(txHash string) ([]byte, error) { + if after, ok := strings.CutPrefix(txHash, "0x"); ok { + txHash = after + } + + if raw, err := hex.DecodeString(txHash); err == nil { + return raw, nil + } + + if raw, err := base64.StdEncoding.DecodeString(txHash); err == nil { + return raw, nil + } + + if raw, err := base64.URLEncoding.DecodeString(txHash); err == nil { + return raw, nil + } + + if raw, err := base64.RawURLEncoding.DecodeString(txHash); err == nil { + return raw, nil + } + + return nil, fmt.Errorf("unsupported tx hash format: %s", txHash) +} + +func (c *client) getTraceRootTxHash(ctx context.Context, txHashStr string) (string, error) { + var baseURL string + switch c.net { + case "mainnet": + baseURL = "https://toncenter.com/api/v3/traces" + case "testnet": + baseURL = "https://testnet.toncenter.com/api/v3/traces" + default: + return "", fmt.Errorf("unsupported network for trace index lookup: %s", c.net) + } + + u, err := url.Parse(baseURL) + if err != nil { + return "", fmt.Errorf("invalid trace endpoint url: %w", err) + } + + q := u.Query() + q.Set("tx_hash", txHashStr) + u.RawQuery = q.Encode() + + httpClient := &http.Client{Timeout: 30 * time.Second} + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return "", fmt.Errorf("failed to create trace request: %w", err) + } + + resp, err := httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to fetch trace from toncenter: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status code from trace endpoint: %d", resp.StatusCode) + } + + var traceResp toncenterTraceResponse + if err := json.NewDecoder(resp.Body).Decode(&traceResp); err != nil { + return "", fmt.Errorf("failed to decode trace response: %w", err) + } + + if len(traceResp.Traces) == 0 { + return "", errors.New("no trace found for transaction") + } + + trace := traceResp.Traces[0] + if len(trace.TransactionsOrder) > 0 && trace.TransactionsOrder[0] != "" { + return trace.TransactionsOrder[0], nil + } + + if trace.Trace.TxHash != "" { + return trace.Trace.TxHash, nil + } + + return "", errors.New("trace root hash missing in trace response") +} + func openInBrowser(ctx context.Context, targetURL string) error { if strings.TrimSpace(targetURL) == "" { return errors.New("empty url") From a71a0895289a77d7b6782dc7528291fda288549f Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Thu, 26 Feb 2026 15:12:32 -0300 Subject: [PATCH 04/15] ref: split cli files, document architecture, small fixes --- .../.misc/dev-guides/explorer/architecture.md | 68 ++ docs/.misc/dev-guides/explorer/development.md | 6 +- docs/.misc/dev-guides/explorer/usage.md | 8 +- docs/README.md | 3 + pkg/ton/codec/debug/explorer/browser.go | 27 + pkg/ton/codec/debug/explorer/cli_args.go | 44 ++ pkg/ton/codec/debug/explorer/explorer.go | 606 ++---------------- pkg/ton/codec/debug/explorer/format.go | 33 + .../codec/debug/explorer/network_connect.go | 65 ++ .../debug/explorer/network_mylocalton.go | 99 +++ pkg/ton/codec/debug/explorer/tx_lookup.go | 277 ++++++++ .../visualizations/sequence/sanitizer.go | 37 ++ .../visualizations/sequence/sanitizer_test.go | 36 ++ .../sequence/sequence_diagram.go | 2 +- 14 files changed, 747 insertions(+), 564 deletions(-) create mode 100644 docs/.misc/dev-guides/explorer/architecture.md create mode 100644 pkg/ton/codec/debug/explorer/browser.go create mode 100644 pkg/ton/codec/debug/explorer/cli_args.go create mode 100644 pkg/ton/codec/debug/explorer/format.go create mode 100644 pkg/ton/codec/debug/explorer/network_connect.go create mode 100644 pkg/ton/codec/debug/explorer/network_mylocalton.go create mode 100644 pkg/ton/codec/debug/explorer/tx_lookup.go diff --git a/docs/.misc/dev-guides/explorer/architecture.md b/docs/.misc/dev-guides/explorer/architecture.md new file mode 100644 index 000000000..7684a61eb --- /dev/null +++ b/docs/.misc/dev-guides/explorer/architecture.md @@ -0,0 +1,68 @@ +# TON Explorer Architecture + +This document describes the explorer command architecture in `pkg/ton/codec/debug/explorer`. + +## Overview + +The explorer flow is organized into five stages: + +1. **CLI input normalization** +2. **Network/API connection setup** +3. **Transaction and trace discovery** +4. **Trace enrichment (actors/contracts)** +5. **Rendering/output** + +## Module layout + +- `explorer.go`: command wiring, `client` lifecycle, trace orchestration, actor discovery. +- `cli_args.go`: positional argument and URL/hash parsing integration (`parseCLIInput`). +- `utils.go`: explorer URL parsing (`ParseURL`). +- `format.go`: visualization format validation (`parseFormat`). +- `network_connect.go`: TON connection bootstrap (`connect`). +- `network_mylocalton.go`: Docker inspection helpers for `mylocalton`. +- `tx_lookup.go`: tx hash decoding, toncenter metadata lookups, tx search/fallback logic. +- `browser.go`: OS-specific browser opening for sequence URL mode. + +## Request lifecycle + +`GenerateExplorerCmd` parses args and flags, creates a `client` with `Connect`, and runs `PrintTrace`. + +`PrintTrace` performs: + +1. Resolve root tx hash from toncenter when supported (`mainnet`/`testnet`). +2. Resolve sender address: + - from user input when provided, + - from toncenter when hash-only mode is used on supported networks. +3. Locate transaction from account history (paged liteclient scan), with toncenter metadata fallback when available. +4. Convert transaction to trace root (`tracetracking.MapToReceivedMessage`) and wait for full trace (`WaitForTrace`). +5. Query contract actors via `typeAndVersion` getter. +6. Render either tree or sequence output. +7. For sequence URL mode, open Mermaid URL in browser. + +## Toncenter behavior + +Toncenter is treated as an optional dependency by network: + +- `mainnet`/`testnet`: toncenter is used for trace-root and tx metadata resolution. +- `mylocalton` or custom config URL networks: toncenter fallback is unavailable. + - Hash-only mode requires explicit source address in these environments. + +## Extension points + +For maintainability, keep future changes aligned with existing seams: + +- Input parsing changes in `cli_args.go`/`utils.go`. +- New visualization output options in `format.go` + rendering branch in `PrintTrace`. +- Network-specific bootstrap logic in `network_connect.go`. +- External metadata providers in `tx_lookup.go`. +- Browser side-effects in `browser.go`. + +## Compatibility contract + +Current CLI contract intentionally remains: + +- `explorer ` +- `explorer
` +- `explorer ` (works when address can be resolved via toncenter) + +`--address` and `--tx` flags were removed because they were unused and misleading. diff --git a/docs/.misc/dev-guides/explorer/development.md b/docs/.misc/dev-guides/explorer/development.md index 4d2300401..0040c047f 100644 --- a/docs/.misc/dev-guides/explorer/development.md +++ b/docs/.misc/dev-guides/explorer/development.md @@ -1,6 +1,8 @@ # TON Explorer Development Guide -For adding support to more contracts, you need to register your decoder in [`defaultDecoders`](../../../../pkg/ton/debug/pretty_print.go). Decoders implement [`ContractDecoder`](../../../../pkg/ton/debug/lib/lib.go) interface: +For explorer architecture and module boundaries, read [TON Explorer Architecture](./architecture.md). + +For adding support to more contracts, you need to register your decoder in [`defaultDecoders`](../../../../pkg/ton/codec/debug/pretty_print.go). Decoders implement [`ContractDecoder`](../../../../pkg/ton/codec/debug/lib/lib.go) interface: ```go type ContractDecoder interface { @@ -30,7 +32,7 @@ type BodyInfo interface { } ``` -Your decoder should go in `pkg/ton/debug/decoders/` package. If it is a ccip contract, then in `pkg/ton/debug/decoders/ccip`. E.g. `pkg/ton/debug/decoders/ccip/feequoter/feequoter.go`. +Your decoder should go in `pkg/ton/codec/debug/decoders/` package. If it is a ccip contract, then in `pkg/ton/codec/debug/decoders/ccip`. E.g. `pkg/ton/codec/debug/decoders/ccip/feequoter/feequoter.go`. I suggest not placing any business logic in the decoder. Instead, create a separate package for that, e.g. `pkg/ccip/bindings/feequoter/codec.go` and use it from the decoder. diff --git a/docs/.misc/dev-guides/explorer/usage.md b/docs/.misc/dev-guides/explorer/usage.md index 284322aae..6beb45822 100644 --- a/docs/.misc/dev-guides/explorer/usage.md +++ b/docs/.misc/dev-guides/explorer/usage.md @@ -2,13 +2,15 @@ Command-line tool for analyzing TON blockchain transactions and traces. +Read [TON Explorer Architecture](./architecture.md) for internal module layout and execution flow. + ## Usage Three ways to run: 1. **URL**: `./explorer ` 2. **Hash + Address**: `./explorer
` -3. **Hash only**: `./explorer ` (testnet/mainnet only) +3. **Hash only**: `./explorer ` (testnet/mainnet only unless sender address is provided separately) ## Run with Nix @@ -37,7 +39,7 @@ go build # Hash + address ./explorer
[--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] -# Hash only (auto-resolves address) +# Hash only (auto-resolves address via toncenter on testnet/mainnet) ./explorer [--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] ``` @@ -72,6 +74,8 @@ Display message trace as a tree structure with `--visualization tree`. --page-size 10 --max-pages 10 # Control transaction search pagination ``` +Note: `--address` and `--tx` flags are not supported; use positional arguments. + ## Environment injection The same cli is exposed in [chainlink-deployments's repo](https://github.com/smartcontractkit/chainlink-deployments/tree/main/domains/ccip/cmd) which injects contract metadata from the DataStore. diff --git a/docs/README.md b/docs/README.md index 9da387cd6..9678ab9d1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,9 @@ - [Nix - Getting Started](.misc/dev-guides/nix/getting-started.md) - [Nix - Builds](.misc/dev-guides/nix/builds.md) +- [Explorer - Usage](.misc/dev-guides/explorer/usage.md) +- [Explorer - Architecture](.misc/dev-guides/explorer/architecture.md) +- [Explorer - Development](.misc/dev-guides/explorer/development.md) ## CCIP Product E2E Tests diff --git a/pkg/ton/codec/debug/explorer/browser.go b/pkg/ton/codec/debug/explorer/browser.go new file mode 100644 index 000000000..6af09dca6 --- /dev/null +++ b/pkg/ton/codec/debug/explorer/browser.go @@ -0,0 +1,27 @@ +package explorer + +import ( + "context" + "errors" + "os/exec" + "runtime" + "strings" +) + +func openInBrowser(ctx context.Context, targetURL string) error { + if strings.TrimSpace(targetURL) == "" { + return errors.New("empty url") + } + + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.CommandContext(ctx, "open", targetURL) + case "windows": + cmd = exec.CommandContext(ctx, "rundll32", "url.dll,FileProtocolHandler", targetURL) + default: + cmd = exec.CommandContext(ctx, "xdg-open", targetURL) + } + + return cmd.Start() +} diff --git a/pkg/ton/codec/debug/explorer/cli_args.go b/pkg/ton/codec/debug/explorer/cli_args.go new file mode 100644 index 000000000..92b003458 --- /dev/null +++ b/pkg/ton/codec/debug/explorer/cli_args.go @@ -0,0 +1,44 @@ +package explorer + +import ( + "encoding/hex" + "errors" + "fmt" + "strings" + + "github.com/spf13/cobra" +) + +type cliInput struct { + txHash string + address string + net string +} + +func parseCLIInput(cmd *cobra.Command, args []string) (cliInput, error) { + urlOrTx := args[0] + txHash, address, parsedNet, parseURLErr := ParseURL(urlOrTx) + if parseURLErr == nil { + if cmd.Flags().Changed("net") { + return cliInput{}, errors.New("cannot specify network flag when using URL") + } + if len(args) == 2 { + address = args[1] + } + return cliInput{txHash: txHash, address: address, net: parsedNet}, nil + } + + if len(urlOrTx) != 64 && (len(urlOrTx) != 66 || !strings.HasPrefix(urlOrTx, "0x")) { + return cliInput{}, fmt.Errorf("failed to parse URL: %w", parseURLErr) + } + + if _, err := hex.DecodeString(strings.TrimPrefix(urlOrTx, "0x")); err != nil { + return cliInput{}, fmt.Errorf("invalid transaction hash or url: %w", err) + } + + if len(args) == 2 { + address = args[1] + } + + return cliInput{txHash: urlOrTx, address: address}, nil +} diff --git a/pkg/ton/codec/debug/explorer/explorer.go b/pkg/ton/codec/debug/explorer/explorer.go index 5a3d94f45..71d7f17e0 100644 --- a/pkg/ton/codec/debug/explorer/explorer.go +++ b/pkg/ton/codec/debug/explorer/explorer.go @@ -2,24 +2,12 @@ package explorer import ( "context" - "encoding/base64" - "encoding/hex" - "encoding/json" "errors" "fmt" - "net/http" - "net/url" - "os/exec" - "runtime" - "strconv" - "strings" - "time" "github.com/Masterminds/semver/v3" "github.com/spf13/cobra" "github.com/xssnick/tonutils-go/address" - "github.com/xssnick/tonutils-go/liteclient" - "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/ton" "go.uber.org/zap/zapcore" @@ -33,14 +21,12 @@ import ( func GenerateExplorerCmd(lggr *logger.Logger, contracts map[string]debug.TypeAndVersion, client *ton.APIClient) *cobra.Command { var ( - destAddressStr string - txHashStr string - net string - verbose bool - pageSize uint32 - maxPages uint32 - visualization string - format string + net string + verbose bool + pageSize uint32 + maxPages uint32 + visualization string + format string ) cmd := &cobra.Command{ @@ -81,32 +67,12 @@ Arguments: if client != nil && cmd.Flags().Changed("net") { return errors.New("cannot specify network flag when using existing client") } - var txHash, address, parsedNet string - - urlOrTx := args[0] - var parseURLErr error - txHash, address, parsedNet, parseURLErr = ParseURL(urlOrTx) - if parseURLErr == nil { - if cmd.Root().Flags().Changed("net") { - return errors.New("cannot specify network flag when using URL") - } - net = parsedNet - } else { - // Not a URL, treat as tx-hash - if len(urlOrTx) != 64 && (len(urlOrTx) != 66 || !strings.HasPrefix(urlOrTx, "0x")) { - return fmt.Errorf("failed to parse URL: %w", parseURLErr) - } - - _, err = hex.DecodeString(strings.TrimPrefix(urlOrTx, "0x")) - if err != nil { - return fmt.Errorf("invalid transaction hash or url: %w", err) - } - txHash = urlOrTx - } - if len(args) == 2 { - address = args[1] + input, err := parseCLIInput(cmd, args) + if err != nil { + return err } + net = input.net ctx := context.Background() client, err := Connect(log, client, net, verbose, pageSize, maxPages) @@ -117,7 +83,7 @@ Arguments: if err != nil { return fmt.Errorf("failed to parse format: %w", err) } - err = client.PrintTrace(ctx, txHash, address, explorerFormat, contracts) + err = client.PrintTrace(ctx, input.txHash, input.address, explorerFormat, contracts) if err != nil { return fmt.Errorf("failed to execute trace: %w", err) } @@ -125,10 +91,8 @@ Arguments: }, } - cmd.Flags().StringVarP(&destAddressStr, "address", "a", "", "Destination address in base64 (optional if provided as argument)") cmd.Flags().StringVarP(&visualization, "visualization", "V", "sequence", "Visualization format (sequence or tree)") cmd.Flags().StringVarP(&format, "format", "f", "", "Sequence visualization format (url or raw) (only for sequence visualization)") - cmd.Flags().StringVarP(&txHashStr, "tx", "t", "", "Transaction hash in hex (optional if provided as argument)") cmd.Flags().StringVarP(&net, "net", "n", "testnet", "TON network (mainnet, testnet, mylocalton, or http://domain/x.global.config.json)") if lggr == nil { cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Shows full body of unmatched messages") @@ -139,125 +103,8 @@ Arguments: return cmd } -func parseFormat(visualization string, format string) (Format, error) { - switch visualization { - case "tree": - if format != "" { - return Format(0), errors.New("format option is not applicable for tree visualization") - } - return FormatTree, nil - case "sequence": - switch format { - case "", "url": - return FormatSequenceURL, nil - case "raw": - return FormatSequenceRaw, nil - } - return Format(0), fmt.Errorf("invalid sequence format: %s", format) - } - return Format(0), fmt.Errorf("invalid visualization format: %s", format) -} - -// ContainerInspect represents the structure returned by docker inspect -type ContainerInspect struct { - ID string `json:"Id"` - State struct { - Running bool `json:"Running"` - } `json:"State"` - Config struct { - Image string `json:"Image"` - } `json:"Config"` - NetworkSettings struct { - Ports map[string][]struct { - HostIP string `json:"HostIp"` - HostPort string `json:"HostPort"` - } `json:"Ports"` - } `json:"NetworkSettings"` -} - -// findMylocaltonContainer finds a running mylocalton container and returns its ID -func findMylocaltonContainer(ctx context.Context) (string, error) { - cmd := exec.CommandContext(ctx, "docker", "ps", "--format", "{{.ID}}\t{{.Image}}", "--filter", "status=running") - output, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("failed to list docker containers: %w", err) - } - - lines := strings.Split(strings.TrimSpace(string(output)), "\n") - for _, line := range lines { - if line == "" { - continue - } - parts := strings.Split(line, "\t") - if len(parts) != 2 { - continue - } - containerID := parts[0] - image := parts[1] - - // Look for mylocalton containers, but exclude explorer - if strings.Contains(image, "mylocalton-docker") && !strings.Contains(image, "mylocalton-docker-explorer") { - return containerID, nil - } - } - - return "", errors.New("no running mylocalton container found") -} - -// inspectContainer runs docker inspect on the given container ID -func inspectContainer(ctx context.Context, containerID string) (*ContainerInspect, error) { - cmd := exec.CommandContext(ctx, "docker", "inspect", containerID) - output, err := cmd.CombinedOutput() - if err != nil { - if strings.Contains(string(output), "No such object") || strings.Contains(string(output), "No such container") { - return nil, fmt.Errorf("container %s does not exist", containerID) - } - return nil, fmt.Errorf("docker inspect failed: %w\nOutput: %s", err, string(output)) - } - - var inspects []ContainerInspect - if err := json.Unmarshal(output, &inspects); err != nil { - return nil, fmt.Errorf("failed to parse docker inspect output: %w", err) - } - - if len(inspects) == 0 { - return nil, fmt.Errorf("container %s not found", containerID) - } - - inspect := &inspects[0] - - if !inspect.State.Running { - return nil, fmt.Errorf("container %s exists but is not running", containerID) - } - - return inspect, nil -} - -// getPortMapping extracts the host port that maps to a given container port -func getPortMapping(inspect *ContainerInspect, containerPort string) (string, error) { - portKey := containerPort + "/tcp" - ports, exists := inspect.NetworkSettings.Ports[portKey] - if !exists || len(ports) == 0 { - return "", fmt.Errorf("no port mapping found for container port %s", containerPort) - } - - // Return the first host port mapping - hostPort := ports[0].HostPort - if hostPort == "" { - return "", fmt.Errorf("empty host port mapping for container port %s", containerPort) - } - - return hostPort, nil -} - // Connect establishes a connection to the specified TON network and returns an // explorer instance for tracing transactions. -// -// Parameters: -// - net: The TON network to connect to (e.g., "mainnet", "testnet", "mylocalton", "http://127.0.0.1:8000/localhost.global.config.json"). -// - verbose: Whether to enable verbose output. -// - pageSize: The number of transactions to fetch per page. -// - maxPages: The maximum number of pages to fetch. func Connect(lggr logger.Logger, apiClient *ton.APIClient, net string, verbose bool, pageSize uint32, maxPages uint32) (*client, error) { if apiClient == nil { var err error @@ -295,62 +142,27 @@ type client struct { func (c *client) resilientAPI() ton.APIClientWrapped { return c.connection.WithRetry(5) } -type Format int - -const ( - FormatTree Format = iota - FormatSequenceURL - FormatSequenceRaw -) - -type toncenterTxResult struct { - Account string `json:"account"` - LT string `json:"lt"` - BlockRef struct { - Workchain int32 `json:"workchain"` - Shard string `json:"shard"` - SeqNo uint32 `json:"seqno"` - } `json:"block_ref"` -} - -type toncenterAPIResponse struct { - Transactions []toncenterTxResult `json:"transactions"` -} - -type toncenterTraceResponse struct { - Traces []struct { - Trace struct { - TxHash string `json:"tx_hash"` - } `json:"trace"` - TransactionsOrder []string `json:"transactions_order"` - } `json:"traces"` -} - -// PrintTrace connects to the specified TON network, retrieves the transaction -// by the given source address and transaction hash, and prints the full execution -// trace of the transaction, including all outgoing messages and their subsequent -// messages. -// -// Parameters: -// - ctx: The context for managing request deadlines and cancellation. -// - txHashStr: The transaction hash in hexadecimal format. -// - srcAddrStr: The source address of the transaction in string format. func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr string, format Format, knownActors map[string]debug.TypeAndVersion) error { api := c.resilientAPI() effectiveTxHash := txHashStr - rootTxHash, rootErr := c.getTraceRootTxHash(ctx, txHashStr) - if rootErr == nil && rootTxHash != "" { - effectiveTxHash = rootTxHash - if rootTxHash != txHashStr { - c.lggr.Info("resolved input transaction to trace root", "input_tx_hash", txHashStr, "root_tx_hash", rootTxHash) + if c.supportsToncenter() { + rootTxHash, rootErr := c.getTraceRootTxHash(ctx, txHashStr) + if rootErr == nil && rootTxHash != "" { + effectiveTxHash = rootTxHash + if rootTxHash != txHashStr { + c.lggr.Info("resolved input transaction to trace root", "input_tx_hash", txHashStr, "root_tx_hash", rootTxHash) + } + } else if rootErr != nil { + c.lggr.Debug("failed to resolve trace root tx hash, continuing with provided tx", "tx_hash", txHashStr, "error", rootErr) } - } else if rootErr != nil { - c.lggr.Debug("failed to resolve trace root tx hash, continuing with provided tx", "tx_hash", txHashStr, "error", rootErr) } var senderAddr *address.Address var err error if srcAddrStr == "" { + if !c.supportsToncenter() { + return fmt.Errorf("source address is required for network %s when toncenter metadata is unavailable", c.net) + } c.lggr.Debug("source address not provided, attempting to fetch from toncenter by hash...") senderAddr, err = c.GetSenderAddressFromTxHash(ctx, effectiveTxHash) if err != nil { @@ -358,24 +170,30 @@ func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr st } c.lggr.Debug("source address found:", senderAddr.String()) } else if effectiveTxHash != txHashStr { - // User-provided address may correspond to a non-root tx. Prefer root tx account. - senderAddr, err = c.GetSenderAddressFromTxHash(ctx, effectiveTxHash) - if err != nil { - return fmt.Errorf("failed to get root sender address from tx hash: %w", err) + if c.supportsToncenter() { + senderAddr, err = c.GetSenderAddressFromTxHash(ctx, effectiveTxHash) + if err != nil { + return fmt.Errorf("failed to get root sender address from tx hash: %w", err) + } + c.lggr.Debug("overriding provided source address with trace root account", senderAddr.String()) + } else { + senderAddr, err = address.ParseAddr(srcAddrStr) + if err != nil { + return fmt.Errorf("failed to parse transaction address: %w", err) + } } - c.lggr.Debug("overriding provided source address with trace root account", senderAddr.String()) } else { senderAddr, err = address.ParseAddr(srcAddrStr) if err != nil { return fmt.Errorf("failed to parse transaction address: %w", err) } } - txHash, err := decodeTxHash(effectiveTxHash) + decodedTxHash, err := decodeTxHash(effectiveTxHash) if err != nil { return fmt.Errorf("failed to decode tx hash: %w", err) } - tx, err := c.findTx(ctx, api, senderAddr, effectiveTxHash, txHash) + tx, err := c.findTx(ctx, api, senderAddr, effectiveTxHash, decodedTxHash) if err != nil { return err } @@ -388,22 +206,17 @@ func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr st } c.lggr.Info("waiting for full trace...") - - err = recvMsg.WaitForTrace(ctx, api) - if err != nil { + if err = recvMsg.WaitForTrace(ctx, api); err != nil { return fmt.Errorf("failed to wait for trace: %w", err) } c.lggr.Debug("actors before query:\n", knownActors) c.lggr.Info("querying actors") - err = c.queryActors(ctx, api, &recvMsg, knownActors) - if err != nil { + if err = c.queryActors(ctx, api, &recvMsg, knownActors); err != nil { return fmt.Errorf("failed to query actors: %w", err) } c.lggr.Debug("actors after query:\n", knownActors) - c.lggr.Info("full trace received:") - var debugger debug.DebuggerEnvironment switch format { case FormatSequenceURL: @@ -418,7 +231,7 @@ func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr st output := debugger.DumpReceived(&recvMsg, c.verbose) if format == FormatSequenceURL { - if err := openInBrowser(ctx, output); err != nil { + if err = openInBrowser(ctx, output); err != nil { return fmt.Errorf("failed to open mermaid url in browser: %w", err) } c.lggr.Info("opened mermaid visualization in browser") @@ -426,110 +239,6 @@ func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr st } c.lggr.Info(output) - - return nil -} - -func decodeTxHash(txHash string) ([]byte, error) { - if after, ok := strings.CutPrefix(txHash, "0x"); ok { - txHash = after - } - - if raw, err := hex.DecodeString(txHash); err == nil { - return raw, nil - } - - if raw, err := base64.StdEncoding.DecodeString(txHash); err == nil { - return raw, nil - } - - if raw, err := base64.URLEncoding.DecodeString(txHash); err == nil { - return raw, nil - } - - if raw, err := base64.RawURLEncoding.DecodeString(txHash); err == nil { - return raw, nil - } - - return nil, fmt.Errorf("unsupported tx hash format: %s", txHash) -} - -func (c *client) getTraceRootTxHash(ctx context.Context, txHashStr string) (string, error) { - var baseURL string - switch c.net { - case "mainnet": - baseURL = "https://toncenter.com/api/v3/traces" - case "testnet": - baseURL = "https://testnet.toncenter.com/api/v3/traces" - default: - return "", fmt.Errorf("unsupported network for trace index lookup: %s", c.net) - } - - u, err := url.Parse(baseURL) - if err != nil { - return "", fmt.Errorf("invalid trace endpoint url: %w", err) - } - - q := u.Query() - q.Set("tx_hash", txHashStr) - u.RawQuery = q.Encode() - - httpClient := &http.Client{Timeout: 30 * time.Second} - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) - if err != nil { - return "", fmt.Errorf("failed to create trace request: %w", err) - } - - resp, err := httpClient.Do(req) - if err != nil { - return "", fmt.Errorf("failed to fetch trace from toncenter: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("unexpected status code from trace endpoint: %d", resp.StatusCode) - } - - var traceResp toncenterTraceResponse - if err := json.NewDecoder(resp.Body).Decode(&traceResp); err != nil { - return "", fmt.Errorf("failed to decode trace response: %w", err) - } - - if len(traceResp.Traces) == 0 { - return "", errors.New("no trace found for transaction") - } - - trace := traceResp.Traces[0] - if len(trace.TransactionsOrder) > 0 && trace.TransactionsOrder[0] != "" { - return trace.TransactionsOrder[0], nil - } - - if trace.Trace.TxHash != "" { - return trace.Trace.TxHash, nil - } - - return "", errors.New("trace root hash missing in trace response") -} - -func openInBrowser(ctx context.Context, targetURL string) error { - if strings.TrimSpace(targetURL) == "" { - return errors.New("empty url") - } - - var cmd *exec.Cmd - switch runtime.GOOS { - case "darwin": - cmd = exec.CommandContext(ctx, "open", targetURL) - case "windows": - cmd = exec.CommandContext(ctx, "rundll32", "url.dll,FileProtocolHandler", targetURL) - default: - cmd = exec.CommandContext(ctx, "xdg-open", targetURL) - } - - if err := cmd.Start(); err != nil { - return err - } - return nil } @@ -552,15 +261,14 @@ func (c *client) queryActorsReceivedRec(ctx context.Context, api ton.APIClientWr if err != nil { return err } - err = c.queryOutgoingMessages(ctx, api, block, message.OutgoingInternalSentMessages, message.OutgoingInternalReceivedMessages, knownActors, visited) - return err - } else if message.ExternalMsg != nil { + return c.queryOutgoingMessages(ctx, api, block, message.OutgoingInternalSentMessages, message.OutgoingInternalReceivedMessages, knownActors, visited) + } + if message.ExternalMsg != nil { err := c.queryActorIfNotVisited(ctx, api, block, message.ExternalMsg.DstAddr, knownActors, visited) if err != nil { return err } - err = c.queryOutgoingMessages(ctx, api, block, message.OutgoingInternalSentMessages, message.OutgoingInternalReceivedMessages, knownActors, visited) - return err + return c.queryOutgoingMessages(ctx, api, block, message.OutgoingInternalSentMessages, message.OutgoingInternalReceivedMessages, knownActors, visited) } return fmt.Errorf("unknown message type: %+v", message) } @@ -577,8 +285,7 @@ func (c *client) queryOutgoingMessages(ctx context.Context, api ton.APIClientWra } } for _, outMsg := range outgoingReceivedMessages { - err := c.queryActorsReceivedRec(ctx, api, block, outMsg, knownActors, visited) - if err != nil { + if err := c.queryActorsReceivedRec(ctx, api, block, outMsg, knownActors, visited); err != nil { return err } } @@ -587,28 +294,20 @@ func (c *client) queryOutgoingMessages(ctx context.Context, api ton.APIClientWra func (c *client) queryActorIfNotVisited(ctx context.Context, api ton.APIClientWrapped, block *ton.BlockIDExt, addr *address.Address, knownActors map[string]debug.TypeAndVersion, visited map[string]bool) error { c.lggr.Debug("queryActorIfNotVisited", addr.String()) - c.lggr.Debug("visited:", visited) - c.lggr.Debug("knownActors:", knownActors) if visited[addr.String()] { - c.lggr.Debug("already visited", addr.String()) return nil } if _, known := knownActors[addr.String()]; known { visited[addr.String()] = true - c.lggr.Debug("actor found in knownActors", addr.String()) return nil } - c.lggr.Debug("actor not known") - var typeVersion common.TypeAndVersion + result, err := api.RunGetMethod(ctx, block, addr, "typeAndVersion") if err != nil { - // We don't fail here because many contracts don't implement typeAndVersion - return nil // TODO try deducing from code? + return nil } - defer func() { - }() - typeVersion, err = common.GetTypeAndVersion.Decoder.Decode(result) + typeVersion, err := common.GetTypeAndVersion.Decoder.Decode(result) if err != nil { return fmt.Errorf("failed to parse typeAndVersion: %w", err) } @@ -620,214 +319,3 @@ func (c *client) queryActorIfNotVisited(ctx context.Context, api ton.APIClientWr } return nil } - -func (c *client) GetSenderAddressFromTxHash(ctx context.Context, txHashStr string) (*address.Address, error) { - res, err := c.getToncenterTxByHash(ctx, txHashStr) - if err != nil { - return nil, err - } - - addr, err := address.ParseRawAddr(res.Account) - if err != nil { - return nil, fmt.Errorf("failed to parse source address from toncenter response: %w", err) - } - return addr, nil -} - -func (c *client) getToncenterTxByHash(ctx context.Context, txHashStr string) (*toncenterTxResult, error) { - // fetch from https://testnet.toncenter.com/api/v3/transactions?hash=txHashStr - var baseURL string - switch c.net { - case "mainnet": - baseURL = "https://toncenter.com/api/v3/transactions" - case "testnet": - baseURL = "https://testnet.toncenter.com/api/v3/transactions" - default: - return nil, fmt.Errorf("unsupported network: %s", c.net) - } - // Use url.URL for safer URL construction - u, err := url.Parse(baseURL) - if err != nil { - return nil, fmt.Errorf("invalid base URL: %w", err) - } - - // Add query parameters safely - q := u.Query() - q.Set("hash", txHashStr) // No need for manual encoding when using url.Values - u.RawQuery = q.Encode() - - // Create request with context and timeout - client := &http.Client{Timeout: 30 * time.Second} - req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to fetch transaction info from toncenter: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code from toncenter: %d", resp.StatusCode) - } - var respData toncenterAPIResponse - err = json.NewDecoder(resp.Body).Decode(&respData) - if err != nil { - return nil, fmt.Errorf("failed to decode toncenter response: %w", err) - } - if len(respData.Transactions) != 1 { - return nil, errors.New("transaction not found in toncenter response") - } - - return &respData.Transactions[0], nil -} - -func (c *client) findTxByToncenterMetadata(ctx context.Context, api ton.APIClientWrapped, txHashStr string, txHash []byte, srcAddr *address.Address) (*tlb.Transaction, error) { - res, err := c.getToncenterTxByHash(ctx, txHashStr) - if err != nil { - return nil, err - } - - lt, err := strconv.ParseUint(res.LT, 10, 64) - if err != nil { - return nil, fmt.Errorf("failed to parse lt from toncenter response: %w", err) - } - - shard, err := strconv.ParseUint(res.BlockRef.Shard, 16, 64) - if err != nil { - return nil, fmt.Errorf("failed to parse shard from toncenter response: %w", err) - } - - block, err := api.LookupBlock(ctx, res.BlockRef.Workchain, int64(shard), res.BlockRef.SeqNo) - if err != nil { - return nil, fmt.Errorf("failed to lookup block from toncenter metadata: %w", err) - } - - tx, err := api.GetTransaction(ctx, block, srcAddr, lt) - if err != nil { - return nil, fmt.Errorf("failed to fetch transaction from toncenter metadata: %w", err) - } - - if !equalHash(tx.Hash, txHash) { - return nil, errors.New("toncenter metadata lookup returned a different transaction hash") - } - - return tx, nil -} - -func (c *client) findTx(ctx context.Context, api ton.APIClientWrapped, srcAddr *address.Address, txHashStr string, txHash []byte) (*tlb.Transaction, error) { - block, err := api.GetMasterchainInfo(ctx) - if err != nil { - return nil, fmt.Errorf("get masterchain info: %w", err) - } - account, err := api.GetAccount(ctx, block, srcAddr) - if err != nil { - return nil, fmt.Errorf("get account: %w", err) - } - - // Start from the latest transaction - maxLT := account.LastTxLT - maxHash := account.LastTxHash - for range c.maxPages { - txs, err := api.ListTransactions(ctx, srcAddr, c.pageSize, maxLT, maxHash) - if err != nil { - return nil, fmt.Errorf("get transaction: %w", err) - } - if len(txs) == 0 { - return nil, errors.New("transaction not found in searched range. Try increasing --page-size and --max-pages") - } - for _, tx := range txs { - if equalHash(tx.Hash, txHash) { - return tx, nil - } - } - // Move to the previous page - last := txs[len(txs)-1] - maxLT = last.PrevTxLT - maxHash = last.PrevTxHash - } - tx, err := c.findTxByToncenterMetadata(ctx, api, txHashStr, txHash, srcAddr) - if err == nil { - return tx, nil - } - - return nil, fmt.Errorf("transaction not found in searched range. Try increasing --page-size and --max-pages (fallback failed: %w)", err) -} - -func equalHash(a, b []byte) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] != b[i] { - return false - } - } - return true -} - -func connect(ctx context.Context, net string) (*ton.APIClient, error) { - pool := liteclient.NewConnectionPool() - switch net { - case "mainnet": - configURL := "https://ton-blockchain.github.io/global.config.json" - err := pool.AddConnectionsFromConfigUrl(ctx, configURL) - if err != nil { - return nil, fmt.Errorf("failed to add connections from config url: %w", err) - } - case "testnet": - configURL := "https://ton.org/testnet-global.config.json" - err := pool.AddConnectionsFromConfigUrl(ctx, configURL) - if err != nil { - return nil, fmt.Errorf("failed to add connections from config url: %w", err) - } - case "mylocalton": - // Find running mylocalton container - containerID, err := findMylocaltonContainer(ctx) - if err != nil { - return nil, fmt.Errorf("failed to find mylocalton container: %w", err) - } - - // Inspect the container to get port mappings - inspect, err := inspectContainer(ctx, containerID) - if err != nil { - return nil, fmt.Errorf("failed to inspect container %s: %w", containerID, err) - } - - // Get the external port mapping for internal port 8000 (config server) - configPort, err := getPortMapping(inspect, "8000") - if err != nil { - return nil, fmt.Errorf("failed to get port mapping for config server: %w", err) - } - - // Fetch the config from the mapped port - configURL := fmt.Sprintf("http://127.0.0.1:%s/localhost.global.config.json", configPort) - config, err := liteclient.GetConfigFromUrl(ctx, configURL) - if err != nil { - return nil, fmt.Errorf("failed to get config from url: %w", err) - } - - // Get the liteserver port mapping - liteserverConfig := config.Liteservers[0] - liteserverPort := strconv.Itoa(liteserverConfig.Port) - externalLiteserverPort, err := getPortMapping(inspect, liteserverPort) - if err != nil { - return nil, fmt.Errorf("failed to get port mapping for liteserver: %w", err) - } - - // Connect to the liteserver using the external port - connectionString := "127.0.0.1:" + externalLiteserverPort - err = pool.AddConnection(ctx, connectionString, liteserverConfig.ID.Key) - if err != nil { - return nil, fmt.Errorf("failed to add localton connection: %w", err) - } - default: - configURL := net - err := pool.AddConnectionsFromConfigUrl(ctx, configURL) - if err != nil { - return nil, fmt.Errorf("failed to add connections from config url: %w", err) - } - } - return ton.NewAPIClient(pool, ton.ProofCheckPolicyFast), nil -} diff --git a/pkg/ton/codec/debug/explorer/format.go b/pkg/ton/codec/debug/explorer/format.go new file mode 100644 index 000000000..7ffc3f74b --- /dev/null +++ b/pkg/ton/codec/debug/explorer/format.go @@ -0,0 +1,33 @@ +package explorer + +import ( + "errors" + "fmt" +) + +type Format int + +const ( + FormatTree Format = iota + FormatSequenceURL + FormatSequenceRaw +) + +func parseFormat(visualization string, format string) (Format, error) { + switch visualization { + case "tree": + if format != "" { + return Format(0), errors.New("format option is not applicable for tree visualization") + } + return FormatTree, nil + case "sequence": + switch format { + case "", "url": + return FormatSequenceURL, nil + case "raw": + return FormatSequenceRaw, nil + } + return Format(0), fmt.Errorf("invalid sequence format: %s", format) + } + return Format(0), fmt.Errorf("invalid visualization format: %s", visualization) +} diff --git a/pkg/ton/codec/debug/explorer/network_connect.go b/pkg/ton/codec/debug/explorer/network_connect.go new file mode 100644 index 000000000..31705dced --- /dev/null +++ b/pkg/ton/codec/debug/explorer/network_connect.go @@ -0,0 +1,65 @@ +package explorer + +import ( + "context" + "fmt" + "strconv" + + "github.com/xssnick/tonutils-go/liteclient" + "github.com/xssnick/tonutils-go/ton" +) + +func connect(ctx context.Context, net string) (*ton.APIClient, error) { + pool := liteclient.NewConnectionPool() + switch net { + case "mainnet": + configURL := "https://ton-blockchain.github.io/global.config.json" + if err := pool.AddConnectionsFromConfigUrl(ctx, configURL); err != nil { + return nil, fmt.Errorf("failed to add connections from config url: %w", err) + } + case "testnet": + configURL := "https://ton.org/testnet-global.config.json" + if err := pool.AddConnectionsFromConfigUrl(ctx, configURL); err != nil { + return nil, fmt.Errorf("failed to add connections from config url: %w", err) + } + case "mylocalton": + containerID, err := findMylocaltonContainer(ctx) + if err != nil { + return nil, fmt.Errorf("failed to find mylocalton container: %w", err) + } + + inspect, err := inspectContainer(ctx, containerID) + if err != nil { + return nil, fmt.Errorf("failed to inspect container %s: %w", containerID, err) + } + + configPort, err := getPortMapping(inspect, "8000") + if err != nil { + return nil, fmt.Errorf("failed to get port mapping for config server: %w", err) + } + + configURL := fmt.Sprintf("http://127.0.0.1:%s/localhost.global.config.json", configPort) + config, err := liteclient.GetConfigFromUrl(ctx, configURL) + if err != nil { + return nil, fmt.Errorf("failed to get config from url: %w", err) + } + + liteserverConfig := config.Liteservers[0] + liteserverPort := strconv.Itoa(liteserverConfig.Port) + externalLiteserverPort, err := getPortMapping(inspect, liteserverPort) + if err != nil { + return nil, fmt.Errorf("failed to get port mapping for liteserver: %w", err) + } + + connectionString := "127.0.0.1:" + externalLiteserverPort + if err = pool.AddConnection(ctx, connectionString, liteserverConfig.ID.Key); err != nil { + return nil, fmt.Errorf("failed to add localton connection: %w", err) + } + default: + if err := pool.AddConnectionsFromConfigUrl(ctx, net); err != nil { + return nil, fmt.Errorf("failed to add connections from config url: %w", err) + } + } + + return ton.NewAPIClient(pool, ton.ProofCheckPolicyFast), nil +} diff --git a/pkg/ton/codec/debug/explorer/network_mylocalton.go b/pkg/ton/codec/debug/explorer/network_mylocalton.go new file mode 100644 index 000000000..4d820e94f --- /dev/null +++ b/pkg/ton/codec/debug/explorer/network_mylocalton.go @@ -0,0 +1,99 @@ +package explorer + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os/exec" + "strings" +) + +// ContainerInspect represents the structure returned by docker inspect +// for the fields needed by mylocalton discovery. +type ContainerInspect struct { + ID string `json:"Id"` + State struct { + Running bool `json:"Running"` + } `json:"State"` + Config struct { + Image string `json:"Image"` + } `json:"Config"` + NetworkSettings struct { + Ports map[string][]struct { + HostIP string `json:"HostIp"` + HostPort string `json:"HostPort"` + } `json:"Ports"` + } `json:"NetworkSettings"` +} + +// findMylocaltonContainer finds a running mylocalton container and returns its ID. +func findMylocaltonContainer(ctx context.Context) (string, error) { + cmd := exec.CommandContext(ctx, "docker", "ps", "--format", "{{.ID}}\t{{.Image}}", "--filter", "status=running") + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to list docker containers: %w", err) + } + + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, line := range lines { + if line == "" { + continue + } + parts := strings.Split(line, "\t") + if len(parts) != 2 { + continue + } + containerID := parts[0] + image := parts[1] + + if strings.Contains(image, "mylocalton-docker") && !strings.Contains(image, "mylocalton-docker-explorer") { + return containerID, nil + } + } + + return "", errors.New("no running mylocalton container found") +} + +// inspectContainer runs docker inspect on the given container ID. +func inspectContainer(ctx context.Context, containerID string) (*ContainerInspect, error) { + cmd := exec.CommandContext(ctx, "docker", "inspect", containerID) + output, err := cmd.CombinedOutput() + if err != nil { + if strings.Contains(string(output), "No such object") || strings.Contains(string(output), "No such container") { + return nil, fmt.Errorf("container %s does not exist", containerID) + } + return nil, fmt.Errorf("docker inspect failed: %w\nOutput: %s", err, string(output)) + } + + var inspects []ContainerInspect + if err := json.Unmarshal(output, &inspects); err != nil { + return nil, fmt.Errorf("failed to parse docker inspect output: %w", err) + } + if len(inspects) == 0 { + return nil, fmt.Errorf("container %s not found", containerID) + } + + inspect := &inspects[0] + if !inspect.State.Running { + return nil, fmt.Errorf("container %s exists but is not running", containerID) + } + + return inspect, nil +} + +// getPortMapping extracts the host port that maps to a given container port. +func getPortMapping(inspect *ContainerInspect, containerPort string) (string, error) { + portKey := containerPort + "/tcp" + ports, exists := inspect.NetworkSettings.Ports[portKey] + if !exists || len(ports) == 0 { + return "", fmt.Errorf("no port mapping found for container port %s", containerPort) + } + + hostPort := ports[0].HostPort + if hostPort == "" { + return "", fmt.Errorf("empty host port mapping for container port %s", containerPort) + } + + return hostPort, nil +} diff --git a/pkg/ton/codec/debug/explorer/tx_lookup.go b/pkg/ton/codec/debug/explorer/tx_lookup.go new file mode 100644 index 000000000..9e3ddcb92 --- /dev/null +++ b/pkg/ton/codec/debug/explorer/tx_lookup.go @@ -0,0 +1,277 @@ +package explorer + +import ( + "context" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton" +) + +type toncenterTxResult struct { + Account string `json:"account"` + LT string `json:"lt"` + BlockRef struct { + Workchain int32 `json:"workchain"` + Shard string `json:"shard"` + SeqNo uint32 `json:"seqno"` + } `json:"block_ref"` +} + +type toncenterAPIResponse struct { + Transactions []toncenterTxResult `json:"transactions"` +} + +type toncenterTraceResponse struct { + Traces []struct { + Trace struct { + TxHash string `json:"tx_hash"` + } `json:"trace"` + TransactionsOrder []string `json:"transactions_order"` + } `json:"traces"` +} + +func (c *client) supportsToncenter() bool { + return c.net == "mainnet" || c.net == "testnet" +} + +func decodeTxHash(txHash string) ([]byte, error) { + if after, ok := strings.CutPrefix(txHash, "0x"); ok { + txHash = after + } + + if raw, err := hex.DecodeString(txHash); err == nil { + return raw, nil + } + if raw, err := base64.StdEncoding.DecodeString(txHash); err == nil { + return raw, nil + } + if raw, err := base64.URLEncoding.DecodeString(txHash); err == nil { + return raw, nil + } + if raw, err := base64.RawURLEncoding.DecodeString(txHash); err == nil { + return raw, nil + } + + return nil, fmt.Errorf("unsupported tx hash format: %s", txHash) +} + +func (c *client) getTraceRootTxHash(ctx context.Context, txHashStr string) (string, error) { + u, err := c.tonCenterTraceURL() + if err != nil { + return "", err + } + + q := u.Query() + q.Set("tx_hash", txHashStr) + u.RawQuery = q.Encode() + + httpClient := &http.Client{Timeout: 30 * time.Second} + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return "", fmt.Errorf("failed to create trace request: %w", err) + } + + resp, err := httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to fetch trace from toncenter: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status code from trace endpoint: %d", resp.StatusCode) + } + + var traceResp toncenterTraceResponse + if err = json.NewDecoder(resp.Body).Decode(&traceResp); err != nil { + return "", fmt.Errorf("failed to decode trace response: %w", err) + } + + if len(traceResp.Traces) == 0 { + return "", errors.New("no trace found for transaction") + } + + trace := traceResp.Traces[0] + if len(trace.TransactionsOrder) > 0 && trace.TransactionsOrder[0] != "" { + return trace.TransactionsOrder[0], nil + } + if trace.Trace.TxHash != "" { + return trace.Trace.TxHash, nil + } + + return "", errors.New("trace root hash missing in trace response") +} + +func (c *client) GetSenderAddressFromTxHash(ctx context.Context, txHashStr string) (*address.Address, error) { + res, err := c.getToncenterTxByHash(ctx, txHashStr) + if err != nil { + return nil, err + } + + addr, err := address.ParseRawAddr(res.Account) + if err != nil { + return nil, fmt.Errorf("failed to parse source address from toncenter response: %w", err) + } + return addr, nil +} + +func (c *client) getToncenterTxByHash(ctx context.Context, txHashStr string) (*toncenterTxResult, error) { + u, err := c.tonCenterTransactionsURL() + if err != nil { + return nil, err + } + + q := u.Query() + q.Set("hash", txHashStr) + u.RawQuery = q.Encode() + + httpClient := &http.Client{Timeout: 30 * time.Second} + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch transaction info from toncenter: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code from toncenter: %d", resp.StatusCode) + } + + var respData toncenterAPIResponse + if err = json.NewDecoder(resp.Body).Decode(&respData); err != nil { + return nil, fmt.Errorf("failed to decode toncenter response: %w", err) + } + if len(respData.Transactions) != 1 { + return nil, errors.New("transaction not found in toncenter response") + } + + return &respData.Transactions[0], nil +} + +func (c *client) findTxByToncenterMetadata(ctx context.Context, api ton.APIClientWrapped, txHashStr string, txHash []byte, srcAddr *address.Address) (*tlb.Transaction, error) { + res, err := c.getToncenterTxByHash(ctx, txHashStr) + if err != nil { + return nil, err + } + + lt, err := strconv.ParseUint(res.LT, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse lt from toncenter response: %w", err) + } + + shard, err := strconv.ParseUint(res.BlockRef.Shard, 16, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse shard from toncenter response: %w", err) + } + + block, err := api.LookupBlock(ctx, res.BlockRef.Workchain, int64(shard), res.BlockRef.SeqNo) + if err != nil { + return nil, fmt.Errorf("failed to lookup block from toncenter metadata: %w", err) + } + + tx, err := api.GetTransaction(ctx, block, srcAddr, lt) + if err != nil { + return nil, fmt.Errorf("failed to fetch transaction from toncenter metadata: %w", err) + } + + if !equalHash(tx.Hash, txHash) { + return nil, errors.New("toncenter metadata lookup returned a different transaction hash") + } + + return tx, nil +} + +func (c *client) findTx(ctx context.Context, api ton.APIClientWrapped, srcAddr *address.Address, txHashStr string, txHash []byte) (*tlb.Transaction, error) { + block, err := api.GetMasterchainInfo(ctx) + if err != nil { + return nil, fmt.Errorf("get masterchain info: %w", err) + } + account, err := api.GetAccount(ctx, block, srcAddr) + if err != nil { + return nil, fmt.Errorf("get account: %w", err) + } + + maxLT := account.LastTxLT + maxHash := account.LastTxHash + for range c.maxPages { + txs, listErr := api.ListTransactions(ctx, srcAddr, c.pageSize, maxLT, maxHash) + if listErr != nil { + return nil, fmt.Errorf("get transaction: %w", listErr) + } + if len(txs) == 0 { + return nil, errors.New("transaction not found in searched range. Try increasing --page-size and --max-pages") + } + for _, tx := range txs { + if equalHash(tx.Hash, txHash) { + return tx, nil + } + } + + last := txs[len(txs)-1] + maxLT = last.PrevTxLT + maxHash = last.PrevTxHash + } + + if !c.supportsToncenter() { + return nil, errors.New("transaction not found in searched range and toncenter fallback is unavailable for this network") + } + + fallbackTx, fallbackErr := c.findTxByToncenterMetadata(ctx, api, txHashStr, txHash, srcAddr) + if fallbackErr == nil { + return fallbackTx, nil + } + return nil, fmt.Errorf("transaction not found in searched range. Try increasing --page-size and --max-pages (fallback failed: %w)", fallbackErr) +} + +func equalHash(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func (c *client) tonCenterTraceURL() (*url.URL, error) { + return c.tonCenterURL("traces") +} + +func (c *client) tonCenterTransactionsURL() (*url.URL, error) { + return c.tonCenterURL("transactions") +} + +func (c *client) tonCenterURL(path string) (*url.URL, error) { + var baseURL string + switch c.net { + case "mainnet": + baseURL = "https://toncenter.com/api/v3/" + case "testnet": + baseURL = "https://testnet.toncenter.com/api/v3/" + default: + return nil, fmt.Errorf("unsupported network for toncenter lookup: %s", c.net) + } + + u, err := url.Parse(baseURL) + if err != nil { + return nil, fmt.Errorf("invalid base URL: %w", err) + } + u.Path = strings.TrimSuffix(u.Path, "/") + "/" + path + return u, nil +} diff --git a/pkg/ton/codec/debug/visualizations/sequence/sanitizer.go b/pkg/ton/codec/debug/visualizations/sequence/sanitizer.go index 3aceaf90c..4cb8d4a02 100644 --- a/pkg/ton/codec/debug/visualizations/sequence/sanitizer.go +++ b/pkg/ton/codec/debug/visualizations/sequence/sanitizer.go @@ -5,6 +5,43 @@ import ( "unicode/utf8" ) +func sanitizeMermaidIdentifier(s string) string { + if s == "" { + return "actor" + } + + var b strings.Builder + b.Grow(len(s) + 6) + + for i, r := range s { + isAlpha := (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') + isDigit := r >= '0' && r <= '9' + + if i == 0 { + if isAlpha || r == '_' { + b.WriteRune(r) + continue + } + if isDigit { + b.WriteString("a_") + b.WriteRune(r) + continue + } + b.WriteString("a_") + b.WriteByte('_') + continue + } + + if isAlpha || isDigit || r == '_' { + b.WriteRune(r) + } else { + b.WriteByte('_') + } + } + + return b.String() +} + func sanitizeString(s string) string { var b strings.Builder for i, line := range strings.Split(s, "\n") { diff --git a/pkg/ton/codec/debug/visualizations/sequence/sanitizer_test.go b/pkg/ton/codec/debug/visualizations/sequence/sanitizer_test.go index ff8159ccf..6e85808f7 100644 --- a/pkg/ton/codec/debug/visualizations/sequence/sanitizer_test.go +++ b/pkg/ton/codec/debug/visualizations/sequence/sanitizer_test.go @@ -135,3 +135,39 @@ func TestWrap(t *testing.T) { }) } } + +func TestSanitizeMermaidIdentifier(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "masterchain raw address", + input: "-1_e69571e7b9f58edfebefa297e547f36920532289dbe9ff1b76d107fcbac30104", + expected: "a__1_e69571e7b9f58edfebefa297e547f36920532289dbe9ff1b76d107fcbac30104", + }, + { + name: "colon separated raw address", + input: "-1:e69571e7b9f58edfebefa297e547f36920532289dbe9ff1b76d107fcbac30104", + expected: "a__1_e69571e7b9f58edfebefa297e547f36920532289dbe9ff1b76d107fcbac30104", + }, + { + name: "starts with digit", + input: "0_abc", + expected: "a_0_abc", + }, + { + name: "already valid", + input: "abc_123", + expected: "abc_123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := sanitizeMermaidIdentifier(tt.input) + assert.Equalf(t, tt.expected, result, "Failed test: %s", tt.name) + }) + } +} diff --git a/pkg/ton/codec/debug/visualizations/sequence/sequence_diagram.go b/pkg/ton/codec/debug/visualizations/sequence/sequence_diagram.go index 280756c20..6a6c5a9af 100644 --- a/pkg/ton/codec/debug/visualizations/sequence/sequence_diagram.go +++ b/pkg/ton/codec/debug/visualizations/sequence/sequence_diagram.go @@ -120,7 +120,7 @@ func (v *visualization) actorFromAddr(addr *address.Address) *sequence.Actor { var actor *sequence.Actor var ok bool name := v.describeAddr(addr) - id := strings.ReplaceAll(addr.StringRaw(), ":", "_") + id := sanitizeMermaidIdentifier(addr.StringRaw()) if actor, ok = v.ActiveActors[id]; !ok { actor = v.Diagram.AddActor(id, name, sequence.ActorParticipant) v.ActiveActors[id] = actor From e3e5ba080ce8bf9ec996d83e9781794599425a42 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Thu, 26 Feb 2026 15:59:33 -0300 Subject: [PATCH 05/15] ref: extract getSenderAddress --- pkg/ton/codec/debug/explorer/explorer.go | 64 +++++++++++++----------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/pkg/ton/codec/debug/explorer/explorer.go b/pkg/ton/codec/debug/explorer/explorer.go index 71d7f17e0..704e5c1b6 100644 --- a/pkg/ton/codec/debug/explorer/explorer.go +++ b/pkg/ton/codec/debug/explorer/explorer.go @@ -157,36 +157,9 @@ func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr st } } - var senderAddr *address.Address - var err error - if srcAddrStr == "" { - if !c.supportsToncenter() { - return fmt.Errorf("source address is required for network %s when toncenter metadata is unavailable", c.net) - } - c.lggr.Debug("source address not provided, attempting to fetch from toncenter by hash...") - senderAddr, err = c.GetSenderAddressFromTxHash(ctx, effectiveTxHash) - if err != nil { - return fmt.Errorf("failed to get sender address from tx hash: %w", err) - } - c.lggr.Debug("source address found:", senderAddr.String()) - } else if effectiveTxHash != txHashStr { - if c.supportsToncenter() { - senderAddr, err = c.GetSenderAddressFromTxHash(ctx, effectiveTxHash) - if err != nil { - return fmt.Errorf("failed to get root sender address from tx hash: %w", err) - } - c.lggr.Debug("overriding provided source address with trace root account", senderAddr.String()) - } else { - senderAddr, err = address.ParseAddr(srcAddrStr) - if err != nil { - return fmt.Errorf("failed to parse transaction address: %w", err) - } - } - } else { - senderAddr, err = address.ParseAddr(srcAddrStr) - if err != nil { - return fmt.Errorf("failed to parse transaction address: %w", err) - } + senderAddr, err := resolveSenderAddress(ctx, c, srcAddrStr, effectiveTxHash, txHashStr) + if err != nil { + return fmt.Errorf("failed to resolve sender address: %w", err) } decodedTxHash, err := decodeTxHash(effectiveTxHash) if err != nil { @@ -242,6 +215,37 @@ func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr st return nil } +func resolveSenderAddress(ctx context.Context, c *client, srcAddrStr string, effectiveTxHash string, txHashStr string) (*address.Address, error) { + var err error + if srcAddrStr == "" { + if !c.supportsToncenter() { + return nil, fmt.Errorf("source address is required for network %s when toncenter metadata is unavailable", c.net) + } + c.lggr.Debug("source address not provided, attempting to fetch from toncenter by hash...") + senderAddr, err := c.GetSenderAddressFromTxHash(ctx, effectiveTxHash) + if err != nil { + return nil, fmt.Errorf("failed to get sender address from tx hash: %w", err) + } + c.lggr.Debug("source address found:", senderAddr.String()) + return senderAddr, nil + } + if effectiveTxHash != txHashStr && c.supportsToncenter() { + senderAddr, err := c.GetSenderAddressFromTxHash(ctx, effectiveTxHash) + if err != nil { + return nil, fmt.Errorf("failed to get root sender address from tx hash: %w", err) + } + c.lggr.Debug("overriding provided source address with trace root account", senderAddr.String()) + return senderAddr, nil + } + + senderAddr, err := address.ParseAddr(srcAddrStr) + if err != nil { + return nil, fmt.Errorf("failed to parse transaction address: %w", err) + } + + return senderAddr, nil +} + func (c *client) queryActors(ctx context.Context, api ton.APIClientWrapped, message *tracetracking.ReceivedMessage, knownActors map[string]debug.TypeAndVersion) error { visited := make(map[string]bool) block, err := api.CurrentMasterchainInfo(ctx) From 67b1b647c04fa349a12eb9cf858348f546dd00b1 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Thu, 26 Feb 2026 16:02:37 -0300 Subject: [PATCH 06/15] fix: lint --- pkg/ton/codec/debug/explorer/explorer.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/ton/codec/debug/explorer/explorer.go b/pkg/ton/codec/debug/explorer/explorer.go index 704e5c1b6..d9cc3f101 100644 --- a/pkg/ton/codec/debug/explorer/explorer.go +++ b/pkg/ton/codec/debug/explorer/explorer.go @@ -216,7 +216,6 @@ func (c *client) PrintTrace(ctx context.Context, txHashStr string, srcAddrStr st } func resolveSenderAddress(ctx context.Context, c *client, srcAddrStr string, effectiveTxHash string, txHashStr string) (*address.Address, error) { - var err error if srcAddrStr == "" { if !c.supportsToncenter() { return nil, fmt.Errorf("source address is required for network %s when toncenter metadata is unavailable", c.net) From 126b5779e6356841c01f95045f2ff3da6b7b54ed Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Thu, 26 Feb 2026 16:05:40 -0300 Subject: [PATCH 07/15] fix: overflow --- pkg/ton/codec/debug/explorer/tx_lookup.go | 38 +++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/pkg/ton/codec/debug/explorer/tx_lookup.go b/pkg/ton/codec/debug/explorer/tx_lookup.go index 9e3ddcb92..ade2dcb03 100644 --- a/pkg/ton/codec/debug/explorer/tx_lookup.go +++ b/pkg/ton/codec/debug/explorer/tx_lookup.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "math/big" "net/http" "net/url" "strconv" @@ -66,6 +67,39 @@ func decodeTxHash(txHash string) ([]byte, error) { return nil, fmt.Errorf("unsupported tx hash format: %s", txHash) } +func parseShardID(shardHex string) (int64, error) { + if after, ok := strings.CutPrefix(shardHex, "0x"); ok { + shardHex = after + } + + parsed := new(big.Int) + if _, ok := parsed.SetString(shardHex, 16); !ok { + return 0, fmt.Errorf("invalid shard id: %s", shardHex) + } + + if parsed.Sign() < 0 { + if !parsed.IsInt64() { + return 0, fmt.Errorf("shard id out of int64 range: %s", shardHex) + } + return parsed.Int64(), nil + } + + if parsed.BitLen() > 64 { + return 0, fmt.Errorf("shard id out of 64-bit range: %s", shardHex) + } + + if parsed.Bit(63) == 1 { + twoTo64 := new(big.Int).Lsh(big.NewInt(1), 64) + parsed.Sub(parsed, twoTo64) + } + + if !parsed.IsInt64() { + return 0, fmt.Errorf("shard id out of int64 range: %s", shardHex) + } + + return parsed.Int64(), nil +} + func (c *client) getTraceRootTxHash(ctx context.Context, txHashStr string) (string, error) { u, err := c.tonCenterTraceURL() if err != nil { @@ -173,12 +207,12 @@ func (c *client) findTxByToncenterMetadata(ctx context.Context, api ton.APIClien return nil, fmt.Errorf("failed to parse lt from toncenter response: %w", err) } - shard, err := strconv.ParseUint(res.BlockRef.Shard, 16, 64) + shard, err := parseShardID(res.BlockRef.Shard) if err != nil { return nil, fmt.Errorf("failed to parse shard from toncenter response: %w", err) } - block, err := api.LookupBlock(ctx, res.BlockRef.Workchain, int64(shard), res.BlockRef.SeqNo) + block, err := api.LookupBlock(ctx, res.BlockRef.Workchain, shard, res.BlockRef.SeqNo) if err != nil { return nil, fmt.Errorf("failed to lookup block from toncenter metadata: %w", err) } From ec90cb85210cf0d4f973f841152da77723bb632e Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Wed, 4 Mar 2026 15:42:42 -0500 Subject: [PATCH 08/15] feat: move functionality under trace command --- .../.misc/dev-guides/explorer/architecture.md | 8 +++--- docs/.misc/dev-guides/explorer/usage.md | 16 ++++++------ pkg/ton/codec/debug/explorer/explorer.go | 25 ++++++++++++------- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/docs/.misc/dev-guides/explorer/architecture.md b/docs/.misc/dev-guides/explorer/architecture.md index 7684a61eb..09c4c65fd 100644 --- a/docs/.misc/dev-guides/explorer/architecture.md +++ b/docs/.misc/dev-guides/explorer/architecture.md @@ -59,10 +59,10 @@ For maintainability, keep future changes aligned with existing seams: ## Compatibility contract -Current CLI contract intentionally remains: +Current trace CLI contract is: -- `explorer ` -- `explorer
` -- `explorer ` (works when address can be resolved via toncenter) +- `explorer trace ` +- `explorer trace
` +- `explorer trace ` (works when address can be resolved via toncenter) `--address` and `--tx` flags were removed because they were unused and misleading. diff --git a/docs/.misc/dev-guides/explorer/usage.md b/docs/.misc/dev-guides/explorer/usage.md index 6beb45822..086b23265 100644 --- a/docs/.misc/dev-guides/explorer/usage.md +++ b/docs/.misc/dev-guides/explorer/usage.md @@ -8,9 +8,9 @@ Read [TON Explorer Architecture](./architecture.md) for internal module layout a Three ways to run: -1. **URL**: `./explorer ` -2. **Hash + Address**: `./explorer
` -3. **Hash only**: `./explorer ` (testnet/mainnet only unless sender address is provided separately) +1. **URL**: `./explorer trace ` +2. **Hash + Address**: `./explorer trace
` +3. **Hash only**: `./explorer trace ` (testnet/mainnet only unless sender address is provided separately) ## Run with Nix @@ -19,7 +19,7 @@ The `explorer` binary is packaged with `chainlink-ton-extras` pkg bundle. We can start a dev shell including specific pkg contents and execute a bash cmd: ```bash -nix shell .#chainlink-ton-extras -c explorer https://testnet.tonscan.org/tx/ +nix shell .#chainlink-ton-extras -c explorer trace https://testnet.tonscan.org/tx/ ``` ## Build @@ -33,14 +33,14 @@ go build ```bash # URL (recommended) -./explorer https://testnet.tonscan.org/tx/ -./explorer http://localhost:8080/transaction?account=&hash= +./explorer trace https://testnet.tonscan.org/tx/ +./explorer trace http://localhost:8080/transaction?account=&hash= # Hash + address -./explorer
[--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] +./explorer trace
[--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] # Hash only (auto-resolves address via toncenter on testnet/mainnet) -./explorer [--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] +./explorer trace [--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] ``` ## Networks diff --git a/pkg/ton/codec/debug/explorer/explorer.go b/pkg/ton/codec/debug/explorer/explorer.go index d9cc3f101..c9a11560d 100644 --- a/pkg/ton/codec/debug/explorer/explorer.go +++ b/pkg/ton/codec/debug/explorer/explorer.go @@ -30,19 +30,24 @@ func GenerateExplorerCmd(lggr *logger.Logger, contracts map[string]debug.TypeAnd ) cmd := &cobra.Command{ - Use: "explorer
| ", + Use: "explorer", Short: "TON blockchain explorer and trace analyzer", Long: `A command-line tool for exploring TON blockchain transactions and analyzing traces. This tool helps debug and understand transaction flows on the TON network. Usage: - explorer
- Analyze transaction with address and hash - explorer - Analyze transaction from URL + explorer trace [address] - Analyze transaction with address and hash + explorer trace [address] - Analyze transaction from URL Arguments: address Destination address in base64 tx-hash Transaction hash in hex url tonscan TX URL`, + } + + traceCmd := &cobra.Command{ + Use: "trace [address] | [address]", + Short: "Analyze a TON transaction trace", Args: func(cmd *cobra.Command, args []string) error { if len(args) != 1 && len(args) != 2 { return errors.New("requires 1 argument (URL) or 2 arguments (
)") @@ -91,14 +96,16 @@ Arguments: }, } - cmd.Flags().StringVarP(&visualization, "visualization", "V", "sequence", "Visualization format (sequence or tree)") - cmd.Flags().StringVarP(&format, "format", "f", "", "Sequence visualization format (url or raw) (only for sequence visualization)") - cmd.Flags().StringVarP(&net, "net", "n", "testnet", "TON network (mainnet, testnet, mylocalton, or http://domain/x.global.config.json)") + traceCmd.Flags().StringVarP(&visualization, "visualization", "V", "sequence", "Visualization format (sequence or tree)") + traceCmd.Flags().StringVarP(&format, "format", "f", "", "Sequence visualization format (url or raw) (only for sequence visualization)") + traceCmd.Flags().StringVarP(&net, "net", "n", "testnet", "TON network (mainnet, testnet, mylocalton, or http://domain/x.global.config.json)") if lggr == nil { - cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Shows full body of unmatched messages") + traceCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Shows full body of unmatched messages") } - cmd.Flags().Uint32VarP(&pageSize, "page-size", "s", 10, "Number of blocks to fetch per page") - cmd.Flags().Uint32VarP(&maxPages, "max-pages", "p", 10, "Maximum number of pages to fetch") + traceCmd.Flags().Uint32VarP(&pageSize, "page-size", "s", 10, "Number of blocks to fetch per page") + traceCmd.Flags().Uint32VarP(&maxPages, "max-pages", "p", 10, "Maximum number of pages to fetch") + + cmd.AddCommand(traceCmd) return cmd } From 77b3d393241d1bcdfa234e95042cc627064d6056 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Wed, 4 Mar 2026 17:04:19 -0500 Subject: [PATCH 09/15] feat: getter --- pkg/ton/codec/debug/explorer/explorer.go | 5 +- pkg/ton/codec/debug/explorer/get.go | 347 +++++++++++++++++++++++ 2 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 pkg/ton/codec/debug/explorer/get.go diff --git a/pkg/ton/codec/debug/explorer/explorer.go b/pkg/ton/codec/debug/explorer/explorer.go index c9a11560d..496590948 100644 --- a/pkg/ton/codec/debug/explorer/explorer.go +++ b/pkg/ton/codec/debug/explorer/explorer.go @@ -38,11 +38,13 @@ This tool helps debug and understand transaction flows on the TON network. Usage: explorer trace [address] - Analyze transaction with address and hash explorer trace [address] - Analyze transaction from URL + explorer get
- Execute a no-args getter on a contract Arguments: address Destination address in base64 tx-hash Transaction hash in hex - url tonscan TX URL`, + url tonscan TX URL + getter_name Getter name registered in bindings.GetterMap`, } traceCmd := &cobra.Command{ @@ -106,6 +108,7 @@ Arguments: traceCmd.Flags().Uint32VarP(&maxPages, "max-pages", "p", 10, "Maximum number of pages to fetch") cmd.AddCommand(traceCmd) + cmd.AddCommand(newGetCmd(lggr, contracts, client)) return cmd } diff --git a/pkg/ton/codec/debug/explorer/get.go b/pkg/ton/codec/debug/explorer/get.go new file mode 100644 index 000000000..c69d5aebb --- /dev/null +++ b/pkg/ton/codec/debug/explorer/get.go @@ -0,0 +1,347 @@ +package explorer + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "reflect" + "sort" + "strings" + + "github.com/spf13/cobra" + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/ton" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings" + "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/common" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" +) + +type noArgsGetterInvocation struct { + name string + decode func(*ton.ExecutionResult) (any, error) +} + +func newGetCmd(lggr *logger.Logger, contracts map[string]debug.TypeAndVersion, apiClient *ton.APIClient) *cobra.Command { + var ( + net string + verbose bool + contractType string + ) + + cmd := &cobra.Command{ + Use: "get
", + Short: "Execute a no-args getter", + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + if len(args) > 1 && args[1] != "" { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + targetAddr, err := address.ParseAddr(args[0]) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + log, err := buildCmdLogger(lggr, false) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + if apiClient != nil && cmd.Flags().Changed("net") { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + ctx := context.Background() + explorerClient, err := Connect(log, apiClient, net, false, 1, 1) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + effectiveContractType, err := resolveContractType(ctx, explorerClient, targetAddr, contractType, contracts) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + candidates := listNoArgsGetterNames(effectiveContractType) + if toComplete == "" { + return candidates, cobra.ShellCompDirectiveNoFileComp + } + + filtered := make([]string, 0, len(candidates)) + for _, candidate := range candidates { + if strings.HasPrefix(candidate, toComplete) { + filtered = append(filtered, candidate) + } + } + + return filtered, cobra.ShellCompDirectiveNoFileComp + }, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 || len(args) > 2 { + return errors.New("requires
and ") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 2 { + return errors.New("requires exactly 2 arguments (
)") + } + + log, err := buildCmdLogger(lggr, verbose) + if err != nil { + return err + } + + if apiClient != nil && cmd.Flags().Changed("net") { + return errors.New("cannot specify network flag when using existing client") + } + + targetAddr, err := address.ParseAddr(args[0]) + if err != nil { + return fmt.Errorf("failed to parse contract address: %w", err) + } + + ctx := context.Background() + explorerClient, err := Connect(log, apiClient, net, verbose, 1, 1) + if err != nil { + return fmt.Errorf("failed to initialize explorer: %w", err) + } + + effectiveContractType, err := resolveContractType(ctx, explorerClient, targetAddr, contractType, contracts) + if err != nil { + return err + } + + getterName := args[1] + invocation, err := resolveNoArgsGetter(effectiveContractType, getterName) + if err != nil { + return err + } + + api := explorerClient.resilientAPI() + block, err := api.CurrentMasterchainInfo(ctx) + if err != nil { + return fmt.Errorf("failed to get current block: %w", err) + } + + result, err := api.RunGetMethod(ctx, block, targetAddr, invocation.name) + if err != nil { + return fmt.Errorf("failed to run getter %q: %w", invocation.name, err) + } + decoded, err := invocation.decode(result) + if err != nil { + return fmt.Errorf("failed to decode getter %q result: %w", invocation.name, err) + } + + fmt.Fprintln(cmd.OutOrStdout(), formatGetterResult(decoded)) + return nil + }, + } + + cmd.Flags().StringVarP(&net, "net", "n", "testnet", "TON network (mainnet, testnet, mylocalton, or http://domain/x.global.config.json)") + cmd.Flags().StringVarP(&contractType, "contract-type", "t", "", "Contract type (e.g. link.chain.ton.ccip.Router)") + if lggr == nil { + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable debug logs") + } + + return cmd +} + +func buildCmdLogger(lggr *logger.Logger, verbose bool) (logger.Logger, error) { + if lggr != nil { + return *lggr, nil + } + + config := logger.Config{} + if verbose { + config.Level = zapcore.DebugLevel + } + log, err := config.New() + if err != nil { + return nil, fmt.Errorf("failed to create logger: %w", err) + } + return log, nil +} + +func resolveContractType(ctx context.Context, c *client, targetAddr *address.Address, contractTypeFlag string, contracts map[string]debug.TypeAndVersion) (string, error) { + if contractTypeFlag != "" { + return contractTypeFlag, nil + } + + if tv, ok := contracts[targetAddr.String()]; ok && tv.Type != "" { + return tv.Type, nil + } + + api := c.resilientAPI() + block, err := api.CurrentMasterchainInfo(ctx) + if err != nil { + return "", fmt.Errorf("failed to get current block for contract type resolution: %w", err) + } + + result, err := api.RunGetMethod(ctx, block, targetAddr, common.GetTypeAndVersion.Name) + if err != nil { + return "", errors.New("contract type is required; pass --contract-type (automatic resolution via typeAndVersion failed)") + } + decoded, err := common.GetTypeAndVersion.Decoder.Decode(result) + if err != nil { + return "", errors.New("contract type is required; pass --contract-type (failed to decode typeAndVersion)") + } + if decoded.Type == "" { + return "", errors.New("contract type is required; pass --contract-type") + } + + return decoded.Type, nil +} + +func resolveNoArgsGetter(contractType string, getterName string) (noArgsGetterInvocation, error) { + available := listNoArgsGetterNames(contractType) + if len(available) == 0 { + return noArgsGetterInvocation{}, fmt.Errorf("no no-args getters registered for contract type %q", contractType) + } + + for _, name := range available { + if name != getterName { + continue + } + getter, found := resolveRegisteredGetter(contractType, getterName) + if !found { + break + } + return newNoArgsGetterInvocation(getter) + } + + return noArgsGetterInvocation{}, fmt.Errorf("getter %q is not registered for %q (available: %s)", getterName, contractType, strings.Join(available, ", ")) +} + +func listNoArgsGetterNames(contractType string) []string { + availableSet := make(map[string]struct{}) + collectNoArgsGetterNames(availableSet, bindings.TypeToGetterMap[contractType]) + collectNoArgsGetterNames(availableSet, bindings.TypeToGetterMap["link.chain.ton.ccip.Common"]) + collectNoArgsGetterNames(availableSet, bindings.TypeToGetterMap[bindings.TypeOwnable]) + + available := make([]string, 0, len(availableSet)) + for name := range availableSet { + available = append(available, name) + } + sort.Strings(available) + return available +} + +func collectNoArgsGetterNames(dest map[string]struct{}, getterMap bindings.GetterMap) { + for name, getter := range getterMap { + if _, err := newNoArgsGetterInvocation(getter); err == nil { + dest[name] = struct{}{} + } + } +} + +func resolveRegisteredGetter(contractType string, getterName string) (any, bool) { + mapsToSearch := []bindings.GetterMap{ + bindings.TypeToGetterMap[contractType], + bindings.TypeToGetterMap["link.chain.ton.ccip.Common"], + bindings.TypeToGetterMap[bindings.TypeOwnable], + } + for _, getterMap := range mapsToSearch { + getter, ok := getterMap[getterName] + if ok { + return getter, true + } + } + return nil, false +} + +func newNoArgsGetterInvocation(getter any) (noArgsGetterInvocation, error) { + value := reflect.ValueOf(getter) + if value.Kind() != reflect.Struct { + return noArgsGetterInvocation{}, fmt.Errorf("registered getter has unsupported shape %T", getter) + } + + nameField := value.FieldByName("Name") + if !nameField.IsValid() || nameField.Kind() != reflect.String || nameField.String() == "" { + return noArgsGetterInvocation{}, fmt.Errorf("registered getter has invalid Name field %T", getter) + } + + encoderField := value.FieldByName("Encoder") + if !encoderField.IsValid() { + return noArgsGetterInvocation{}, errors.New("getter has no encoder information") + } + if encoderField.IsNil() { + if !isNoArgsEncoderFieldType(encoderField.Type()) { + return noArgsGetterInvocation{}, errors.New("getter requires input arguments; only no-args getters are currently supported") + } + } else { + encodeMethod := encoderField.MethodByName("Encode") + if !encodeMethod.IsValid() { + return noArgsGetterInvocation{}, errors.New("getter encoder has no Encode method") + } + if encodeMethod.Type().NumIn() != 1 { + return noArgsGetterInvocation{}, errors.New("getter encoder has unexpected Encode signature") + } + + // Use method signature to ensure this getter accepts tvm.NoArgs only. + if !isNoArgsInputType(encodeMethod.Type().In(0)) { + return noArgsGetterInvocation{}, errors.New("getter requires input arguments; only no-args getters are currently supported") + } + } + + decoderField := value.FieldByName("Decoder") + if !decoderField.IsValid() || decoderField.IsNil() { + return noArgsGetterInvocation{}, errors.New("getter has no decoder") + } + + decodeMethod := decoderField.MethodByName("Decode") + if !decodeMethod.IsValid() { + return noArgsGetterInvocation{}, errors.New("getter decoder has no Decode method") + } + + decodeFn := func(result *ton.ExecutionResult) (any, error) { + outs := decodeMethod.Call([]reflect.Value{reflect.ValueOf(result)}) + if len(outs) != 2 { + return nil, errors.New("getter decoder returned unexpected values") + } + if !outs[1].IsNil() { + err, ok := outs[1].Interface().(error) + if !ok { + return nil, errors.New("getter decoder returned non-error second value") + } + return nil, err + } + return outs[0].Interface(), nil + } + + return noArgsGetterInvocation{name: nameField.String(), decode: decodeFn}, nil +} + +func formatGetterResult(decoded any) string { + formatted, err := json.MarshalIndent(decoded, "", " ") + if err == nil { + return string(formatted) + } + return fmt.Sprintf("%+v", decoded) +} + +func isNoArgsInputType(t reflect.Type) bool { + if t == reflect.TypeOf(tvm.NoArgs{}) { + return true + } + + // Some bindings model no-args getters as Getter[struct{}, R]. + return t.Kind() == reflect.Struct && t.NumField() == 0 +} + +func isNoArgsEncoderFieldType(t reflect.Type) bool { + if t == nil { + return false + } + + typeStr := t.String() + return strings.Contains(typeStr, "[tvm.NoArgs]") || strings.Contains(typeStr, "[struct {}]") +} From 52d058256ee2b5aef2c55ca1e5be0fb538ec38f0 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Wed, 4 Mar 2026 17:08:45 -0500 Subject: [PATCH 10/15] doc: usage --- docs/.misc/dev-guides/explorer/usage.md | 68 ++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/docs/.misc/dev-guides/explorer/usage.md b/docs/.misc/dev-guides/explorer/usage.md index 086b23265..a6ebcc1d6 100644 --- a/docs/.misc/dev-guides/explorer/usage.md +++ b/docs/.misc/dev-guides/explorer/usage.md @@ -6,11 +6,12 @@ Read [TON Explorer Architecture](./architecture.md) for internal module layout a ## Usage -Three ways to run: +Four ways to run: 1. **URL**: `./explorer trace ` 2. **Hash + Address**: `./explorer trace
` 3. **Hash only**: `./explorer trace ` (testnet/mainnet only unless sender address is provided separately) +4. **Getter call**: `./explorer get
` ## Run with Nix @@ -41,8 +42,72 @@ go build # Hash only (auto-resolves address via toncenter on testnet/mainnet) ./explorer trace [--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] + +# Getter call (no-args getters only for now) +./explorer get
[--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] [--contract-type ] + +# Example +./explorer get EQA-CUZI_USus4w0_Erf-wTj5uhaAR7XldEimU0w0WAJGGod dynamicConfig +``` + +## Getter command + +`explorer get` currently supports no-args getters and prints decoded JSON output. + +The command tries to infer the contract type by calling `typeAndVersion` on the target address. +If inference is unavailable/fails, pass `--contract-type` explicitly. + +Examples: + +```bash +# auto-detect contract type +./explorer get
owner + +# explicit contract type +./explorer get
owner --contract-type link.chain.ton.ccip.OnRamp +``` + +## Autocomplete setup + +The explorer uses Cobra shell completion, including dynamic getter completion for: + +`explorer get
` + +### zsh (current shell only) + +```bash +source <(./explorer completion zsh) ``` +### zsh (persistent) + +```bash +mkdir -p ~/.zfunc +./explorer completion zsh > ~/.zfunc/_explorer +``` + +Add this to `~/.zshrc`: + +```bash +fpath=(~/.zfunc $fpath) +autoload -Uz compinit +compinit +``` + +Reload: + +```bash +source ~/.zshrc +``` + +If you run the local binary directly from this repository, add an alias in `~/.zshrc`: + +```bash +alias explorer="/Users/patricio.passarino/Code/ton/chainlink-ton-explorer/explorer" +``` + +Then you can tab-complete getter names for a contract address. + ## Networks Choose the network with `-n`/`--net` flag: @@ -72,6 +137,7 @@ Display message trace as a tree structure with `--visualization tree`. ```bash --verbose # Show debugging information --page-size 10 --max-pages 10 # Control transaction search pagination +--contract-type # (get command) optional contract type override ``` Note: `--address` and `--tx` flags are not supported; use positional arguments. From de7a408c7c155bfdadeb9867fbb0e79b72629386 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Wed, 4 Mar 2026 17:16:07 -0500 Subject: [PATCH 11/15] feat: interactive getter --- docs/.misc/dev-guides/explorer/usage.md | 6 ++- pkg/ton/codec/debug/explorer/get.go | 61 +++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/docs/.misc/dev-guides/explorer/usage.md b/docs/.misc/dev-guides/explorer/usage.md index a6ebcc1d6..58ab728f1 100644 --- a/docs/.misc/dev-guides/explorer/usage.md +++ b/docs/.misc/dev-guides/explorer/usage.md @@ -11,7 +11,7 @@ Four ways to run: 1. **URL**: `./explorer trace ` 2. **Hash + Address**: `./explorer trace
` 3. **Hash only**: `./explorer trace ` (testnet/mainnet only unless sender address is provided separately) -4. **Getter call**: `./explorer get
` +4. **Getter call**: `./explorer get
[getter_name]` ## Run with Nix @@ -44,7 +44,7 @@ go build ./explorer trace [--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] # Getter call (no-args getters only for now) -./explorer get
[--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] [--contract-type ] +./explorer get
[getter_name] [--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] [--contract-type ] # Example ./explorer get EQA-CUZI_USus4w0_Erf-wTj5uhaAR7XldEimU0w0WAJGGod dynamicConfig @@ -54,6 +54,8 @@ go build `explorer get` currently supports no-args getters and prints decoded JSON output. +When `getter_name` is omitted in an interactive terminal, explorer opens a numbered selector prompt (`0` to cancel). + The command tries to infer the contract type by calling `typeAndVersion` on the target address. If inference is unavailable/fails, pass `--contract-type` explicitly. diff --git a/pkg/ton/codec/debug/explorer/get.go b/pkg/ton/codec/debug/explorer/get.go index c69d5aebb..d7dc9accb 100644 --- a/pkg/ton/codec/debug/explorer/get.go +++ b/pkg/ton/codec/debug/explorer/get.go @@ -1,12 +1,15 @@ package explorer import ( + "bufio" "context" "encoding/json" "errors" "fmt" + "os" "reflect" "sort" + "strconv" "strings" "github.com/spf13/cobra" @@ -16,6 +19,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" + "golang.org/x/term" + "github.com/smartcontractkit/chainlink-ton/pkg/bindings" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/common" "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug" @@ -91,8 +96,8 @@ func newGetCmd(lggr *logger.Logger, contracts map[string]debug.TypeAndVersion, a return nil }, RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 2 { - return errors.New("requires exactly 2 arguments (
)") + if len(args) < 1 || len(args) > 2 { + return errors.New("requires
and optional ") } log, err := buildCmdLogger(lggr, verbose) @@ -120,7 +125,22 @@ func newGetCmd(lggr *logger.Logger, contracts map[string]debug.TypeAndVersion, a return err } - getterName := args[1] + availableGetters := listNoArgsGetterNames(effectiveContractType) + if len(availableGetters) == 0 { + return fmt.Errorf("no no-args getters registered for %q", effectiveContractType) + } + + getterName := "" + if len(args) == 2 { + getterName = args[1] + } else { + selected, selectErr := selectGetterInteractive(cmd, targetAddr.String(), effectiveContractType, availableGetters) + if selectErr != nil { + return selectErr + } + getterName = selected + } + invocation, err := resolveNoArgsGetter(effectiveContractType, getterName) if err != nil { return err @@ -345,3 +365,38 @@ func isNoArgsEncoderFieldType(t reflect.Type) bool { typeStr := t.String() return strings.Contains(typeStr, "[tvm.NoArgs]") || strings.Contains(typeStr, "[struct {}]") } + +func selectGetterInteractive(cmd *cobra.Command, addr string, contractType string, options []string) (string, error) { + fd := int(os.Stdin.Fd()) + if !term.IsTerminal(fd) { + return "", errors.New("getter_name is required in non-interactive mode") + } + + out := cmd.OutOrStdout() + reader := bufio.NewReader(os.Stdin) + fmt.Fprintf(out, "Select getter for %s (%s):\n", addr, contractType) + for i, opt := range options { + fmt.Fprintf(out, " %d) %s\n", i+1, opt) + } + fmt.Fprintln(out, " 0) cancel") + + for { + fmt.Fprintf(out, "Enter number [0-%d]: ", len(options)) + line, err := reader.ReadString('\n') + if err != nil { + return "", fmt.Errorf("interactive selector failed: %w", err) + } + + choice := strings.TrimSpace(line) + index, convErr := strconv.Atoi(choice) + if convErr != nil || index < 0 || index > len(options) { + fmt.Fprintln(out, "Invalid selection, try again.") + continue + } + if index == 0 { + return "", errors.New("selection canceled") + } + + return options[index-1], nil + } +} From ebcee0e819ba75c5c4b13d55e3e88627ba8b6b83 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Thu, 5 Mar 2026 17:18:18 -0500 Subject: [PATCH 12/15] fix: getter with params --- docs/.misc/dev-guides/explorer/usage.md | 16 +- pkg/bindings/getters.go | 2 + pkg/ccip/bindings/router/reader.go | 12 + pkg/ton/codec/debug/explorer/explorer.go | 5 +- pkg/ton/codec/debug/explorer/get.go | 172 ++------- pkg/ton/codec/debug/explorer/get_args.go | 469 +++++++++++++++++++++++ 6 files changed, 536 insertions(+), 140 deletions(-) create mode 100644 pkg/ton/codec/debug/explorer/get_args.go diff --git a/docs/.misc/dev-guides/explorer/usage.md b/docs/.misc/dev-guides/explorer/usage.md index 58ab728f1..d211dca3c 100644 --- a/docs/.misc/dev-guides/explorer/usage.md +++ b/docs/.misc/dev-guides/explorer/usage.md @@ -11,7 +11,7 @@ Four ways to run: 1. **URL**: `./explorer trace ` 2. **Hash + Address**: `./explorer trace
` 3. **Hash only**: `./explorer trace ` (testnet/mainnet only unless sender address is provided separately) -4. **Getter call**: `./explorer get
[getter_name]` +4. **Getter call**: `./explorer get
[getter_name] [args...]` ## Run with Nix @@ -43,8 +43,8 @@ go build # Hash only (auto-resolves address via toncenter on testnet/mainnet) ./explorer trace [--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] -# Getter call (no-args getters only for now) -./explorer get
[getter_name] [--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] [--contract-type ] +# Getter call +./explorer get
[getter_name] [args...] [--arg name=value] [--net testnet|mainnet|mylocalton|http://custom-domain/global.config.json] [--contract-type ] # Example ./explorer get EQA-CUZI_USus4w0_Erf-wTj5uhaAR7XldEimU0w0WAJGGod dynamicConfig @@ -52,9 +52,10 @@ go build ## Getter command -`explorer get` currently supports no-args getters and prints decoded JSON output. +`explorer get` supports no-args and argument-based getters and prints decoded JSON output. When `getter_name` is omitted in an interactive terminal, explorer opens a numbered selector prompt (`0` to cancel). +When a selected getter requires arguments and values are missing, explorer prompts for those values. The command tries to infer the contract type by calling `typeAndVersion` on the target address. If inference is unavailable/fails, pass `--contract-type` explicitly. @@ -67,6 +68,12 @@ Examples: # explicit contract type ./explorer get
owner --contract-type link.chain.ton.ccip.OnRamp + +# positional args +./explorer get onRamp 16015286601757825753 + +# named args +./explorer get getRoleMember --arg role=1 --arg index=0 ``` ## Autocomplete setup @@ -140,6 +147,7 @@ Display message trace as a tree structure with `--visualization tree`. --verbose # Show debugging information --page-size 10 --max-pages 10 # Control transaction search pagination --contract-type # (get command) optional contract type override +--arg name=value # (get command) named getter argument (repeatable) ``` Note: `--address` and `--tx` flags are not supported; use positional arguments. diff --git a/pkg/bindings/getters.go b/pkg/bindings/getters.go index 33b1de9ed..4d739e1a3 100644 --- a/pkg/bindings/getters.go +++ b/pkg/bindings/getters.go @@ -28,6 +28,8 @@ var TypeToGetterMap = map[string]GetterMap{ "rmn_pendingOwner": router.GetRMNPendingOwner, "onRamp": router.GetOnRamp, "destChainSelectors": router.GetDestChainSelectors, + "verifyNotCursed": router.GetVerifyNotCursed, + "cursedSubjects": router.GetCursedSubjects, }, TypeOnRamp: { "owner": onramp.GetOwner, diff --git a/pkg/ccip/bindings/router/reader.go b/pkg/ccip/bindings/router/reader.go index ee61f363a..1d0744ab1 100644 --- a/pkg/ccip/bindings/router/reader.go +++ b/pkg/ccip/bindings/router/reader.go @@ -59,3 +59,15 @@ var GetVerifyNotCursed = tvm.Getter[*big.Int, bool]{ return notCursed.Cmp(big.NewInt(0)) != 0, nil }), } + +// GetCursedSubjects gets all cursed subjects. +var GetCursedSubjects = tvm.Getter[struct{}, []*big.Int]{ + Name: "cursedSubjects", + Decoder: tvm.NewResultDecoder(func(r *ton.ExecutionResult) ([]*big.Int, error) { + subjects, err := parser.ParseLispTuple[*big.Int](r.AsTuple()) + if err != nil { + return nil, err + } + return subjects, nil + }), +} diff --git a/pkg/ton/codec/debug/explorer/explorer.go b/pkg/ton/codec/debug/explorer/explorer.go index 496590948..be0941c0c 100644 --- a/pkg/ton/codec/debug/explorer/explorer.go +++ b/pkg/ton/codec/debug/explorer/explorer.go @@ -38,13 +38,14 @@ This tool helps debug and understand transaction flows on the TON network. Usage: explorer trace [address] - Analyze transaction with address and hash explorer trace [address] - Analyze transaction from URL - explorer get
- Execute a no-args getter on a contract + explorer get
[getter_name] [args...] - Execute a getter on a contract Arguments: address Destination address in base64 tx-hash Transaction hash in hex url tonscan TX URL - getter_name Getter name registered in bindings.GetterMap`, + getter_name Getter name registered in bindings.GetterMap + args Optional getter arguments`, } traceCmd := &cobra.Command{ diff --git a/pkg/ton/codec/debug/explorer/get.go b/pkg/ton/codec/debug/explorer/get.go index d7dc9accb..2b52ddbd8 100644 --- a/pkg/ton/codec/debug/explorer/get.go +++ b/pkg/ton/codec/debug/explorer/get.go @@ -8,7 +8,6 @@ import ( "fmt" "os" "reflect" - "sort" "strconv" "strings" @@ -27,21 +26,17 @@ import ( "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" ) -type noArgsGetterInvocation struct { - name string - decode func(*ton.ExecutionResult) (any, error) -} - func newGetCmd(lggr *logger.Logger, contracts map[string]debug.TypeAndVersion, apiClient *ton.APIClient) *cobra.Command { var ( net string verbose bool contractType string + namedArgs []string ) cmd := &cobra.Command{ - Use: "get
", - Short: "Execute a no-args getter", + Use: "get
[getter_name] [args...]", + Short: "Execute a getter", ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { return nil, cobra.ShellCompDirectiveNoFileComp @@ -75,7 +70,7 @@ func newGetCmd(lggr *logger.Logger, contracts map[string]debug.TypeAndVersion, a return nil, cobra.ShellCompDirectiveNoFileComp } - candidates := listNoArgsGetterNames(effectiveContractType) + candidates := listGetterNames(effectiveContractType) if toComplete == "" { return candidates, cobra.ShellCompDirectiveNoFileComp } @@ -90,14 +85,14 @@ func newGetCmd(lggr *logger.Logger, contracts map[string]debug.TypeAndVersion, a return filtered, cobra.ShellCompDirectiveNoFileComp }, Args: func(cmd *cobra.Command, args []string) error { - if len(args) < 1 || len(args) > 2 { - return errors.New("requires
and ") + if len(args) < 1 { + return errors.New("requires
") } return nil }, RunE: func(cmd *cobra.Command, args []string) error { - if len(args) < 1 || len(args) > 2 { - return errors.New("requires
and optional ") + if len(args) < 1 { + return errors.New("requires
") } log, err := buildCmdLogger(lggr, verbose) @@ -125,13 +120,13 @@ func newGetCmd(lggr *logger.Logger, contracts map[string]debug.TypeAndVersion, a return err } - availableGetters := listNoArgsGetterNames(effectiveContractType) + availableGetters := listGetterNames(effectiveContractType) if len(availableGetters) == 0 { - return fmt.Errorf("no no-args getters registered for %q", effectiveContractType) + return fmt.Errorf("no getters registered for %q", effectiveContractType) } getterName := "" - if len(args) == 2 { + if len(args) >= 2 { getterName = args[1] } else { selected, selectErr := selectGetterInteractive(cmd, targetAddr.String(), effectiveContractType, availableGetters) @@ -141,24 +136,45 @@ func newGetCmd(lggr *logger.Logger, contracts map[string]debug.TypeAndVersion, a getterName = selected } - invocation, err := resolveNoArgsGetter(effectiveContractType, getterName) + desc, err := resolveGetterDescriptor(effectiveContractType, getterName) + if err != nil { + return err + } + + named, err := parseNamedArgs(namedArgs) if err != nil { return err } + positional := []string{} + if len(args) > 2 { + positional = args[2:] + } + + interactive := term.IsTerminal(int(os.Stdin.Fd())) + inputValue, err := buildGetterInput(desc, positional, named, interactive, promptForArgValue) + if err != nil { + return err + } + + params, err := desc.Encode(inputValue) + if err != nil { + return fmt.Errorf("failed to encode getter %q args: %w", getterName, err) + } + api := explorerClient.resilientAPI() block, err := api.CurrentMasterchainInfo(ctx) if err != nil { return fmt.Errorf("failed to get current block: %w", err) } - result, err := api.RunGetMethod(ctx, block, targetAddr, invocation.name) + result, err := api.RunGetMethod(ctx, block, targetAddr, desc.MethodName, params...) if err != nil { - return fmt.Errorf("failed to run getter %q: %w", invocation.name, err) + return fmt.Errorf("failed to run getter %q: %w", desc.MethodName, err) } - decoded, err := invocation.decode(result) + decoded, err := desc.Decode(result) if err != nil { - return fmt.Errorf("failed to decode getter %q result: %w", invocation.name, err) + return fmt.Errorf("failed to decode getter %q result: %w", desc.MethodName, err) } fmt.Fprintln(cmd.OutOrStdout(), formatGetterResult(decoded)) @@ -168,6 +184,7 @@ func newGetCmd(lggr *logger.Logger, contracts map[string]debug.TypeAndVersion, a cmd.Flags().StringVarP(&net, "net", "n", "testnet", "TON network (mainnet, testnet, mylocalton, or http://domain/x.global.config.json)") cmd.Flags().StringVarP(&contractType, "contract-type", "t", "", "Contract type (e.g. link.chain.ton.ccip.Router)") + cmd.Flags().StringArrayVar(&namedArgs, "arg", nil, "Getter argument in name=value form (repeatable)") if lggr == nil { cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable debug logs") } @@ -221,48 +238,6 @@ func resolveContractType(ctx context.Context, c *client, targetAddr *address.Add return decoded.Type, nil } -func resolveNoArgsGetter(contractType string, getterName string) (noArgsGetterInvocation, error) { - available := listNoArgsGetterNames(contractType) - if len(available) == 0 { - return noArgsGetterInvocation{}, fmt.Errorf("no no-args getters registered for contract type %q", contractType) - } - - for _, name := range available { - if name != getterName { - continue - } - getter, found := resolveRegisteredGetter(contractType, getterName) - if !found { - break - } - return newNoArgsGetterInvocation(getter) - } - - return noArgsGetterInvocation{}, fmt.Errorf("getter %q is not registered for %q (available: %s)", getterName, contractType, strings.Join(available, ", ")) -} - -func listNoArgsGetterNames(contractType string) []string { - availableSet := make(map[string]struct{}) - collectNoArgsGetterNames(availableSet, bindings.TypeToGetterMap[contractType]) - collectNoArgsGetterNames(availableSet, bindings.TypeToGetterMap["link.chain.ton.ccip.Common"]) - collectNoArgsGetterNames(availableSet, bindings.TypeToGetterMap[bindings.TypeOwnable]) - - available := make([]string, 0, len(availableSet)) - for name := range availableSet { - available = append(available, name) - } - sort.Strings(available) - return available -} - -func collectNoArgsGetterNames(dest map[string]struct{}, getterMap bindings.GetterMap) { - for name, getter := range getterMap { - if _, err := newNoArgsGetterInvocation(getter); err == nil { - dest[name] = struct{}{} - } - } -} - func resolveRegisteredGetter(contractType string, getterName string) (any, bool) { mapsToSearch := []bindings.GetterMap{ bindings.TypeToGetterMap[contractType], @@ -278,68 +253,6 @@ func resolveRegisteredGetter(contractType string, getterName string) (any, bool) return nil, false } -func newNoArgsGetterInvocation(getter any) (noArgsGetterInvocation, error) { - value := reflect.ValueOf(getter) - if value.Kind() != reflect.Struct { - return noArgsGetterInvocation{}, fmt.Errorf("registered getter has unsupported shape %T", getter) - } - - nameField := value.FieldByName("Name") - if !nameField.IsValid() || nameField.Kind() != reflect.String || nameField.String() == "" { - return noArgsGetterInvocation{}, fmt.Errorf("registered getter has invalid Name field %T", getter) - } - - encoderField := value.FieldByName("Encoder") - if !encoderField.IsValid() { - return noArgsGetterInvocation{}, errors.New("getter has no encoder information") - } - if encoderField.IsNil() { - if !isNoArgsEncoderFieldType(encoderField.Type()) { - return noArgsGetterInvocation{}, errors.New("getter requires input arguments; only no-args getters are currently supported") - } - } else { - encodeMethod := encoderField.MethodByName("Encode") - if !encodeMethod.IsValid() { - return noArgsGetterInvocation{}, errors.New("getter encoder has no Encode method") - } - if encodeMethod.Type().NumIn() != 1 { - return noArgsGetterInvocation{}, errors.New("getter encoder has unexpected Encode signature") - } - - // Use method signature to ensure this getter accepts tvm.NoArgs only. - if !isNoArgsInputType(encodeMethod.Type().In(0)) { - return noArgsGetterInvocation{}, errors.New("getter requires input arguments; only no-args getters are currently supported") - } - } - - decoderField := value.FieldByName("Decoder") - if !decoderField.IsValid() || decoderField.IsNil() { - return noArgsGetterInvocation{}, errors.New("getter has no decoder") - } - - decodeMethod := decoderField.MethodByName("Decode") - if !decodeMethod.IsValid() { - return noArgsGetterInvocation{}, errors.New("getter decoder has no Decode method") - } - - decodeFn := func(result *ton.ExecutionResult) (any, error) { - outs := decodeMethod.Call([]reflect.Value{reflect.ValueOf(result)}) - if len(outs) != 2 { - return nil, errors.New("getter decoder returned unexpected values") - } - if !outs[1].IsNil() { - err, ok := outs[1].Interface().(error) - if !ok { - return nil, errors.New("getter decoder returned non-error second value") - } - return nil, err - } - return outs[0].Interface(), nil - } - - return noArgsGetterInvocation{name: nameField.String(), decode: decodeFn}, nil -} - func formatGetterResult(decoded any) string { formatted, err := json.MarshalIndent(decoded, "", " ") if err == nil { @@ -357,15 +270,6 @@ func isNoArgsInputType(t reflect.Type) bool { return t.Kind() == reflect.Struct && t.NumField() == 0 } -func isNoArgsEncoderFieldType(t reflect.Type) bool { - if t == nil { - return false - } - - typeStr := t.String() - return strings.Contains(typeStr, "[tvm.NoArgs]") || strings.Contains(typeStr, "[struct {}]") -} - func selectGetterInteractive(cmd *cobra.Command, addr string, contractType string, options []string) (string, error) { fd := int(os.Stdin.Fd()) if !term.IsTerminal(fd) { diff --git a/pkg/ton/codec/debug/explorer/get_args.go b/pkg/ton/codec/debug/explorer/get_args.go new file mode 100644 index 000000000..c32def650 --- /dev/null +++ b/pkg/ton/codec/debug/explorer/get_args.go @@ -0,0 +1,469 @@ +package explorer + +import ( + "bufio" + "errors" + "fmt" + "math/big" + "os" + "reflect" + "sort" + "strconv" + "strings" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/ton" + "golang.org/x/term" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" +) + +type getterArgSpec struct { + Name string + Type reflect.Type + FieldIndex int +} + +type getterDescriptor struct { + MethodName string + InputType reflect.Type + ArgSpecs []getterArgSpec + Decode func(*ton.ExecutionResult) (any, error) + Encode func(any) ([]any, error) +} + +func listGetterNames(contractType string) []string { + availableSet := make(map[string]struct{}) + collectGetterNames(availableSet, bindings.TypeToGetterMap[contractType]) + collectGetterNames(availableSet, bindings.TypeToGetterMap["link.chain.ton.ccip.Common"]) + collectGetterNames(availableSet, bindings.TypeToGetterMap[bindings.TypeOwnable]) + + available := make([]string, 0, len(availableSet)) + for name := range availableSet { + available = append(available, name) + } + sort.Strings(available) + return available +} + +func collectGetterNames(dest map[string]struct{}, getterMap bindings.GetterMap) { + for name := range getterMap { + dest[name] = struct{}{} + } +} + +func resolveGetterDescriptor(contractType string, getterName string) (getterDescriptor, error) { + getter, found := resolveRegisteredGetter(contractType, getterName) + if !found { + available := listGetterNames(contractType) + if len(available) == 0 { + return getterDescriptor{}, fmt.Errorf("no getters registered for contract type %q", contractType) + } + return getterDescriptor{}, fmt.Errorf("getter %q is not registered for %q (available: %s)", getterName, contractType, strings.Join(available, ", ")) + } + + desc, err := describeGetter(getter) + if err != nil { + return getterDescriptor{}, fmt.Errorf("unsupported getter %q: %w", getterName, err) + } + return desc, nil +} + +func describeGetter(getter any) (getterDescriptor, error) { + value := reflect.ValueOf(getter) + if value.Kind() != reflect.Struct { + return getterDescriptor{}, fmt.Errorf("registered getter has unsupported shape %T", getter) + } + + nameField := value.FieldByName("Name") + if !nameField.IsValid() || nameField.Kind() != reflect.String || nameField.String() == "" { + return getterDescriptor{}, fmt.Errorf("registered getter has invalid Name field %T", getter) + } + + decoderField := value.FieldByName("Decoder") + if !decoderField.IsValid() || decoderField.IsNil() { + return getterDescriptor{}, errors.New("getter has no decoder") + } + decodeMethod := decoderField.MethodByName("Decode") + if !decodeMethod.IsValid() { + return getterDescriptor{}, errors.New("getter decoder has no Decode method") + } + + encoderField := value.FieldByName("Encoder") + if !encoderField.IsValid() { + return getterDescriptor{}, errors.New("getter has no encoder information") + } + + inputType, err := inferGetterInputType(encoderField) + if err != nil { + return getterDescriptor{}, err + } + + encodeFn, err := buildEncodeFunction(encoderField) + if err != nil { + return getterDescriptor{}, err + } + + argSpecs := buildArgSpecs(inputType) + + decodeFn := func(result *ton.ExecutionResult) (any, error) { + outs := decodeMethod.Call([]reflect.Value{reflect.ValueOf(result)}) + if len(outs) != 2 { + return nil, errors.New("getter decoder returned unexpected values") + } + if !outs[1].IsNil() { + err, ok := outs[1].Interface().(error) + if !ok { + return nil, errors.New("getter decoder returned non-error second value") + } + return nil, err + } + return outs[0].Interface(), nil + } + + return getterDescriptor{ + MethodName: nameField.String(), + InputType: inputType, + ArgSpecs: argSpecs, + Decode: decodeFn, + Encode: encodeFn, + }, nil +} + +func inferGetterInputType(encoderField reflect.Value) (reflect.Type, error) { + encodeMethodType, err := findEncodeMethodType(encoderField) + if err != nil { + return nil, err + } + if encodeMethodType.NumIn() == 1 { + return encodeMethodType.In(0), nil + } + if encodeMethodType.NumIn() == 2 { + // Method signatures discovered from interface types include receiver as arg 0. + return encodeMethodType.In(1), nil + } + if encodeMethodType.NumIn() == 0 { + return nil, errors.New("getter encoder has unexpected Encode signature") + } + if encodeMethodType.NumIn() > 2 { + return nil, errors.New("getter encoder has unexpected Encode signature") + } + return nil, errors.New("getter encoder has unexpected Encode signature") +} + +func findEncodeMethodType(encoderField reflect.Value) (reflect.Type, error) { + if !encoderField.IsNil() { + encodeMethod := encoderField.MethodByName("Encode") + if !encodeMethod.IsValid() { + return nil, errors.New("getter encoder has no Encode method") + } + return encodeMethod.Type(), nil + } + + t := encoderField.Type() + m, ok := t.MethodByName("Encode") + if !ok { + return nil, errors.New("getter encoder type has no Encode method") + } + return m.Type, nil +} + +func buildEncodeFunction(encoderField reflect.Value) (func(any) ([]any, error), error) { + if !encoderField.IsNil() { + encodeMethod := encoderField.MethodByName("Encode") + if !encodeMethod.IsValid() { + return nil, errors.New("getter encoder has no Encode method") + } + + return func(input any) ([]any, error) { + outs := encodeMethod.Call([]reflect.Value{reflect.ValueOf(input)}) + if len(outs) != 2 { + return nil, errors.New("getter encoder returned unexpected values") + } + if !outs[1].IsNil() { + err, ok := outs[1].Interface().(error) + if !ok { + return nil, errors.New("getter encoder returned non-error second value") + } + return nil, err + } + params, ok := outs[0].Interface().([]any) + if !ok { + return nil, errors.New("getter encoder returned non-[]any params") + } + return params, nil + }, nil + } + + return func(input any) ([]any, error) { + return encodeArgsDefault(input) + }, nil +} + +func buildArgSpecs(inputType reflect.Type) []getterArgSpec { + if isNoArgsInputType(inputType) { + return nil + } + + if inputType.Kind() != reflect.Struct { + return []getterArgSpec{{Name: "arg1", Type: inputType, FieldIndex: -1}} + } + + out := make([]getterArgSpec, 0, inputType.NumField()) + for i := 0; i < inputType.NumField(); i++ { + field := inputType.Field(i) + if !field.IsExported() || field.Tag.Get("tvm") == "-" { + continue + } + out = append(out, getterArgSpec{Name: strings.ToLower(field.Name[:1]) + field.Name[1:], Type: field.Type, FieldIndex: i}) + } + return out +} + +func parseNamedArgs(argPairs []string) (map[string]string, error) { + result := make(map[string]string, len(argPairs)) + for _, pair := range argPairs { + name, value, ok := strings.Cut(pair, "=") + if !ok { + return nil, fmt.Errorf("invalid --arg %q, expected name=value", pair) + } + name = strings.TrimSpace(name) + if name == "" { + return nil, fmt.Errorf("invalid --arg %q, argument name is empty", pair) + } + if _, exists := result[name]; exists { + return nil, fmt.Errorf("duplicate named arg %q", name) + } + result[name] = value + } + return result, nil +} + +func buildGetterInput(desc getterDescriptor, positional []string, named map[string]string, interactive bool, cmdOut func(string) (string, error)) (any, error) { + if len(desc.ArgSpecs) == 0 { + return tvm.NoArgs{}, nil + } + + if len(desc.ArgSpecs) == 1 && desc.ArgSpecs[0].FieldIndex == -1 { + if len(positional) > 1 { + return nil, fmt.Errorf("too many positional args: expected 1, got %d", len(positional)) + } + if _, hasNamed := named[desc.ArgSpecs[0].Name]; hasNamed && len(positional) > 0 { + return nil, fmt.Errorf("arg %q provided as both positional and named", desc.ArgSpecs[0].Name) + } + + raw, err := resolveRawArgValue(desc.ArgSpecs[0], positional, named, 0, interactive, cmdOut) + if err != nil { + return nil, err + } + v, err := parseValueForType(raw, desc.ArgSpecs[0].Type) + if err != nil { + return nil, fmt.Errorf("arg 1 (%s): %w", desc.ArgSpecs[0].Name, err) + } + for name := range named { + if name != desc.ArgSpecs[0].Name { + return nil, fmt.Errorf("unknown named arg %q", name) + } + } + return v.Interface(), nil + } + + input := reflect.New(desc.InputType).Elem() + usedNamed := make(map[string]bool) + posIndex := 0 + for i, spec := range desc.ArgSpecs { + raw, err := resolveRawArgValue(spec, positional, named, posIndex, interactive, cmdOut) + if err != nil { + return nil, err + } + if _, ok := named[spec.Name]; ok { + usedNamed[spec.Name] = true + } else if posIndex < len(positional) { + posIndex++ + } + + v, err := parseValueForType(raw, spec.Type) + if err != nil { + return nil, fmt.Errorf("arg %d (%s): %w", i+1, spec.Name, err) + } + input.Field(spec.FieldIndex).Set(v) + } + + if posIndex < len(positional) { + return nil, fmt.Errorf("too many positional args: expected %d, got %d", len(desc.ArgSpecs), len(positional)) + } + + for name := range named { + if !usedNamed[name] { + return nil, fmt.Errorf("unknown named arg %q", name) + } + } + + return input.Interface(), nil +} + +func resolveRawArgValue(spec getterArgSpec, positional []string, named map[string]string, posIndex int, interactive bool, promptFn func(string) (string, error)) (string, error) { + if value, ok := named[spec.Name]; ok { + return value, nil + } + if posIndex < len(positional) { + return positional[posIndex], nil + } + if interactive { + return promptFn(fmt.Sprintf("Enter value for %s (%s) [or 0 to cancel]: ", spec.Name, spec.Type.String())) + } + return "", fmt.Errorf("missing required arg %q (%s)", spec.Name, spec.Type.String()) +} + +func promptForArgValue(prompt string) (string, error) { + if !term.IsTerminal(int(os.Stdin.Fd())) { + return "", errors.New("missing args in non-interactive mode") + } + fmt.Fprint(os.Stdout, prompt) + reader := bufio.NewReader(os.Stdin) + line, err := reader.ReadString('\n') + if err != nil { + return "", err + } + value := strings.TrimSpace(line) + if value == "0" { + return "", errors.New("selection canceled") + } + return value, nil +} + +func parseValueForType(raw string, t reflect.Type) (reflect.Value, error) { + if t == reflect.TypeOf(tvm.NoArgs{}) { + return reflect.ValueOf(tvm.NoArgs{}), nil + } + + if t.Kind() == reflect.Pointer { + if raw == "" { + return reflect.Zero(t), nil + } + v, err := parseValueForType(raw, t.Elem()) + if err != nil { + return reflect.Value{}, err + } + ptr := reflect.New(t.Elem()) + ptr.Elem().Set(v) + return ptr, nil + } + + if t == reflect.TypeOf(address.Address{}) { + addr, err := address.ParseAddr(raw) + if err != nil { + return reflect.Value{}, fmt.Errorf("expected address, got %q", raw) + } + return reflect.ValueOf(*addr), nil + } + + if t == reflect.TypeOf(big.Int{}) { + v, ok := new(big.Int).SetString(raw, 10) + if !ok { + return reflect.Value{}, fmt.Errorf("expected decimal big.Int, got %q", raw) + } + return reflect.ValueOf(*v), nil + } + + switch t.Kind() { + case reflect.String: + return reflect.ValueOf(raw).Convert(t), nil + case reflect.Bool: + b, err := strconv.ParseBool(raw) + if err != nil { + return reflect.Value{}, fmt.Errorf("expected bool, got %q", raw) + } + return reflect.ValueOf(b).Convert(t), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n, err := strconv.ParseInt(raw, 10, t.Bits()) + if err != nil { + return reflect.Value{}, fmt.Errorf("expected %s, got %q", t.String(), raw) + } + return reflect.ValueOf(n).Convert(t), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + n, err := strconv.ParseUint(raw, 10, t.Bits()) + if err != nil { + return reflect.Value{}, fmt.Errorf("expected %s, got %q", t.String(), raw) + } + return reflect.ValueOf(n).Convert(t), nil + case reflect.Slice: + if t.Elem().Kind() == reflect.Uint8 { + hexInput := strings.TrimPrefix(raw, "0x") + decoded, decErr := decodeHexOrBytes(hexInput) + if decErr != nil { + return reflect.Value{}, fmt.Errorf("expected hex bytes, got %q", raw) + } + return reflect.ValueOf(decoded).Convert(t), nil + } + } + + return reflect.Value{}, fmt.Errorf("unsupported arg type %s", t.String()) +} + +func decodeHexOrBytes(raw string) ([]byte, error) { + if len(raw)%2 == 1 { + raw = "0" + raw + } + decoded := make([]byte, len(raw)/2) + for i := 0; i < len(decoded); i++ { + v, err := strconv.ParseUint(raw[2*i:2*i+2], 16, 8) + if err != nil { + return nil, err + } + decoded[i] = byte(v) + } + return decoded, nil +} + +// Mirrors tvm.encodeArgsDefault behavior for nil-encoder getters. +func encodeArgsDefault(input any) ([]any, error) { + if input == nil { + return []any{nil}, nil + } + + value := reflect.ValueOf(input) + if !value.IsValid() { + return nil, errors.New("cannot encode invalid argument value") + } + + switch value.Kind() { + case reflect.Interface: + if value.IsNil() { + return []any{nil}, nil + } + return encodeArgsDefault(value.Elem().Interface()) + case reflect.Pointer: + if value.IsNil() { + return []any{nil}, nil + } + return []any{value.Interface()}, nil + case reflect.Struct: + if value.Type() == reflect.TypeOf(tvm.NoArgs{}) { + return []any{}, nil + } + t := value.Type() + params := make([]any, 0, t.NumField()) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if !field.IsExported() || field.Tag.Get("tvm") == "-" { + continue + } + params = append(params, value.Field(i).Interface()) + } + return params, nil + case reflect.Slice, reflect.Array: + if value.Type().Elem().Kind() == reflect.Uint8 { + return []any{value.Interface()}, nil + } + length := value.Len() + params := make([]any, length) + for i := 0; i < length; i++ { + params[i] = value.Index(i).Interface() + } + return params, nil + default: + return []any{value.Interface()}, nil + } +} From 837b6d5b36c18dabc0911c4746030c63bd994546 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Mon, 9 Mar 2026 15:26:39 -0400 Subject: [PATCH 13/15] feat: decoders for merkle_root, receiveexecutor, receiver feat feat: event decoders for offramp, onramp --- deployment/ccip/sequence/deploy_ccip.go | 2 +- pkg/bindings/index.go | 33 ++++--- pkg/ccip/bindings/merkleroot/merkle_root.go | 23 +++++ pkg/ccip/bindings/offramp/offramp.go | 90 ++++++++++++++++++- pkg/ccip/bindings/offramp/offramp_test.go | 25 ++++++ pkg/ccip/bindings/onramp/onramp.go | 5 ++ pkg/ccip/bindings/onramp/onramp_test.go | 5 ++ .../receiveexecutor/receiveexecutor.go | 59 ++++++++++++ pkg/ccip/bindings/receiver/receiver.go | 33 +++++-- pkg/ccip/bindings/receiver/receiver_test.go | 13 +++ .../ccip/ccipsendexecutor/ccipsendexecutor.go | 2 +- .../decoders/ccip/feequoter/feequoter.go | 2 +- .../decoders/ccip/merkler_root/decoder.go | 47 ++++++++++ .../debug/decoders/ccip/offramp/decoder.go | 61 ++++++++++++- .../debug/decoders/ccip/onramp/onramp.go | 28 +++++- .../ccip/receiveexecutor/receiveexecutor.go | 42 +++++++++ .../debug/decoders/ccip/receiver/decoder.go | 49 ++++++++++ .../debug/decoders/ccip/router/router.go | 2 +- pkg/ton/codec/debug/decoders/jetton/common.go | 2 +- .../decoders/jetton/minter/jetton_minter.go | 6 +- .../decoders/jetton/wallet/jetton_wallet.go | 6 +- .../debug/decoders/lib/access/rbac/decoder.go | 2 +- .../codec/debug/decoders/mcms/mcms/decoder.go | 2 +- .../debug/decoders/mcms/timelock/decoder.go | 2 +- pkg/ton/codec/debug/pretty_print.go | 24 +++++ 25 files changed, 532 insertions(+), 33 deletions(-) create mode 100644 pkg/ccip/bindings/receiveexecutor/receiveexecutor.go create mode 100644 pkg/ccip/bindings/receiver/receiver_test.go create mode 100644 pkg/ton/codec/debug/decoders/ccip/merkler_root/decoder.go create mode 100644 pkg/ton/codec/debug/decoders/ccip/receiveexecutor/receiveexecutor.go create mode 100644 pkg/ton/codec/debug/decoders/ccip/receiver/decoder.go diff --git a/deployment/ccip/sequence/deploy_ccip.go b/deployment/ccip/sequence/deploy_ccip.go index fd87c48e8..164b9da31 100644 --- a/deployment/ccip/sequence/deploy_ccip.go +++ b/deployment/ccip/sequence/deploy_ccip.go @@ -235,7 +235,7 @@ func deployCCIPSequence(b operations.Bundle, dp *dep.DependencyProvider, in Depl PendingOwner: address.NewAddressNone(), }, AuthorizedCaller: &routerAddress, - Behavior: receiver.Accept, + Behavior: receiver.BehaviorAccept, } outputAddr, err = utils.InvokeDeployContractOperation(b, dp, in.ChainSelector, tonCompiledContracts[state.TonReceiver], receiverStorage, nil, in.CCIPConfig.ReceiverParams.Coin, in.CCIPConfig.ReceiverParams.ContractsSemver) diff --git a/pkg/bindings/index.go b/pkg/bindings/index.go index 1eaca63e0..6ffce30e8 100644 --- a/pkg/bindings/index.go +++ b/pkg/bindings/index.go @@ -9,7 +9,10 @@ import ( "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/timelock" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/ccipsendexecutor" + "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/merkleroot" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/ownable2step" + "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/receiveexecutor" + "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/receiver" "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/feequoter" @@ -40,11 +43,16 @@ const ( TypeTimelock = PkgMCMS + ".Timelock" // CCIP - TypeRouter = PkgCCIP + ".Router" - TypeOnRamp = PkgCCIP + ".OnRamp" - TypeOffRamp = PkgCCIP + ".OffRamp" - TypeFeeQuoter = PkgCCIP + ".FeeQuoter" - TypeSendExecutor = PkgCCIP + ".CCIPSendExecutor" + TypeRouter = PkgCCIP + ".Router" + TypeOnRamp = PkgCCIP + ".OnRamp" + TypeOffRamp = PkgCCIP + ".OffRamp" + TypeFeeQuoter = PkgCCIP + ".FeeQuoter" + TypeSendExecutor = PkgCCIP + ".CCIPSendExecutor" + TypeReceiveExecutor = PkgCCIP + ".ReceiveExecutor" + TypeMerkleRoot = PkgCCIP + ".MerkleRoot" + + // Test CCIP + TypeReceiver = PkgCCIP + ".Receiver" // Jetton TypeJettonWallet = PkgJetton + ".contracts.jetton-wallet" @@ -64,11 +72,16 @@ var Registry = tvm.ContractTLBRegistry{ TypeTimelock: timelock.TLBs, // CCIP contract types - TypeRouter: router.TLBs, - TypeOnRamp: onramp.TLBs, - TypeOffRamp: offramp.TLBs, - TypeFeeQuoter: feequoter.TLBs, - TypeSendExecutor: ccipsendexecutor.TLBs, + TypeRouter: router.TLBs, + TypeOnRamp: onramp.TLBs, + TypeOffRamp: offramp.TLBs, + TypeFeeQuoter: feequoter.TLBs, + TypeSendExecutor: ccipsendexecutor.TLBs, + TypeReceiveExecutor: receiveexecutor.TLBs, + TypeMerkleRoot: merkleroot.TLBs, + + // Test contract types + TypeReceiver: receiver.TLBs, // Jetton contract types TypeJettonWallet: wallet.TLBs, diff --git a/pkg/ccip/bindings/merkleroot/merkle_root.go b/pkg/ccip/bindings/merkleroot/merkle_root.go index 1cb3ddcce..297e2f172 100644 --- a/pkg/ccip/bindings/merkleroot/merkle_root.go +++ b/pkg/ccip/bindings/merkleroot/merkle_root.go @@ -4,10 +4,28 @@ import ( "math/big" "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/ocr" "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" ) +// Validate represents the MerkleRoot_Validate message. +type Validate struct { + _ tlb.Magic `tlb:"#038ede91" json:"-"` //nolint:revive // Ignore opcode tag + Message ocr.Any2TVMRampMessage `tlb:"^"` + PermissionlessExecutionThresholdSeconds uint32 `tlb:"## 32"` + MetadataHash *big.Int `tlb:"## 256"` + GasOverride *tlb.Coins `tlb:"maybe ."` +} + +// MarkState represents the MerkleRoot_MarkState message. +type MarkState struct { + _ tlb.Magic `tlb:"#019f4cd2" json:"-"` //nolint:revive // Ignore opcode tag + SeqNum uint64 `tlb:"## 64"` + State uint8 `tlb:"## 8"` +} + type Storage struct { Root *big.Int `tlb:"## 256"` Owner *address.Address `tlb:"addr"` @@ -18,6 +36,11 @@ type Storage struct { DeliveredMessageCount uint16 `tlb:"## 16"` } +var TLBs = tvm.MustNewTLBMap([]any{ + Validate{}, + MarkState{}, +}).MustWithStorageType(Storage{}) + //go:generate go run golang.org/x/tools/cmd/stringer@v0.38.0 -type=ExitCode type ExitCode tvm.ExitCode diff --git a/pkg/ccip/bindings/offramp/offramp.go b/pkg/ccip/bindings/offramp/offramp.go index 08aa1d375..4e12f43c3 100644 --- a/pkg/ccip/bindings/offramp/offramp.go +++ b/pkg/ccip/bindings/offramp/offramp.go @@ -15,6 +15,18 @@ import ( "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" ) +// Topics +const ( + TopicExecutionStateChanged = 0x4C94C360 // CRC32("ExecutionStateChanged") + TopicCommitReportAccepted = 0x27D3BCE8 // CRC32("CommitReportAccepted") + TopicSourceChainSelectorAdded = 0x989AA53E // CRC32("SourceChainSelectorAdded") + TopicSourceChainConfigUpdated = 0x71E9FD30 // CRC32("SourceChainConfigUpdated") + TopicDynamicConfigSet = 0xAD76A933 // CRC32("DynamicConfigSet") + TopicReceiveExecutorInitExecuteBounced = 0x8DC48A3C // CRC32("ReceiveExecutorInitExecuteBounced") + TopicDeployableInitializeBounced = 0x408AA96F // CRC32("DeployableInitializeBounced") + TopicRouteMessageBounced = 0x9C288FEA // CRC32("RouteMessageBounced") +) + // OCR3Config represents the OCR3 configuration stored on-chain type OCR3Config struct { ConfigInfo ConfigInfo `tlb:"."` @@ -166,6 +178,73 @@ type Execute struct { ExecuteReport ocr.ExecuteReport `tlb:"."` } +// ExecuteValidated represents the executeValidated message. +type ExecuteValidated struct { + _ tlb.Magic `tlb:"#c73d5a8a" json:"-"` //nolint:revive // Ignore opcode tag + Message ocr.Any2TVMRampMessage `tlb:"^"` + Root []byte `tlb:"bits 256"` + MetadataHash []byte `tlb:"bits 256"` + GasOverride *tlb.Coins `tlb:"maybe ."` + ExecutionState uint8 `tlb:"## 8"` +} + +// ManuallyExecute represents the manuallyExecute message. +type ManuallyExecute struct { + _ tlb.Magic `tlb:"#a00785cf" json:"-"` //nolint:revive // Ignore opcode tag + QueryID uint64 `tlb:"## 64"` + Report ocr.ExecuteReport `tlb:"."` + GasOverride tlb.Coins `tlb:"."` +} + +// DispatchValidated represents the dispatchValidated message. +type DispatchValidated struct { + _ tlb.Magic `tlb:"#58cfcb02" json:"-"` //nolint:revive // Ignore opcode tag + Message ocr.Any2TVMRampMessage `tlb:"^"` + ExecID []byte `tlb:"bits 192"` + GasOverride *tlb.Coins `tlb:"maybe ."` +} + +// CCIPReceiveConfirm represents the ccipReceiveConfirm message. +type CCIPReceiveConfirm struct { + _ tlb.Magic `tlb:"#28f4166f" json:"-"` //nolint:revive // Ignore opcode tag + ExecID []byte `tlb:"bits 192"` + Receiver *address.Address `tlb:"addr"` +} + +// CCIPReceiveBounced represents the ccipReceiveBounced message. +type CCIPReceiveBounced struct { + _ tlb.Magic `tlb:"#2dcf2a43" json:"-"` //nolint:revive // Ignore opcode tag + ExecID []byte `tlb:"bits 192"` + Receiver *address.Address `tlb:"addr"` +} + +// NotifySuccess represents the notifySuccess message. +type NotifySuccess struct { + _ tlb.Magic `tlb:"#59e56170" json:"-"` //nolint:revive // Ignore opcode tag + Header ocr.RampMessageHeader `tlb:"."` + ExecID []byte `tlb:"bits 192"` + Root *address.Address `tlb:"addr"` +} + +// NotifyFailure represents the notifyFailure message. +type NotifyFailure struct { + _ tlb.Magic `tlb:"#177ebd03" json:"-"` //nolint:revive // Ignore opcode tag + Header ocr.RampMessageHeader `tlb:"."` + ExecID []byte `tlb:"bits 192"` + Root *address.Address `tlb:"addr"` +} + +// CursedSubjects wraps the cursed subject map used by RMN remote. +type CursedSubjects struct { + Data *cell.Dictionary `tlb:"dict 128"` +} + +// UpdateCursedSubjects represents the updateCursedSubjects message. +type UpdateCursedSubjects struct { + _ tlb.Magic `tlb:"#4ca1bcb3" json:"-"` //nolint:revive // Ignore opcode tag + CursedSubjects CursedSubjects `tlb:"."` +} + type SetDynamicConfig struct { _ tlb.Magic `tlb:"#95bc5a5c" json:"-"` //nolint:revive // Ignore opcode tag QueryID uint64 `tlb:"## 64"` @@ -182,11 +261,20 @@ type UpdateDeployables struct { var TLBs = tvm.MustNewTLBMap([]any{ CCIPReceive{}, - SetOCR3Config{}, UpdateSourceChainConfigs{}, Commit{}, Execute{}, + ExecuteValidated{}, + ManuallyExecute{}, + DispatchValidated{}, + UpdateSourceChainConfigs{}, + CCIPReceiveConfirm{}, + CCIPReceiveBounced{}, + NotifyFailure{}, + NotifySuccess{}, + UpdateCursedSubjects{}, SetDynamicConfig{}, + SetOCR3Config{}, UpdateDeployables{}, }).MustWithStorageType(Storage{}) diff --git a/pkg/ccip/bindings/offramp/offramp_test.go b/pkg/ccip/bindings/offramp/offramp_test.go index 1ae5d0760..5af16d824 100644 --- a/pkg/ccip/bindings/offramp/offramp_test.go +++ b/pkg/ccip/bindings/offramp/offramp_test.go @@ -2,6 +2,7 @@ package offramp import ( "encoding/hex" + "hash/crc32" "math/big" "testing" @@ -15,6 +16,30 @@ import ( "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" ) +func TestTopicCRC32Values(t *testing.T) { + tests := []struct { + name string + topic string + expected uint32 + }{ + {name: "TopicExecutionStateChanged", topic: "ExecutionStateChanged", expected: TopicExecutionStateChanged}, + {name: "TopicCommitReportAccepted", topic: "CommitReportAccepted", expected: TopicCommitReportAccepted}, + {name: "TopicSourceChainSelectorAdded", topic: "SourceChainSelectorAdded", expected: TopicSourceChainSelectorAdded}, + {name: "TopicSourceChainConfigUpdated", topic: "SourceChainConfigUpdated", expected: TopicSourceChainConfigUpdated}, + {name: "TopicDynamicConfigSet", topic: "DynamicConfigSet", expected: TopicDynamicConfigSet}, + {name: "TopicReceiveExecutorInitExecuteBounced", topic: "ReceiveExecutorInitExecuteBounced", expected: TopicReceiveExecutorInitExecuteBounced}, + {name: "TopicDeployableInitializeBounced", topic: "DeployableInitializeBounced", expected: TopicDeployableInitializeBounced}, + {name: "TopicRouteMessageBounced", topic: "RouteMessageBounced", expected: TopicRouteMessageBounced}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + computed := crc32.ChecksumIEEE([]byte(tt.topic)) + require.Equal(t, tt.expected, computed, "CRC32 mismatch for %s: expected 0x%08X, got 0x%08X", tt.topic, tt.expected, computed) + }) + } +} + func TestCommit_EncodingAndDecoding(t *testing.T) { addr, err := address.ParseAddr("EQDtFpEwcFAEcRe5mLVh2N6C0x-_hJEM7W61_JLnSF74p4q2") require.NoError(t, err) diff --git a/pkg/ccip/bindings/onramp/onramp.go b/pkg/ccip/bindings/onramp/onramp.go index 095138204..e150efe10 100644 --- a/pkg/ccip/bindings/onramp/onramp.go +++ b/pkg/ccip/bindings/onramp/onramp.go @@ -33,6 +33,8 @@ const ( TopicDestChainSelectorAdded = 0xD3D104FF // CRC32("DestChainSelectorAdded") TopicDestChainConfigUpdated = 0x3AA25CF1 // CRC32("DestChainConfigUpdated") TopicConfigSet = 0x1E32222C // CRC32("ConfigSet") + // TopicDynamicConfigSet is an alias used by some tooling for the ConfigSet event. + TopicDynamicConfigSet = TopicConfigSet ) // Registry method names @@ -63,6 +65,9 @@ type ConfigSet struct { DynamicConfig DynamicConfig `tlb:"."` } +// DynamicConfigSet is an alias for ConfigSet kept for naming consistency with logs tooling. +type DynamicConfigSet = ConfigSet + // GenericExtraArgsV2 represents generic extra arguments for transactions. type GenericExtraArgsV2 struct { _ tlb.Magic `tlb:"#181dcf10" json:"-"` //nolint:revive // Ignore opcode tag // hex encoded bytes4(keccak256("CCIP EVMExtraArgsV2")), can be verified with hexutil.MustDecode("0x181dcf10") diff --git a/pkg/ccip/bindings/onramp/onramp_test.go b/pkg/ccip/bindings/onramp/onramp_test.go index 1f14d6955..086cab229 100644 --- a/pkg/ccip/bindings/onramp/onramp_test.go +++ b/pkg/ccip/bindings/onramp/onramp_test.go @@ -43,6 +43,11 @@ func TestTopicCRC32Values(t *testing.T) { topic: "ConfigSet", expected: TopicConfigSet, }, + { + name: "TopicDynamicConfigSetAlias", + topic: "ConfigSet", + expected: TopicDynamicConfigSet, + }, } for _, tt := range tests { diff --git a/pkg/ccip/bindings/receiveexecutor/receiveexecutor.go b/pkg/ccip/bindings/receiveexecutor/receiveexecutor.go new file mode 100644 index 000000000..1c21585ab --- /dev/null +++ b/pkg/ccip/bindings/receiveexecutor/receiveexecutor.go @@ -0,0 +1,59 @@ +package receiveexecutor + +import ( + "math/big" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + + "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/ocr" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" +) + +// InitExecute represents the ReceiveExecutor_InitExecute message. +type InitExecute struct { + _ tlb.Magic `tlb:"#64cd2fd2" json:"-"` //nolint:revive // Ignore opcode tag + GasOverride *tlb.Coins `tlb:"maybe ."` + Root *address.Address `tlb:"addr"` + SequenceNumber uint64 `tlb:"## 64"` + SourceChainSelector uint64 `tlb:"## 64"` + MessageID *big.Int `tlb:"## 256"` +} + +// Confirm represents the ReceiveExecutor_Confirm message. +type Confirm struct { + _ tlb.Magic `tlb:"#00e5dd97" json:"-"` //nolint:revive // Ignore opcode tag + Receiver *address.Address `tlb:"addr"` +} + +// Bounced represents the ReceiveExecutor_Bounced message. +type Bounced struct { + _ tlb.Magic `tlb:"#05dee1bb" json:"-"` //nolint:revive // Ignore opcode tag + Receiver *address.Address `tlb:"addr"` +} + +// MessageState is the ReceiveExecutor message state machine state. +type MessageState uint8 + +const ( + MessageStateUntouched MessageState = iota + MessageStateExecute + MessageStateExecuteFailed + MessageStateSuccess +) + +// Storage represents ReceiveExecutor storage state. +type Storage struct { + Owner *address.Address `tlb:"addr"` + Message ocr.Any2TVMRampMessage `tlb:"^"` + Root *address.Address `tlb:"addr"` + ExecID *big.Int `tlb:"## 192"` + State MessageState `tlb:"## 8"` + LastExecutionTimestamp uint64 `tlb:"## 64"` +} + +var TLBs = tvm.MustNewTLBMap([]any{ + InitExecute{}, + Bounced{}, + Confirm{}, +}).MustWithStorageType(Storage{}) diff --git a/pkg/ccip/bindings/receiver/receiver.go b/pkg/ccip/bindings/receiver/receiver.go index 4df97b391..1e9e771dd 100644 --- a/pkg/ccip/bindings/receiver/receiver.go +++ b/pkg/ccip/bindings/receiver/receiver.go @@ -2,17 +2,11 @@ package receiver import ( "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/offramp" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/ownable2step" -) - -type Behavior uint8 - -const ( - Accept Behavior = iota - RejectAll - ConsumeAllGas + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" ) // Storage represents the storage structure for the CCIP receiver contract. @@ -23,6 +17,29 @@ type Storage struct { Behavior Behavior `tlb:"## 8"` } +type Behavior uint8 + +const ( + BehaviorAccept Behavior = iota + BehaviorRejectAll + BehaviorConsumeAllGas +) + +type UpdateBehavior struct { + _ tlb.Magic `tlb:"#cf87a147" json:"-"` //nolint:revive // (opcode) should stay uninitialized + Behavior Behavior `tlb:"## 8"` +} + +type UpdateAuthorizedCaller struct { + _ tlb.Magic `tlb:"#9f5e489f" json:"-"` //nolint:revive // (opcode) should stay uninitialized + AuthorizedCaller *address.Address `tlb:"addr"` +} + +var TLBs = tvm.MustNewTLBMap([]any{ + UpdateBehavior{}, + UpdateAuthorizedCaller{}, +}).MustWithStorageType(Storage{}) + // CCIPMessageReceivedEventTopic is the event topic for Receiver_CCIPMessageReceived event // crc32('Receiver_CCIPMessageReceived') = 0xc5a40ab3 const CCIPMessageReceivedEventTopic = 0xc5a40ab3 diff --git a/pkg/ccip/bindings/receiver/receiver_test.go b/pkg/ccip/bindings/receiver/receiver_test.go new file mode 100644 index 000000000..4f1d06d08 --- /dev/null +++ b/pkg/ccip/bindings/receiver/receiver_test.go @@ -0,0 +1,13 @@ +package receiver + +import ( + "hash/crc32" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCCIPMessageReceivedEventTopicCRC32(t *testing.T) { + computed := crc32.ChecksumIEEE([]byte("Receiver_CCIPMessageReceived")) + require.Equal(t, uint32(CCIPMessageReceivedEventTopic), computed) +} diff --git a/pkg/ton/codec/debug/decoders/ccip/ccipsendexecutor/ccipsendexecutor.go b/pkg/ton/codec/debug/decoders/ccip/ccipsendexecutor/ccipsendexecutor.go index 1c17908b4..a7b55b64b 100644 --- a/pkg/ton/codec/debug/decoders/ccip/ccipsendexecutor/ccipsendexecutor.go +++ b/pkg/ton/codec/debug/decoders/ccip/ccipsendexecutor/ccipsendexecutor.go @@ -27,7 +27,7 @@ func (d *decoder) ContractType() string { } func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { - return nil, codec.ErrUnknownMessage + return lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) } func (d *decoder) ExternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { diff --git a/pkg/ton/codec/debug/decoders/ccip/feequoter/feequoter.go b/pkg/ton/codec/debug/decoders/ccip/feequoter/feequoter.go index 454b861f5..2e2f5ea3e 100644 --- a/pkg/ton/codec/debug/decoders/ccip/feequoter/feequoter.go +++ b/pkg/ton/codec/debug/decoders/ccip/feequoter/feequoter.go @@ -27,7 +27,7 @@ func (d *decoder) ContractType() string { } func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { - return nil, codec.ErrUnknownMessage + return lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) } func (d *decoder) ExternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { diff --git a/pkg/ton/codec/debug/decoders/ccip/merkler_root/decoder.go b/pkg/ton/codec/debug/decoders/ccip/merkler_root/decoder.go new file mode 100644 index 000000000..ed041732e --- /dev/null +++ b/pkg/ton/codec/debug/decoders/ccip/merkler_root/decoder.go @@ -0,0 +1,47 @@ +package merkle_root + +import ( + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings" + "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/merkleroot" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/lib" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" +) + +var TLBs = merkleroot.TLBs + +type decoder struct { + tlbsCtx tvm.TLBMap +} + +func NewDecoder(tlbsCtx tvm.TLBMap) lib.ContractDecoder { + return &decoder{tlbsCtx} +} + +func (d *decoder) ContractType() string { + return bindings.TypeMerkleRoot +} + +func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { + return nil, codec.ErrUnknownMessage +} + +func (d *decoder) ExternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { + return nil, codec.ErrUnknownMessage +} + +func (d *decoder) InternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { + return lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) +} + +func (d *decoder) ExitCodeInfo(exitCode tvm.ExitCode) (string, error) { + ec, err := merkleroot.ExitCodeCodec.NewFrom(exitCode) + if err != nil { + return "", codec.ErrUnknownMessage + } + + return ec.String(), nil +} diff --git a/pkg/ton/codec/debug/decoders/ccip/offramp/decoder.go b/pkg/ton/codec/debug/decoders/ccip/offramp/decoder.go index ea4429e99..f44589a6a 100644 --- a/pkg/ton/codec/debug/decoders/ccip/offramp/decoder.go +++ b/pkg/ton/codec/debug/decoders/ccip/offramp/decoder.go @@ -2,12 +2,14 @@ package offramp import ( "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/tvm/cell" "github.com/smartcontractkit/chainlink-ton/pkg/bindings" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/offramp" "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec" "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/lib" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/event" "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" ) @@ -26,7 +28,64 @@ func (d *decoder) ContractType() string { } func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { - return nil, codec.ErrUnknownMessage + bucket := event.NewExtOutLogBucket(dstAddr) + topic, err := bucket.DecodeEventTopic() + if err != nil { + return nil, codec.ErrUnknownMessage + } + + switch topic { + case offramp.TopicExecutionStateChanged: + var eventData offramp.ExecutionStateChanged + if err := tlb.LoadFromCell(&eventData, msg.BeginParse()); err != nil { + return nil, err + } + return lib.NewMessageInfo("ExecutionStateChanged", eventData) + case offramp.TopicCommitReportAccepted: + var eventData offramp.CommitReportAccepted + if err := tlb.LoadFromCell(&eventData, msg.BeginParse()); err != nil { + return nil, err + } + return lib.NewMessageInfo("CommitReportAccepted", eventData) + case offramp.TopicSourceChainSelectorAdded: + var eventData offramp.SourceChainSelectorAdded + if err := tlb.LoadFromCell(&eventData, msg.BeginParse()); err != nil { + return nil, err + } + return lib.NewMessageInfo("SourceChainSelectorAdded", eventData) + case offramp.TopicSourceChainConfigUpdated: + var eventData offramp.SourceChainConfigUpdated + if err := tlb.LoadFromCell(&eventData, msg.BeginParse()); err != nil { + return nil, err + } + return lib.NewMessageInfo("SourceChainConfigUpdated", eventData) + case offramp.TopicDynamicConfigSet: + var eventData offramp.DynamicConfigSet + if err := tlb.LoadFromCell(&eventData, msg.BeginParse()); err != nil { + return nil, err + } + return lib.NewMessageInfo("DynamicConfigSet", eventData) + case offramp.TopicReceiveExecutorInitExecuteBounced: + var eventData offramp.ReceiveExecutorInitExecuteBounced + if err := tlb.LoadFromCell(&eventData, msg.BeginParse()); err != nil { + return nil, err + } + return lib.NewMessageInfo("ReceiveExecutorInitExecuteBounced", eventData) + case offramp.TopicDeployableInitializeBounced: + var eventData offramp.DeployableInitializeBounced + if err := tlb.LoadFromCell(&eventData, msg.BeginParse()); err != nil { + return nil, err + } + return lib.NewMessageInfo("DeployableInitializeBounced", eventData) + case offramp.TopicRouteMessageBounced: + var eventData offramp.RouteMessageBounced + if err := tlb.LoadFromCell(&eventData, msg.BeginParse()); err != nil { + return nil, err + } + return lib.NewMessageInfo("RouteMessageBounced", eventData) + default: + return nil, codec.ErrUnknownMessage + } } func (d *decoder) ExternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { diff --git a/pkg/ton/codec/debug/decoders/ccip/onramp/onramp.go b/pkg/ton/codec/debug/decoders/ccip/onramp/onramp.go index 6ba256ff8..03c44298c 100644 --- a/pkg/ton/codec/debug/decoders/ccip/onramp/onramp.go +++ b/pkg/ton/codec/debug/decoders/ccip/onramp/onramp.go @@ -35,16 +35,38 @@ func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.Messa if err != nil { return nil, codec.ErrUnknownMessage } - if topic == onramp.TopicCCIPMessageSent { + switch topic { + case onramp.TopicCCIPMessageSent: var ccipMessageSent onramp.CCIPMessageSent err := tlb.LoadFromCell(&ccipMessageSent, msg.BeginParse()) if err != nil { return nil, err } return lib.NewMessageInfo("CCIPMessageSent", ccipMessageSent) + case onramp.TopicDestChainSelectorAdded: + var destChainSelectorAdded onramp.DestChainSelectorAdded + err := tlb.LoadFromCell(&destChainSelectorAdded, msg.BeginParse()) + if err != nil { + return nil, err + } + return lib.NewMessageInfo("DestChainSelectorAdded", destChainSelectorAdded) + case onramp.TopicDestChainConfigUpdated: + var destChainConfigUpdated onramp.DestChainConfigUpdated + err := tlb.LoadFromCell(&destChainConfigUpdated, msg.BeginParse()) + if err != nil { + return nil, err + } + return lib.NewMessageInfo("DestChainConfigUpdated", destChainConfigUpdated) + case onramp.TopicConfigSet: + var configSet onramp.ConfigSet + err := tlb.LoadFromCell(&configSet, msg.BeginParse()) + if err != nil { + return nil, err + } + return lib.NewMessageInfo("ConfigSet", configSet) + default: + return nil, codec.ErrUnknownMessage } - - return nil, codec.ErrUnknownMessage } func (d *decoder) ExternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { diff --git a/pkg/ton/codec/debug/decoders/ccip/receiveexecutor/receiveexecutor.go b/pkg/ton/codec/debug/decoders/ccip/receiveexecutor/receiveexecutor.go new file mode 100644 index 000000000..a2e79bd7a --- /dev/null +++ b/pkg/ton/codec/debug/decoders/ccip/receiveexecutor/receiveexecutor.go @@ -0,0 +1,42 @@ +package receiveexecutor + +import ( + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings" + receiveexecutorbindings "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/receiveexecutor" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/lib" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" +) + +var TLBs = receiveexecutorbindings.TLBs + +type decoder struct { + tlbsCtx tvm.TLBMap +} + +func NewDecoder(tlbsCtx tvm.TLBMap) lib.ContractDecoder { + return &decoder{tlbsCtx} +} + +func (d *decoder) ContractType() string { + return bindings.TypeReceiveExecutor +} + +func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { + return lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) +} + +func (d *decoder) ExternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { + return nil, codec.ErrUnknownMessage +} + +func (d *decoder) InternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { + return lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) +} + +func (d *decoder) ExitCodeInfo(exitCode tvm.ExitCode) (string, error) { + return "", codec.ErrUnknownMessage +} diff --git a/pkg/ton/codec/debug/decoders/ccip/receiver/decoder.go b/pkg/ton/codec/debug/decoders/ccip/receiver/decoder.go new file mode 100644 index 000000000..bb7c78f36 --- /dev/null +++ b/pkg/ton/codec/debug/decoders/ccip/receiver/decoder.go @@ -0,0 +1,49 @@ +package receiver + +import ( + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings" + "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/receiver" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/lib" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" +) + +var TLBs = receiver.TLBs + +type decoder struct { + tlbsCtx tvm.TLBMap +} + +func NewDecoder(tlbsCtx tvm.TLBMap) lib.ContractDecoder { + return &decoder{tlbsCtx} +} + +func (d *decoder) ContractType() string { + return bindings.TypeReceiver +} + +func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { + return nil, codec.ErrUnknownMessage +} + +func (d *decoder) ExternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { + return lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) +} + +func (d *decoder) InternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { + return lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) +} + +func (d *decoder) ExitCodeInfo(exitCode tvm.ExitCode) (string, error) { + return "", codec.ErrUnknownMessage + // TODO + // ec, err := receiver.ExitCodeCodec.NewFrom(exitCode) + // if err != nil { + // return "", codec.ErrUnknownMessage + // } + + // return ec.String(), nil +} diff --git a/pkg/ton/codec/debug/decoders/ccip/router/router.go b/pkg/ton/codec/debug/decoders/ccip/router/router.go index c12b19819..eea475011 100644 --- a/pkg/ton/codec/debug/decoders/ccip/router/router.go +++ b/pkg/ton/codec/debug/decoders/ccip/router/router.go @@ -27,7 +27,7 @@ func (d *decoder) ContractType() string { } func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { - return nil, codec.ErrUnknownMessage + return lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) } func (d *decoder) ExternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { diff --git a/pkg/ton/codec/debug/decoders/jetton/common.go b/pkg/ton/codec/debug/decoders/jetton/common.go index b4a98222f..ba6a77e23 100644 --- a/pkg/ton/codec/debug/decoders/jetton/common.go +++ b/pkg/ton/codec/debug/decoders/jetton/common.go @@ -29,7 +29,7 @@ func (d *decoder) ContractType() string { // EventInfo implements lib.ContractDecoder. func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { - return nil, codec.ErrUnknownMessage + return lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) } // ExternalMessageInfo implements lib.ContractDecoder. diff --git a/pkg/ton/codec/debug/decoders/jetton/minter/jetton_minter.go b/pkg/ton/codec/debug/decoders/jetton/minter/jetton_minter.go index 92eed930d..3d08c3eae 100644 --- a/pkg/ton/codec/debug/decoders/jetton/minter/jetton_minter.go +++ b/pkg/ton/codec/debug/decoders/jetton/minter/jetton_minter.go @@ -29,7 +29,11 @@ func (d *decoder) ContractType() string { // EventInfo implements lib.ContractDecoder. func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { - return nil, codec.ErrUnknownMessage + info, err := lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) + if err != nil { + return jetton.NewDecoder(d.tlbsCtx, d.ContractType()).InternalMessageInfo(msg) + } + return info, nil } // ExternalMessageInfo implements lib.ContractDecoder. diff --git a/pkg/ton/codec/debug/decoders/jetton/wallet/jetton_wallet.go b/pkg/ton/codec/debug/decoders/jetton/wallet/jetton_wallet.go index 8a8ced5dd..dd4fb0c55 100644 --- a/pkg/ton/codec/debug/decoders/jetton/wallet/jetton_wallet.go +++ b/pkg/ton/codec/debug/decoders/jetton/wallet/jetton_wallet.go @@ -29,7 +29,11 @@ func (d *decoder) ContractType() string { // EventInfo implements lib.ContractDecoder. func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { - return nil, codec.ErrUnknownMessage + info, err := lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) + if err != nil { + return jetton_common.NewDecoder(d.tlbsCtx, d.ContractType()).InternalMessageInfo(msg) + } + return info, nil } // ExternalMessageInfo implements lib.ContractDecoder. diff --git a/pkg/ton/codec/debug/decoders/lib/access/rbac/decoder.go b/pkg/ton/codec/debug/decoders/lib/access/rbac/decoder.go index 45f995b61..2711260f6 100644 --- a/pkg/ton/codec/debug/decoders/lib/access/rbac/decoder.go +++ b/pkg/ton/codec/debug/decoders/lib/access/rbac/decoder.go @@ -27,7 +27,7 @@ func (d *decoder) ContractType() string { } func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { - return nil, codec.ErrUnknownMessage + return lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) } func (d *decoder) ExternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { diff --git a/pkg/ton/codec/debug/decoders/mcms/mcms/decoder.go b/pkg/ton/codec/debug/decoders/mcms/mcms/decoder.go index 777e1d6b9..575d63b50 100644 --- a/pkg/ton/codec/debug/decoders/mcms/mcms/decoder.go +++ b/pkg/ton/codec/debug/decoders/mcms/mcms/decoder.go @@ -27,7 +27,7 @@ func (d *decoder) ContractType() string { } func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { - return nil, codec.ErrUnknownMessage + return lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) } func (d *decoder) ExternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { diff --git a/pkg/ton/codec/debug/decoders/mcms/timelock/decoder.go b/pkg/ton/codec/debug/decoders/mcms/timelock/decoder.go index 702481366..44d319f7d 100644 --- a/pkg/ton/codec/debug/decoders/mcms/timelock/decoder.go +++ b/pkg/ton/codec/debug/decoders/mcms/timelock/decoder.go @@ -27,7 +27,7 @@ func (d *decoder) ContractType() string { } func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { - return nil, codec.ErrUnknownMessage + return lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) } func (d *decoder) ExternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { diff --git a/pkg/ton/codec/debug/pretty_print.go b/pkg/ton/codec/debug/pretty_print.go index e2f864b56..4f0d124f0 100644 --- a/pkg/ton/codec/debug/pretty_print.go +++ b/pkg/ton/codec/debug/pretty_print.go @@ -14,8 +14,11 @@ import ( "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec" "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/decoders/ccip/ccipsendexecutor" "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/decoders/ccip/feequoter" + merkle_root "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/decoders/ccip/merkler_root" "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/decoders/ccip/offramp" "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/decoders/ccip/onramp" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/decoders/ccip/receiveexecutor" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/decoders/ccip/receiver" "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/decoders/ccip/router" "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/decoders/jetton/minter" "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/decoders/jetton/wallet" @@ -77,8 +80,12 @@ func defaultDecoders() map[string]lib.ContractDecoder { // CCIP contract types maps.Copy(tlbs, router.TLBs) maps.Copy(tlbs, onramp.TLBs) + maps.Copy(tlbs, merkle_root.TLBs) maps.Copy(tlbs, feequoter.TLBs) maps.Copy(tlbs, ccipsendexecutor.TLBs) + maps.Copy(tlbs, receiveexecutor.TLBs) + // CCIP Test contract types + maps.Copy(tlbs, receiver.TLBs) // MCMS contract types maps.Copy(tlbs, rbac.TLBs) maps.Copy(tlbs, mcms.TLBs) @@ -89,9 +96,12 @@ func defaultDecoders() map[string]lib.ContractDecoder { registerDecoder(t, minter.NewDecoder(tlbs)) registerDecoder(t, router.NewDecoder(tlbs)) registerDecoder(t, onramp.NewDecoder(tlbs)) + registerDecoder(t, merkle_root.NewDecoder(tlbs)) registerDecoder(t, offramp.NewDecoder(tlbs)) registerDecoder(t, feequoter.NewDecoder(tlbs)) registerDecoder(t, ccipsendexecutor.NewDecoder(tlbs)) + registerDecoder(t, receiveexecutor.NewDecoder(tlbs)) + registerDecoder(t, receiver.NewDecoder(tlbs)) registerDecoder(t, rbac.NewDecoder(tlbs)) registerDecoder(t, mcms.NewDecoder(tlbs)) registerDecoder(t, timelock.NewDecoder(tlbs)) @@ -275,6 +285,20 @@ func (d DebuggerEnvironment) describeSentMessage(m *tt.SentMessage, verbose bool func (d DebuggerEnvironment) describeExternalOutMsg(m tt.OutgoingExternalMessages, verbose bool) (*lib.MessageInfo, error) { var info lib.MessageInfo var err error + if m.SrcAddr != nil { + if actor, ok := d.existingAddresses[m.SrcAddr.String()]; ok { + if contract, exists := d.contracts[actor.Type]; exists { + info, err = contract.EventInfo(m.DstAddr, m.Body) + if err == nil { + return &info, nil + } + if !errors.Is(err, codec.ErrUnknownMessage) { + return nil, err + } + } + } + } + for _, contract := range d.contracts { info, err = contract.EventInfo(m.DstAddr, m.Body) if err == nil { From ff9c1b8b14eb48cbb2f1d608099400aef8a09601 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Wed, 11 Mar 2026 13:09:28 -0400 Subject: [PATCH 14/15] feat: feequoter getters --- pkg/bindings/getters.go | 19 ++++-- pkg/ccip/bindings/common/common.go | 4 +- pkg/ccip/bindings/common/reader.go | 24 +++++++ pkg/ccip/bindings/feequoter/fee_quoter.go | 13 ++-- pkg/ccip/bindings/feequoter/reader.go | 82 +++++++++++++++++++++++ 5 files changed, 129 insertions(+), 13 deletions(-) diff --git a/pkg/bindings/getters.go b/pkg/bindings/getters.go index 4d739e1a3..1c1731c8f 100644 --- a/pkg/bindings/getters.go +++ b/pkg/bindings/getters.go @@ -48,17 +48,22 @@ var TypeToGetterMap = map[string]GetterMap{ "sourceChainSelectors": offramp.GetSourceChainSelectors, }, TypeFeeQuoter: { - "owner": feequoter.GetOwner, - "pendingOwner": feequoter.GetPendingOwner, - "destChainConfig": feequoter.GetDestChainConfig, - "destinationChainGasPrice": feequoter.GetDestinationChainGasPrice, - "tokenPrice": feequoter.GetTokenPrice, - "staticConfig": feequoter.GetStaticConfig, - "destChainSelectors": feequoter.GetDestChainSelectors, + "owner": feequoter.GetOwner, + "pendingOwner": feequoter.GetPendingOwner, + "destChainConfig": feequoter.GetDestChainConfig, + "destinationChainGasPrice": feequoter.GetDestinationChainGasPrice, + "tokenPrice": feequoter.GetTokenPrice, + "staticConfig": feequoter.GetStaticConfig, + "destChainSelectors": feequoter.GetDestChainSelectors, + "feeTokens": feequoter.GetFeeTokens, + "premiumMultiplierWeiPerEth": feequoter.GetPremiumMultiplierWeiPerEth, + "tokenTransferFeeConfig": feequoter.GetTokenTransferFeeConfig, }, // Common contract getters (applies to all CCIP contracts) "link.chain.ton.ccip.Common": { "typeAndVersion": common.GetTypeAndVersion, + "facilityId": common.GetFacilityId, + "errorCode": common.GetErrorCode, }, // Ownable2Step pattern (inherited by many contracts) TypeOwnable: { diff --git a/pkg/ccip/bindings/common/common.go b/pkg/ccip/bindings/common/common.go index a184618f9..be937b3f2 100644 --- a/pkg/ccip/bindings/common/common.go +++ b/pkg/ccip/bindings/common/common.go @@ -15,7 +15,9 @@ import ( ) const ( - versionGetter = "typeAndVersion" + versionGetter = "typeAndVersion" + facilityIdGetter = "facilityId" + errorCodeGetter = "errorCode" ) // TVM limits for cell chains, enforced at different stages: diff --git a/pkg/ccip/bindings/common/reader.go b/pkg/ccip/bindings/common/reader.go index f3367e995..a640591dd 100644 --- a/pkg/ccip/bindings/common/reader.go +++ b/pkg/ccip/bindings/common/reader.go @@ -36,3 +36,27 @@ var GetTypeAndVersion = tvm.NewNoArgsGetter(tvm.NoArgsOpts[TypeAndVersion]{ }, nil }), }) + +// GetFacilityId gets the facility ID of the FeeQuoter contract +var GetFacilityId = tvm.NewNoArgsGetter(tvm.NoArgsOpts[uint16]{ + Name: facilityIdGetter, + Decoder: tvm.NewResultDecoder(func(r *ton.ExecutionResult) (uint16, error) { + v, err := r.Int(0) + if err != nil { + return 0, err + } + return uint16(v.Uint64()), nil + }), +}) + +// GetErrorCode gets the contract-specific error code for a given local error code +var GetErrorCode = tvm.Getter[uint16, uint16]{ + Name: errorCodeGetter, + Decoder: tvm.NewResultDecoder(func(r *ton.ExecutionResult) (uint16, error) { + v, err := r.Int(0) + if err != nil { + return 0, err + } + return uint16(v.Uint64()), nil + }), +} diff --git a/pkg/ccip/bindings/feequoter/fee_quoter.go b/pkg/ccip/bindings/feequoter/fee_quoter.go index 5ccbc6739..914931d78 100644 --- a/pkg/ccip/bindings/feequoter/fee_quoter.go +++ b/pkg/ccip/bindings/feequoter/fee_quoter.go @@ -79,11 +79,14 @@ const ( // Registry method names const ( - DestChainsGetter = "destChainSelectors" - tokenPriceGetter = "tokenPrice" - staticConfigGetter = "staticConfig" - destChainConfigGetter = "destChainConfig" - destinationChainGasPriceGetter = "destinationChainGasPrice" + DestChainsGetter = "destChainSelectors" + tokenPriceGetter = "tokenPrice" + staticConfigGetter = "staticConfig" + destChainConfigGetter = "destChainConfig" + destinationChainGasPriceGetter = "destinationChainGasPrice" + FeeTokensGetter = "feeTokens" + premiumMultiplierWeiPerEthGetter = "premiumMultiplierWeiPerEth" + tokenTransferFeeConfigGetter = "tokenTransferFeeConfig" ) type Storage struct { diff --git a/pkg/ccip/bindings/feequoter/reader.go b/pkg/ccip/bindings/feequoter/reader.go index 76f28d33c..a3e786f72 100644 --- a/pkg/ccip/bindings/feequoter/reader.go +++ b/pkg/ccip/bindings/feequoter/reader.go @@ -214,3 +214,85 @@ var GetDestChainSelectors = tvm.NewNoArgsGetter(tvm.NoArgsOpts[[]uint64]{ return lo.Map(selectors, func(x *big.Int, _ int) uint64 { return x.Uint64() }), nil }), }) + +// GetFeeTokens gets the list of fee token addresses +var GetFeeTokens = tvm.NewNoArgsGetter(tvm.NoArgsOpts[[]*address.Address]{ + Name: FeeTokensGetter, + Decoder: tvm.NewResultDecoder(func(r *ton.ExecutionResult) ([]*address.Address, error) { + slices, err := parser.ParseLispTuple[*cell.Slice](r.AsTuple()) + if err != nil { + return nil, err + } + addrs := make([]*address.Address, 0, len(slices)) + for _, s := range slices { + addr, err := s.LoadAddr() + if err != nil { + return nil, err + } + addrs = append(addrs, addr) + } + return addrs, nil + }), +}) + +// GetPremiumMultiplierWeiPerEth gets the premium multiplier wei per eth for a given fee token +var GetPremiumMultiplierWeiPerEth = tvm.Getter[*address.Address, *big.Int]{ + Name: premiumMultiplierWeiPerEthGetter, + Encoder: tvm.NewArgsEncoder(func(addr *address.Address) ([]any, error) { + addrSlice := cell.BeginCell().MustStoreAddr(addr).EndCell().BeginParse() + return []any{addrSlice}, nil + }), + Decoder: tvm.NewResultDecoder(func(r *ton.ExecutionResult) (*big.Int, error) { + return r.Int(0) + }), +} + +// TokenTransferFeeConfigInput is the input type for GetTokenTransferFeeConfig. +type TokenTransferFeeConfigInput struct { + DestChainSelector uint64 + Token *address.Address +} + +// GetTokenTransferFeeConfig gets the token transfer fee config for a given destination chain and token +var GetTokenTransferFeeConfig = tvm.Getter[TokenTransferFeeConfigInput, TokenTransferFeeConfig]{ + Name: tokenTransferFeeConfigGetter, + Encoder: tvm.NewArgsEncoder(func(args TokenTransferFeeConfigInput) ([]any, error) { + addrSlice := cell.BeginCell().MustStoreAddr(args.Token).EndCell().BeginParse() + return []any{args.DestChainSelector, addrSlice}, nil + }), + Decoder: tvm.NewResultDecoder(func(r *ton.ExecutionResult) (TokenTransferFeeConfig, error) { + var c TokenTransferFeeConfig + isEnabledInt, err := r.Int(0) + if err != nil { + return c, err + } + minFeeUsdCents, err := r.Int(1) + if err != nil { + return c, err + } + maxFeeUsdCents, err := r.Int(2) + if err != nil { + return c, err + } + deciBps, err := r.Int(3) + if err != nil { + return c, err + } + destGasOverhead, err := r.Int(4) + if err != nil { + return c, err + } + destBytesOverhead, err := r.Int(5) + if err != nil { + return c, err + } + return TokenTransferFeeConfig{ + IsEnabled: isEnabledInt.Cmp(big.NewInt(-1)) == 0, + MinFeeUsdCents: uint32(minFeeUsdCents.Uint64()), + MaxFeeUsdCents: uint32(maxFeeUsdCents.Uint64()), + DeciBps: uint16(deciBps.Uint64()), + DestGasOverhead: uint32(destGasOverhead.Uint64()), + DestBytesOverhead: uint32(destBytesOverhead.Uint64()), + }, nil + }), +} From a2ff65ad6f48641e15861911f4de348fd554ee27 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Thu, 12 Mar 2026 10:55:27 -0400 Subject: [PATCH 15/15] feat: add support for tonviewer --- pkg/ton/codec/debug/explorer/utils.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/ton/codec/debug/explorer/utils.go b/pkg/ton/codec/debug/explorer/utils.go index 2fd6449a7..c7ddc14ba 100644 --- a/pkg/ton/codec/debug/explorer/utils.go +++ b/pkg/ton/codec/debug/explorer/utils.go @@ -19,21 +19,31 @@ func ParseURL(urlStr string) (txHash, address, network string, err error) { network = "testnet" // default switch { case strings.Contains(u.Host, "testnet.tonscan.org"): + case strings.Contains(u.Host, "testnet.tonviewer.com"): network = "testnet" case strings.Contains(u.Host, "tonscan.org"): + case strings.Contains(u.Host, "tonviewer.com"): network = "mainnet" case strings.Contains(u.Host, "localhost"): network = "mylocalton" } // Handle tonscan.org transaction URLs: /tx/{hash} - if strings.Contains(u.Host, "tonscan.org") { + switch { + + case strings.Contains(u.Host, "tonscan.org"): pathParts := strings.Split(strings.Trim(u.Path, "/"), "/") if len(pathParts) >= 2 && pathParts[0] == "tx" { txHash = pathParts[1] return txHash, address, network, nil } - } else if strings.Contains(u.Host, "localhost") { + case strings.Contains(u.Host, "tonviewer.com"): + pathParts := strings.Split(strings.Trim(u.Path, "/"), "/") + if len(pathParts) >= 2 && pathParts[0] == "transaction" { + txHash = pathParts[1] + return txHash, address, network, nil + } + case strings.Contains(u.Host, "localhost"): // Handle mylocalton transaction URLs: /transaction?hash={hash}&account={address} if u.Path == "/transaction" { query := u.Query()