Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 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
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
47 changes: 27 additions & 20 deletions experimental/analyzer/report_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,27 @@ func BuildProposalReport(ctx context.Context, proposalContext ProposalContext, e
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
calls, err = AnalyzeEVMTransactions(ctx, proposalContext, env, chainSel, []types.Transaction{op.Transaction})

case chainsel.FamilySolana:
dec, err := AnalyzeSolanaTransactions(proposalContext, chainSel, []types.Transaction{op.Transaction})
if err != nil {
return nil, err
}
calls = dec
calls, err = AnalyzeSolanaTransactions(proposalContext, chainSel, []types.Transaction{op.Transaction})

case chainsel.FamilyAptos:
dec, err := AnalyzeAptosTransactions(proposalContext, chainSel, []types.Transaction{op.Transaction})
if err != nil {
return nil, err
}
calls = dec
calls, err = AnalyzeAptosTransactions(proposalContext, chainSel, []types.Transaction{op.Transaction})

case chainsel.FamilySui:
dec, err := AnalyzeSuiTransactions(proposalContext, chainSel, []types.Transaction{op.Transaction})
if err != nil {
return nil, err
}
calls = dec
calls, err = AnalyzeSuiTransactions(proposalContext, chainSel, []types.Transaction{op.Transaction})

case chainsel.FamilyTon:
calls, err = AnalyzeTONTransactions(proposalContext, []types.Transaction{op.Transaction})
default:
calls = []*DecodedCall{}
}

if err != nil {
return nil, err
}

rpt.Operations[i] = OperationReport{
ChainSelector: chainSel,
ChainName: chainNameOrUnknown(chainName),
Expand Down Expand Up @@ -128,6 +122,19 @@ func BuildTimelockReport(ctx context.Context, proposalCtx ProposalContext, env d
Calls: []*DecodedCall{dec[j]},
}
}
case chainsel.FamilyTon:
dec, err := AnalyzeTONTransactions(proposalCtx, 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{
Expand Down
124 changes: 79 additions & 45 deletions experimental/analyzer/report_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,33 +195,44 @@ func TestChainNameOrUnknown(t *testing.T) {
require.Equal(t, "<chain unknown>", chainNameOrUnknown(" "))
}

func TestBuildProposalReport_FamilyBranches(t *testing.T) {
func TestBuildProposalReport_FamilyErrors(t *testing.T) {
t.Parallel()

tests := []struct {
name string
selector uint64
expectedError string
name string
selector uint64
expectedMsg string
wantErr bool // if true, expect returned error; if false, error is in Method field (TON behavior)
}{
{
name: "EVM_missing_registry",
selector: chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector,
expectedError: "EVM registry is not available",
name: "EVM_missing_registry",
selector: chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector,
expectedMsg: "EVM registry is not available",
wantErr: true,
Comment on lines +210 to +211
Copy link

@krebernisak krebernisak Dec 15, 2025

Choose a reason for hiding this comment

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

nit: Can't this be simplified as wantErr: string which if set is the expected error string?

},
{
name: "Solana_missing_registry",
selector: chainsel.SOLANA_DEVNET.Selector,
expectedError: "failed to analyze solana transaction 0: solana decoder registry is not available",
name: "Solana_missing_registry",
selector: chainsel.SOLANA_DEVNET.Selector,
expectedMsg: "failed to analyze solana transaction 0: solana decoder registry is not available",
wantErr: true,
},
{
name: "Aptos_unmarshal_additional_fields",
selector: chainsel.APTOS_TESTNET.Selector,
expectedError: "failed to unmarshal Aptos additional fields: unexpected end of JSON input",
name: "Aptos_unmarshal_additional_fields",
selector: chainsel.APTOS_TESTNET.Selector,
expectedMsg: "failed to unmarshal Aptos additional fields: unexpected end of JSON input",
wantErr: true,
},
{
name: "Sui_unmarshal_additional_fields",
selector: chainsel.SUI_TESTNET.Selector,
expectedError: "failed to unmarshal Sui additional fields: unexpected end of JSON input",
name: "Sui_unmarshal_additional_fields",
selector: chainsel.SUI_TESTNET.Selector,
expectedMsg: "failed to unmarshal Sui additional fields: unexpected end of JSON input",
wantErr: true,
},
{
name: "TON_decode_failure",
selector: chainsel.TON_TESTNET.Selector,
expectedMsg: "failed to decode TON transaction",
wantErr: false, // TON doesn't unmarshal AdditionalFields, so decode errors go to Method field

Choose a reason for hiding this comment

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

Shouldn't diverge from other test cases

Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

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

The comment explains TON's behavior of not unmarshaling AdditionalFields, but this is misleading in the context of decode failures. The actual reason decode errors don't return as errors is stated in ton_analyzer.go:48 - it's to prevent blocking the whole proposal. Consider updating the comment to: '// TON returns decode errors in Method field to avoid blocking proposal processing'.

Suggested change
wantErr: false, // TON doesn't unmarshal AdditionalFields, so decode errors go to Method field
wantErr: false, // TON returns decode errors in Method field to avoid blocking proposal processing

Copilot uses AI. Check for mistakes.
},
}

Expand All @@ -245,40 +256,56 @@ func TestBuildProposalReport_FamilyBranches(t *testing.T) {
},
}

_, err := BuildProposalReport(t.Context(), ctx, deployment.Environment{}, proposal)
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedError)
report, err := BuildProposalReport(t.Context(), ctx, deployment.Environment{}, proposal)
if tt.wantErr {
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedMsg)
} else {
require.NoError(t, err)
require.Contains(t, report.Operations[0].Calls[0].Method, tt.expectedMsg)
}
})
}
}

func TestBuildTimelockReport_FamilyBranches(t *testing.T) {
func TestBuildTimelockReport_FamilyErrors(t *testing.T) {
t.Parallel()

tests := []struct {
name string
selector uint64
expectedError string
name string
selector uint64
expectedMsg string
wantErr bool // if true, expect returned error; if false, error is in Method field (TON behavior)
}{
{
name: "EVM_missing_registry",
selector: chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector,
expectedError: "EVM registry is not available",
name: "EVM_missing_registry",
selector: chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector,
expectedMsg: "EVM registry is not available",
wantErr: true,
},
{
name: "Solana_missing_registry",
selector: chainsel.SOLANA_DEVNET.Selector,
expectedError: "failed to analyze solana transaction 0: solana decoder registry is not available",
name: "Solana_missing_registry",
selector: chainsel.SOLANA_DEVNET.Selector,
expectedMsg: "failed to analyze solana transaction 0: solana decoder registry is not available",
wantErr: true,
},
{
name: "Aptos_unmarshal_additional_fields",
selector: chainsel.APTOS_TESTNET.Selector,
expectedError: "failed to unmarshal Aptos additional fields: unexpected end of JSON input",
name: "Aptos_unmarshal_additional_fields",
selector: chainsel.APTOS_TESTNET.Selector,
expectedMsg: "failed to unmarshal Aptos additional fields: unexpected end of JSON input",
wantErr: true,
},
{
name: "Sui_unmarshal_additional_fields",
selector: chainsel.SUI_TESTNET.Selector,
expectedError: "failed to unmarshal Sui additional fields: unexpected end of JSON input",
name: "Sui_unmarshal_additional_fields",
selector: chainsel.SUI_TESTNET.Selector,
expectedMsg: "failed to unmarshal Sui additional fields: unexpected end of JSON input",
wantErr: true,
},
{
name: "TON_decode_failure",
selector: chainsel.TON_TESTNET.Selector,
expectedMsg: "failed to decode TON transaction",
wantErr: false, // TON doesn't unmarshal AdditionalFields, so decode errors go to Method field

Choose a reason for hiding this comment

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

Hmm, not sure why is here a difference for TON? There shouldn't be one...

Choose a reason for hiding this comment

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

Doesn't SUI impl also surfaces errors via .Method member:

Copy link
Contributor Author

@huangzhen1997 huangzhen1997 Dec 16, 2025

Choose a reason for hiding this comment

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

This test is for default unhappy pass, where the analyze function suppose to fail immediately. Unlike SUI we don't need extra field Unmarshal, so the first error will be hiding in Method field.

Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

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

Same issue as Comment 4 - the comment is misleading about why decode errors go to the Method field. The reason is to avoid blocking proposal processing, not specifically about AdditionalFields unmarshaling.

Suggested change
wantErr: false, // TON doesn't unmarshal AdditionalFields, so decode errors go to Method field
wantErr: false, // decode errors are reported in the Method field so proposal processing is not blocked

Copilot uses AI. Check for mistakes.
},
}

Expand All @@ -302,9 +329,16 @@ func TestBuildTimelockReport_FamilyBranches(t *testing.T) {
},
}

_, err := BuildTimelockReport(t.Context(), proposalCtx, deployment.Environment{}, proposal)
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedError)
report, err := BuildTimelockReport(t.Context(), proposalCtx, deployment.Environment{}, proposal)
if tt.wantErr {
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedMsg)
} else {
require.NoError(t, err)
for _, op := range report.Batches[0].Operations {
require.Contains(t, op.Calls[0].Method, tt.expectedMsg)
}
}
})
}
}
Expand Down Expand Up @@ -372,14 +406,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 +429,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