Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e0f9aab
support ton analyzer, need testing
huangzhen1997 Dec 10, 2025
a51337a
fix typo
huangzhen1997 Dec 10, 2025
09fc3c2
mod tidy
huangzhen1997 Dec 10, 2025
fe17bf1
update test
huangzhen1997 Dec 10, 2025
a7aa1a2
simplify analyze function
huangzhen1997 Dec 10, 2025
d6f8113
refactor
huangzhen1997 Dec 10, 2025
d33bd19
fix lint
huangzhen1997 Dec 10, 2025
751007d
Merge branch 'main' into NONEVM-3070/support-ton-analyzer
huangzhen1997 Dec 11, 2025
c6e01f1
add test coverage
huangzhen1997 Dec 12, 2025
93c385e
lint
huangzhen1997 Dec 12, 2025
a840aea
add test
huangzhen1997 Dec 12, 2025
c56332e
fix lint
huangzhen1997 Dec 12, 2025
7294c52
Merge branch 'main' into NONEVM-3070/support-ton-analyzer
huangzhen1997 Dec 15, 2025
ccad05a
address comments
huangzhen1997 Dec 15, 2025
6f521dc
update test
huangzhen1997 Dec 15, 2025
933d0b8
bump mcms version
huangzhen1997 Dec 16, 2025
0d0ee38
Merge branch 'main' into NONEVM-3070/support-ton-analyzer
huangzhen1997 Dec 16, 2025
ed21211
bump version
huangzhen1997 Dec 16, 2025
19647e5
Merge branch 'main' into NONEVM-3070/support-ton-analyzer
huangzhen1997 Dec 21, 2025
163c1f0
address comments
huangzhen1997 Dec 21, 2025
324486c
fix lint and changeset
huangzhen1997 Dec 21, 2025
a06f6eb
typo
huangzhen1997 Dec 21, 2025
264318f
Merge branch 'main' into NONEVM-3070/support-ton-analyzer
huangzhen1997 Jan 22, 2026
4d80162
fix go mod and update registry
huangzhen1997 Jan 22, 2026
a005805
Update experimental/analyzer/upf/upf_test.go
huangzhen1997 Jan 22, 2026
c3530bf
fix test and refactor timelock checker
huangzhen1997 Jan 22, 2026
46fb259
fix lint
huangzhen1997 Jan 22, 2026
bd55ade
Merge branch 'main' into NONEVM-3070/support-ton-analyzer
huangzhen1997 Jan 22, 2026
d7cf8ae
Merge branch 'main' into NONEVM-3070/support-ton-analyzer
huangzhen1997 Jan 27, 2026
c2b5193
address comment
huangzhen1997 Jan 27, 2026
b23b872
Merge branch 'main' into NONEVM-3070/support-ton-analyzer
huangzhen1997 Jan 27, 2026
cb31ba4
update
huangzhen1997 Jan 27, 2026
4601b1a
Merge branch 'main' into NONEVM-3070/support-ton-analyzer
huangzhen1997 Jan 27, 2026
1b01990
bump mcms version
huangzhen1997 Jan 29, 2026
0e98af6
fix broken test
huangzhen1997 Jan 29, 2026
d69e6a7
bump mcms version
huangzhen1997 Jan 30, 2026
082ed38
Merge branch 'main' into NONEVM-3070/support-ton-analyzer
krebernisak Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/khaki-geese-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink-deployments-framework": minor
---

Adds TON blockchain analyzer support
123 changes: 35 additions & 88 deletions experimental/analyzer/report_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,9 @@ func BuildProposalReport(ctx context.Context, proposalContext ProposalContext, e
}
chainName, _ := GetChainNameBySelector(chainSel)

var calls []*DecodedCall
switch family {
case chainsel.FamilyEVM:
dec, err := AnalyzeEVMTransactions(ctx, proposalContext, env, chainSel, []types.Transaction{op.Transaction})
if err != nil {
return nil, err
}
calls = dec
case chainsel.FamilySolana:
dec, err := AnalyzeSolanaTransactions(proposalContext, chainSel, []types.Transaction{op.Transaction})
if err != nil {
return nil, err
}
calls = dec
case chainsel.FamilyAptos:
dec, err := AnalyzeAptosTransactions(proposalContext, chainSel, []types.Transaction{op.Transaction})
if err != nil {
return nil, err
}
calls = dec
case chainsel.FamilySui:
dec, err := AnalyzeSuiTransactions(proposalContext, chainSel, []types.Transaction{op.Transaction})
if err != nil {
return nil, err
}
calls = dec
default:
calls = []*DecodedCall{}
calls, err := analyzeTransactions(ctx, proposalContext, env, family, chainSel, []types.Transaction{op.Transaction})
if err != nil {
return nil, err
}

rpt.Operations[i] = OperationReport{
Expand All @@ -74,68 +49,22 @@ func BuildTimelockReport(ctx context.Context, proposalCtx ProposalContext, env d
}
chainName, _ := GetChainNameBySelector(chainSel)

dec, err := analyzeTransactions(ctx, proposalCtx, env, family, chainSel, batch.Transactions)
if err != nil {
return nil, err
}

ops := make([]OperationReport, len(batch.Transactions))
switch family {
case chainsel.FamilyEVM:
dec, err := AnalyzeEVMTransactions(ctx, proposalCtx, env, chainSel, batch.Transactions)
if err != nil {
return nil, err
for j := range batch.Transactions {
var calls []*DecodedCall
if j < len(dec) && dec[j] != nil {
calls = []*DecodedCall{dec[j]}
}
for j := range dec {
ops[j] = OperationReport{
ChainSelector: chainSel,
ChainName: chainNameOrUnknown(chainName),
Family: family,
Calls: []*DecodedCall{dec[j]},
}
}
case chainsel.FamilySolana:
dec, err := AnalyzeSolanaTransactions(proposalCtx, chainSel, batch.Transactions)
if err != nil {
return nil, err
}
for j := range dec {
ops[j] = OperationReport{
ChainSelector: chainSel,
ChainName: chainNameOrUnknown(chainName),
Family: family,
Calls: []*DecodedCall{dec[j]},
}
}
case chainsel.FamilyAptos:
dec, err := AnalyzeAptosTransactions(proposalCtx, chainSel, batch.Transactions)
if err != nil {
return nil, err
}
for j := range dec {
ops[j] = OperationReport{
ChainSelector: chainSel,
ChainName: chainNameOrUnknown(chainName),
Family: family,
Calls: []*DecodedCall{dec[j]},
}
}
case chainsel.FamilySui:
dec, err := AnalyzeSuiTransactions(proposalCtx, chainSel, batch.Transactions)
if err != nil {
return nil, err
}
for j := range dec {
ops[j] = OperationReport{
ChainSelector: chainSel,
ChainName: chainNameOrUnknown(chainName),
Family: family,
Calls: []*DecodedCall{dec[j]},
}
}
default:
for j := range batch.Transactions {
ops[j] = OperationReport{
ChainSelector: chainSel,
ChainName: chainNameOrUnknown(chainName),
Family: family,
Calls: nil,
}
ops[j] = OperationReport{
ChainSelector: chainSel,
ChainName: chainNameOrUnknown(chainName),
Family: family,
Calls: calls,
}
}

Expand All @@ -150,6 +79,24 @@ func BuildTimelockReport(ctx context.Context, proposalCtx ProposalContext, env d
return rpt, nil
}

// analyzeTransactions dispatches to the appropriate chain-family analyzer.
func analyzeTransactions(ctx context.Context, proposalCtx ProposalContext, env deployment.Environment, family string, chainSel uint64, txs []types.Transaction) ([]*DecodedCall, error) {
switch family {
case chainsel.FamilyEVM:
return AnalyzeEVMTransactions(ctx, proposalCtx, env, chainSel, txs)
case chainsel.FamilySolana:
return AnalyzeSolanaTransactions(proposalCtx, chainSel, txs)
case chainsel.FamilyAptos:
return AnalyzeAptosTransactions(proposalCtx, chainSel, txs)
case chainsel.FamilySui:
return AnalyzeSuiTransactions(proposalCtx, chainSel, txs)
case chainsel.FamilyTon:
return AnalyzeTONTransactions(proposalCtx, txs)
default:
return []*DecodedCall{}, nil
}
}

func chainNameOrUnknown(n string) string {
if n == "" || strings.TrimSpace(n) == "" {
return "<chain unknown>"
Expand Down
80 changes: 73 additions & 7 deletions experimental/analyzer/report_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ func TestChainNameOrUnknown(t *testing.T) {
require.Equal(t, "<chain unknown>", chainNameOrUnknown(" "))
}

// TestBuildProposalReport_FamilyErrors tests error handling when analyzers fail during
// preprocessing (e.g., missing registry, invalid AdditionalFields). These are hard errors
// that prevent the analyzer from proceeding.
func TestBuildProposalReport_FamilyBranches(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -252,6 +255,38 @@ func TestBuildProposalReport_FamilyBranches(t *testing.T) {
}
}

// TestBuildProposalReport_TONDecodeFailure tests TON's graceful decode error handling.
// Unlike other analyzers, TON doesn't require AdditionalFields for decoding (it only needs
// tx.Data and tx.ContractType), so it proceeds directly to the decode stage. Decode failures
// are handled gracefully by placing the error in the Method field rather than returning an error,
// allowing the proposal to continue processing.
func TestBuildProposalReport_TONDecodeFailure(t *testing.T) {
t.Parallel()

ctx := &DefaultProposalContext{
AddressesByChain: deployment.AddressesByChain{},
renderer: NewMarkdownRenderer(),
}
proposal := &mcms.Proposal{
Operations: []types.Operation{
{
ChainSelector: types.ChainSelector(chainsel.TON_TESTNET.Selector),
Transaction: types.Transaction{
To: "0x1234567890123456789012345678901234567890",
Data: []byte{0x01, 0x02, 0x03, 0x04},
},
},
},
}

report, err := BuildProposalReport(t.Context(), ctx, deployment.Environment{}, proposal)
require.NoError(t, err)
require.Contains(t, report.Operations[0].Calls[0].Method, "failed to decode TON transaction")
}

// TestBuildTimelockReport_FamilyErrors tests error handling when analyzers fail during
// preprocessing (e.g., missing registry, invalid AdditionalFields). These are hard errors
// that prevent the analyzer from proceeding.
func TestBuildTimelockReport_FamilyBranches(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -309,6 +344,37 @@ func TestBuildTimelockReport_FamilyBranches(t *testing.T) {
}
}

// TestBuildTimelockReport_TONDecodeFailure tests TON's graceful decode error handling.
// Unlike other analyzers, TON doesn't require AdditionalFields for decoding (it only needs
// tx.Data and tx.ContractType), so it proceeds directly to the decode stage. Decode failures
// are handled gracefully by placing the error in the Method field rather than returning an error,
// allowing the proposal to continue processing.
func TestBuildTimelockReport_TONDecodeFailure(t *testing.T) {
t.Parallel()

proposalCtx := &DefaultProposalContext{
AddressesByChain: deployment.AddressesByChain{},
renderer: NewMarkdownRenderer(),
}
proposal := &mcms.TimelockProposal{
Operations: []types.BatchOperation{
{
ChainSelector: types.ChainSelector(chainsel.TON_TESTNET.Selector),
Transactions: []types.Transaction{
{To: "0x1111111111111111111111111111111111111111", Data: []byte{0x01, 0x02, 0x03, 0x04}},
{To: "0x2222222222222222222222222222222222222222", Data: []byte{0x05, 0x06, 0x07, 0x08}},
},
},
},
}

report, err := BuildTimelockReport(t.Context(), proposalCtx, deployment.Environment{}, proposal)
require.NoError(t, err)
for _, op := range report.Batches[0].Operations {
require.Contains(t, op.Calls[0].Method, "failed to decode TON transaction")
}
}

// Test native token transfer integration with report builder
func TestBuildProposalReport_NativeTransfer(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -372,14 +438,14 @@ func TestBuildProposalReport_DefaultCase(t *testing.T) {
renderer: NewMarkdownRenderer(),
}

// Use a TON chain selector - TON family is not handled in the switch statement
// Use a TRON chain selector - TRON family is not handled in the switch statement
// so it will trigger the default case
tonChainSelector := chainsel.TON_LOCALNET.Selector
tronChainSelector := chainsel.TRON_DEVNET.Selector

proposal := &mcms.Proposal{
Operations: []types.Operation{
{
ChainSelector: types.ChainSelector(tonChainSelector),
ChainSelector: types.ChainSelector(tronChainSelector),
Transaction: types.Transaction{
To: "0x1234567890123456789012345678901234567890",
Data: []byte{0x01, 0x02, 0x03, 0x04},
Expand All @@ -395,8 +461,8 @@ func TestBuildProposalReport_DefaultCase(t *testing.T) {
require.Len(t, report.Operations, 1)

operation := report.Operations[0]
require.Equal(t, tonChainSelector, operation.ChainSelector)
require.Equal(t, "ton-localnet", operation.ChainName) // TON chain has a known name
require.Equal(t, "ton", operation.Family) // TON family
require.Empty(t, operation.Calls) // Default case sets calls to empty slice
require.Equal(t, tronChainSelector, operation.ChainSelector)
require.Equal(t, chainsel.TRON_DEVNET.Name, operation.ChainName) // TRON chain has a known name
require.Equal(t, chainsel.FamilyTron, operation.Family) // TRON family
require.Empty(t, operation.Calls) // Default case sets calls to empty slice
}
3 changes: 2 additions & 1 deletion experimental/analyzer/sui_analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"

chainsel "github.com/smartcontractkit/chain-selectors"
mcmssuisdk "github.com/smartcontractkit/mcms/sdk/sui"
"github.com/smartcontractkit/mcms/types"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
)
Expand Down
60 changes: 60 additions & 0 deletions experimental/analyzer/ton_analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package analyzer

import (
"fmt"

"github.com/smartcontractkit/chainlink-ton/pkg/bindings"
"github.com/smartcontractkit/mcms/sdk"
"github.com/smartcontractkit/mcms/sdk/ton"
"github.com/smartcontractkit/mcms/types"
)

// AnalyzeTONTransactions decodes a slice of TON transactions and returns their decoded representations.
func AnalyzeTONTransactions(ctx ProposalContext, txs []types.Transaction) ([]*DecodedCall, error) {
decoder := ton.NewDecoder(bindings.Registry)
decodedTxs := make([]*DecodedCall, len(txs))
for i, op := range txs {
analyzedTransaction, err := AnalyzeTONTransaction(ctx, decoder, op)
if err != nil {
return nil, fmt.Errorf("failed to analyze TON transaction %d: %w", i, err)
}
decodedTxs[i] = analyzedTransaction
}

return decodedTxs, nil
}

// AnalyzeTONTransaction decodes a single TON transaction using the MCMS TON decoder.
//
// Unlike Aptos/Sui analyzers, this function does not unmarshal AdditionalFields because
// the TON decoder only requires tx.Data (BOC cell) and tx.ContractType (metadata).
// AdditionalFields in TON is only used by the encoder/timelock_converter for the Value field.
//
// On decode failure, this function returns a DecodedCall with the error in the Method field
// instead of returning an error. This allows the proposal to continue processing even if
// a single transaction fails to decode.
func AnalyzeTONTransaction(_ ProposalContext, decoder sdk.Decoder, mcmsTx types.Transaction) (*DecodedCall, error) {
decodedOp, err := decoder.Decode(mcmsTx, mcmsTx.ContractType)
if err != nil {
// Don't return an error to not block the whole proposal decoding because of a single transaction decode failure.
// Instead, put the error message in the Method field so it's visible in the report.
errStr := fmt.Errorf("failed to decode TON transaction: %w", err)

return &DecodedCall{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should double check this with security. From one side I agree that a failure to decode should not be a blocker for operational activity, but on the other side I know security wants to push for us to reduce blind signing of proposals as much as possible. Will it be common to see decode of operations failing?

Address: mcmsTx.To,
Method: errStr.Error(),
}, nil
}

namedArgs, err := toNamedFields(decodedOp)
if err != nil {
return nil, fmt.Errorf("failed to convert decoded operation to named arguments: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we do the same thing of not returning an error and instead putting it on the Decoded call for the same reasons as the comment above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toNamedFields only fails when there's a keys/arguments mismatch, which indicates a programming bug rather than a transaction decode issue. So I think it would be better to surface this early. Both Sui and Aptos analyzers are returning errors at this point as well.

}

return &DecodedCall{
Address: mcmsTx.To,
Method: decodedOp.MethodName(),
Inputs: namedArgs,
Outputs: []NamedField{},
}, nil
}
Loading
Loading