diff --git a/.changeset/long-snakes-ring.md b/.changeset/long-snakes-ring.md new file mode 100644 index 00000000..6e6c8c30 --- /dev/null +++ b/.changeset/long-snakes-ring.md @@ -0,0 +1,5 @@ +--- +"@smartcontractkit/mcms": minor +--- + +Add TON implementation and unit/e2e tests diff --git a/.github/workflows/pull-request-main.yml b/.github/workflows/pull-request-main.yml index dc0606f0..af822035 100644 --- a/.github/workflows/pull-request-main.yml +++ b/.github/workflows/pull-request-main.yml @@ -69,6 +69,11 @@ jobs: contents: read actions: read steps: + - name: Install Nix + uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + - name: Install Rust uses: moonrepo/setup-rust@ede6de059f8046a5e236c94046823e2af11ca670 # v1.2.2 @@ -114,6 +119,14 @@ jobs: run: | ./e2e/tests/solana/compile-mcm-contracts.sh + - name: Build TON contracts + id: ton-contracts-build + shell: bash + run: | + PATH_CONTRACTS_TON_PKG="$(nix build .#chainlink-ton-contracts --print-out-paths)/" + PATH_CONTRACTS_TON="$PATH_CONTRACTS_TON_PKG/lib/node_modules/@chainlink/contracts-ton/build/" + echo "path=$PATH_CONTRACTS_TON" >> "$GITHUB_OUTPUT" + - name: Run e2e tests uses: smartcontractkit/.github/actions/ci-test-go@ci-test-go/1.0.0 with: @@ -138,11 +151,17 @@ jobs: CTF_CONFIGS=../config.sui.toml go test -p=1 -tags=e2e -v ./e2e/tests/... -run=TestSuiSuite || sui_failure=true echo "::endgroup::" + echo "::group::TON" + export PATH_CONTRACTS_TON="${{ steps.ton-contracts-build.outputs.path }}" + CTF_CONFIGS=../config.ton.toml go test -p=1 -tags=e2e -v ./e2e/tests/... -run=TestTONSuite || ton_failure=true + echo "::endgroup::" + [[ -n "${evm_failure}" ]] && echo "🚨 EVM e2e tests failed." [[ -n "${solana_failure}" ]] && echo "🚨 Solana e2e tests failed." [[ -n "${aptos_failure}" ]] && echo "🚨 Aptos e2e tests failed." [[ -n "${sui_failure}" ]] && echo "🚨 Sui e2e tests failed." - [[ -n "${evm_failure}" || -n "${solana_failure}" || -n "${aptos_failure}" || -n "${sui_failure}" ]] && { + [[ -n "${ton_failure}" ]] && echo "🚨 TON e2e tests failed." + [[ -n "${evm_failure}" || -n "${solana_failure}" || -n "${aptos_failure}" || -n "${sui_failure}" || -n "${ton_failure}" ]] && { exit 1 } || { echo "Exiting" diff --git a/.mockery.yaml b/.mockery.yaml index 5939bed9..959cf9e1 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -14,6 +14,24 @@ mockname: "{{.InterfaceName}}" inpackage: false outpkg: mocks packages: + github.com/xssnick/tonutils-go/ton/wallet: + config: + all: false + outpkg: "mock_ton" + interfaces: + TonAPI: + config: + dir: "./sdk/ton/mocks" + filename: "wallet.go" + github.com/xssnick/tonutils-go/ton: + config: + all: false + outpkg: "mock_ton" + interfaces: + APIClientWrapped: + config: + dir: "./sdk/ton/mocks" + filename: "api.go" github.com/smartcontractkit/mcms/sdk: github.com/smartcontractkit/mcms/sdk/evm: github.com/smartcontractkit/mcms/sdk/evm/bindings: diff --git a/e2e/config.ton.toml b/e2e/config.ton.toml new file mode 100644 index 00000000..5a115cdb --- /dev/null +++ b/e2e/config.ton.toml @@ -0,0 +1,18 @@ +[settings] +private_keys = [ + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", + "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" +] + +[ton_config] +chain_id = "-217" +type = "ton" +image = "ghcr.io/neodix42/mylocalton-docker:v3.97" + +[ton_config.out] +family = "ton" + +[ton_config.custom_env] +NEXT_BLOCK_GENERATION_DELAY = "0.5" +VERSION_CAPABILITIES = "12" diff --git a/e2e/tests/aptos/set_config.go b/e2e/tests/aptos/set_config.go index d8c8c809..721ed34e 100644 --- a/e2e/tests/aptos/set_config.go +++ b/e2e/tests/aptos/set_config.go @@ -4,7 +4,6 @@ package aptos import ( "slices" - "strings" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -30,7 +29,7 @@ func (a *TestSuite) TestSetConfig() { signers[i] = crypto.PubkeyToAddress(key.PublicKey) } slices.SortFunc(signers[:], func(a, b common.Address) int { - return strings.Compare(strings.ToLower(a.Hex()), strings.ToLower(b.Hex())) + return a.Cmp(b) }) bypasserConfig := &types.Config{ diff --git a/e2e/tests/runner_test.go b/e2e/tests/runner_test.go index caaae007..a04ff50e 100644 --- a/e2e/tests/runner_test.go +++ b/e2e/tests/runner_test.go @@ -11,6 +11,7 @@ import ( evme2e "github.com/smartcontractkit/mcms/e2e/tests/evm" solanae2e "github.com/smartcontractkit/mcms/e2e/tests/solana" suie2e "github.com/smartcontractkit/mcms/e2e/tests/sui" + tone2e "github.com/smartcontractkit/mcms/e2e/tests/ton" ) func TestEVMSuite(t *testing.T) { @@ -39,3 +40,12 @@ func TestSuiSuite(t *testing.T) { suite.Run(t, new(suie2e.TimelockCancelProposalTestSuite)) suite.Run(t, new(suie2e.MCMSUserUpgradeTestSuite)) } + +func TestTONSuite(t *testing.T) { + suite.Run(t, new(tone2e.SigningTestSuite)) + suite.Run(t, new(tone2e.SetConfigTestSuite)) + suite.Run(t, new(tone2e.SetRootTestSuite)) + suite.Run(t, new(tone2e.InspectionTestSuite)) + suite.Run(t, new(tone2e.ExecutionTestSuite)) + suite.Run(t, new(tone2e.TimelockInspectionTestSuite)) +} diff --git a/e2e/tests/setup.go b/e2e/tests/setup.go index 6deec077..9aa6f374 100644 --- a/e2e/tests/setup.go +++ b/e2e/tests/setup.go @@ -18,6 +18,9 @@ import ( "github.com/gagliardetto/solana-go/rpc/ws" "github.com/joho/godotenv" "github.com/stretchr/testify/require" + "github.com/xssnick/tonutils-go/ton" + + tonchain "github.com/smartcontractkit/chainlink-ton/pkg/ton/chain" "github.com/smartcontractkit/chainlink-testing-framework/framework" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" @@ -39,6 +42,7 @@ type Config struct { SolanaChain *blockchain.Input `toml:"solana_config"` AptosChain *blockchain.Input `toml:"aptos_config"` SuiChain *blockchain.Input `toml:"sui_config"` + TonChain *blockchain.Input `toml:"ton_config"` Settings struct { PrivateKeys []string `toml:"private_keys"` @@ -57,6 +61,8 @@ type TestSetup struct { AptosBlockchain *blockchain.Output SuiClient sui.ISuiAPI SuiBlockchain *blockchain.Output + TonClient *ton.APIClient + TonBlockchain *blockchain.Output Config } @@ -203,6 +209,30 @@ func InitializeSharedTestSetup(t *testing.T) *TestSetup { t.Logf("Initialized Sui RPC client @ %s", nodeURL) } + var ( + tonClient *ton.APIClient + tonBlockchainOutput *blockchain.Output + ) + if in.TonChain != nil { + // Use blockchain network setup (fallback) + ports := freeport.GetN(t, 2) + port := ports[0] + faucetPort := ports[1] + in.TonChain.Port = strconv.Itoa(port) + in.TonChain.FaucetPort = strconv.Itoa(faucetPort) + + tonBlockchainOutput, err = blockchain.NewBlockchainNetwork(in.TonChain) + require.NoError(t, err, "Failed to initialize TON blockchain") + + nodeURL := tonBlockchainOutput.Nodes[0].ExternalHTTPUrl + pool, err := tonchain.CreateLiteserverConnectionPool(ctx, nodeURL) + require.NoError(t, err, "Failed to initialize TON client - failed to create liteserver connection pool") + tonClient = ton.NewAPIClient(pool, ton.ProofCheckPolicyFast) + + // Test liveness, will also fetch ChainID + t.Logf("Initialized TON RPC client @ %s", nodeURL) + } + sharedSetup = &TestSetup{ ClientA: ethClientA, ClientB: ethClientB, @@ -213,6 +243,8 @@ func InitializeSharedTestSetup(t *testing.T) *TestSetup { AptosBlockchain: aptosBlockchainOutput, SuiClient: suiClient, SuiBlockchain: suiBlockchainOutput, + TonClient: tonClient, + TonBlockchain: tonBlockchainOutput, Config: *in, } }) diff --git a/e2e/tests/sui/set_config.go b/e2e/tests/sui/set_config.go index 7d52bc77..12b4d37b 100644 --- a/e2e/tests/sui/set_config.go +++ b/e2e/tests/sui/set_config.go @@ -4,7 +4,6 @@ package sui import ( "slices" - "strings" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -26,7 +25,7 @@ func (s *TestSuite) TestSetConfig() { signers[i] = crypto.PubkeyToAddress(key.PublicKey) } slices.SortFunc(signers[:], func(a, b common.Address) int { - return strings.Compare(strings.ToLower(a.Hex()), strings.ToLower(b.Hex())) + return a.Cmp(b) }) bypasserConfig := &types.Config{ diff --git a/e2e/tests/ton/common.go b/e2e/tests/ton/common.go new file mode 100644 index 00000000..0ba737fd --- /dev/null +++ b/e2e/tests/ton/common.go @@ -0,0 +1,122 @@ +//go:build e2e + +package tone2e + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/ethereum/go-ethereum/common" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/ton/wallet" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/timelock" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tracetracking" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/wrappers" + + "github.com/smartcontractkit/mcms/internal/testutils" + "github.com/smartcontractkit/mcms/types" +) + +const ( + EnvPathContracts = "PATH_CONTRACTS_TON" + + PathContractsMCMS = "mcms.MCMS.compiled.json" + PathContractsTimelock = "mcms.RBACTimelock.compiled.json" +) + +func must[E any](out E, err error) E { + if err != nil { + panic(err) + } + + return out +} + +type DeployOpts struct { + // Connection + Client *ton.APIClient + Wallet *wallet.Wallet + + // Deployment info + ContractPath string + + Amount tlb.Coins + Data any + Body any +} + +func DeployContract(ctx context.Context, opts DeployOpts) (*address.Address, error) { + contractCode, err := wrappers.ParseCompiledContract(opts.ContractPath) + if err != nil { + return nil, fmt.Errorf("failed to parse compiled contract: %w", err) + } + + contractData, ok := opts.Data.(*cell.Cell) // Cell or we try to decode + if !ok { + contractData, err = tlb.ToCell(opts.Data) + if err != nil { + return nil, fmt.Errorf("failed to create contract data cell: %w", err) + } + } + + bodyCell, ok := opts.Body.(*cell.Cell) // Cell or we try to decode + if !ok { + bodyCell, err = tlb.ToCell(opts.Body) + if err != nil { + return nil, fmt.Errorf("failed to create contract body cell: %w", err) + } + } + + _client := tracetracking.NewSignedAPIClient(opts.Client, *opts.Wallet) + contract, _, err := wrappers.Deploy(ctx, &_client, contractCode, contractData, opts.Amount, bodyCell) + if err != nil { + return nil, fmt.Errorf("failed to deploy contract: %w", err) + } + + return contract.Address, nil +} + +func DeployMCMSContract(ctx context.Context, client *ton.APIClient, w *wallet.Wallet, amount tlb.Coins, data mcms.Data) (*address.Address, error) { + return DeployContract(ctx, DeployOpts{ + Client: client, + Wallet: w, + ContractPath: filepath.Join(os.Getenv(EnvPathContracts), PathContractsMCMS), + Amount: amount, + Data: data, + Body: cell.BeginCell().EndCell(), // empty cell, top up + }) +} + +func DeployTimelockContract(ctx context.Context, client *ton.APIClient, w *wallet.Wallet, amount tlb.Coins, data timelock.Data, body timelock.Init) (*address.Address, error) { + return DeployContract(ctx, DeployOpts{ + Client: client, + Wallet: w, + ContractPath: filepath.Join(os.Getenv(EnvPathContracts), PathContractsTimelock), + Amount: amount, + Data: data, + Body: body, + }) +} + +// GenSimpleTestMCMSConfig generates a simple test configuration that's used in e2e tests. +func GenSimpleTestMCMSConfig(signers []testutils.ECDSASigner) *types.Config { + return &types.Config{ + Quorum: 1, + Signers: []common.Address{signers[0].Address()}, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{signers[1].Address()}, + GroupSigners: []types.Config{}, + }, + }, + } +} diff --git a/e2e/tests/ton/executable.go b/e2e/tests/ton/executable.go new file mode 100644 index 00000000..925aad6c --- /dev/null +++ b/e2e/tests/ton/executable.go @@ -0,0 +1,785 @@ +//go:build e2e + +package tone2e + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "strconv" + + "github.com/stretchr/testify/suite" + + cselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton/wallet" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/lib/access/rbac" + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/timelock" + toncommon "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/common" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/hash" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tlbe" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tracetracking" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + "github.com/ethereum/go-ethereum/common" + + mcmslib "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/sdk/evm" + "github.com/smartcontractkit/mcms/types" + + e2e "github.com/smartcontractkit/mcms/e2e/tests" + "github.com/smartcontractkit/mcms/internal/testutils" + + mcmston "github.com/smartcontractkit/mcms/sdk/ton" +) + +type ExecutionTestSuite struct { + suite.Suite + + signers []testutils.ECDSASigner + + // Sign proposals across multiple chains, execute and verify on Chain A + ChainA types.ChainSelector + ChainB types.ChainSelector + ChainC types.ChainSelector + + // Chain A metadata + mcmsAddr string + timelockAddr string + + wallet *wallet.Wallet + + e2e.TestSetup +} + +// SetupSuite runs before the test suite +func (s *ExecutionTestSuite) SetupSuite() { + s.TestSetup = *e2e.InitializeSharedTestSetup(s.T()) + + // Init wallet + var err error + s.wallet, err = tvm.MyLocalTONWalletDefault(s.TonClient) + s.Require().NoError(err) + + // Generate few test signers + s.signers = testutils.MakeNewECDSASigners(2) + + // Initialize chains + details, err := cselectors.GetChainDetailsByChainIDAndFamily(s.TonBlockchain.ChainID, s.TonBlockchain.Family) + s.Require().NoError(err) + s.ChainA = types.ChainSelector(details.ChainSelector) + + s.ChainB = types.ChainSelector(cselectors.GETH_TESTNET.Selector) + s.ChainC = types.ChainSelector(cselectors.GETH_DEVNET_2.Selector) + + // Deploy contracts on chain A (the one we execute on) + s.deployMCMSContract(hash.CRC32("test.executable.mcms")) + s.deployTimelockContract(hash.CRC32("test.executable.timelock")) +} + +// TestExecuteProposal executes a proposal after setting the root +func (s *ExecutionTestSuite) TestExecuteProposal() { + ctx := context.Background() + + // Construct a proposal + + // Construct a TON transaction to grant a role + + // Grant role data + grantRoleData, err := tlb.ToCell(rbac.GrantRole{ + QueryID: 0x1, + Role: tlbe.NewUint256(timelock.RoleProposer), + Account: address.MustParseAddr(s.mcmsAddr), + }) + s.Require().NoError(err) + + opTX, err := mcmston.NewTransaction( + address.MustParseAddr(s.timelockAddr), + grantRoleData.ToBuilder().ToSlice(), + tlb.MustFromTON("0.1").Nano(), + "RBACTimelock", + []string{"RBACTimelock", "GrantRole"}, + ) + s.Require().NoError(err) + + proposal := mcmslib.Proposal{ + BaseProposal: mcmslib.BaseProposal{ + Version: "v1", + Description: "Grants RBACTimelock 'Proposer' Role to MCMS Contract", + Kind: types.KindProposal, + ValidUntil: 2004259681, + Signatures: []types.Signature{}, + OverridePreviousRoot: false, + ChainMetadata: map[types.ChainSelector]types.ChainMetadata{ + s.ChainA: { + StartingOpCount: 0, + MCMAddress: s.mcmsAddr, + AdditionalFields: testOpAdditionalFields, + }, + }, + }, + Operations: []types.Operation{ + { + ChainSelector: s.ChainA, + Transaction: opTX, + }, + }, + } + + tree, err := proposal.MerkleTree() + s.Require().NotNil(tree) + s.Require().NoError(err) + + // Gen caller map for easy access (we can use geth chainselector for anvil) + inspectors := map[types.ChainSelector]sdk.Inspector{ + s.ChainA: mcmston.NewInspector(s.TonClient), + } + + // Construct executor + signable, err := mcmslib.NewSignable(&proposal, inspectors) + s.Require().NoError(err) + s.Require().NotNil(signable) + + err = signable.ValidateConfigs(ctx) + s.Require().NoError(err) + + _, err = signable.SignAndAppend(mcmslib.NewPrivateKeySigner(s.signers[1].Key)) + s.Require().NoError(err) + + // Validate the signatures + quorumMet, err := signable.ValidateSignatures(ctx) + s.Require().NoError(err) + s.Require().True(quorumMet) + + // Construct encoders + encoders, err := proposal.GetEncoders() + s.Require().NoError(err) + + // Construct executors + encoder := encoders[s.ChainA].(*mcmston.Encoder) + executor, err := mcmston.NewExecutor(mcmston.ExecutorOpts{ + Encoder: encoder, + Client: s.TonClient, + Wallet: s.wallet, + Amount: tlb.MustFromTON("0.1"), + }) + s.Require().NoError(err) + executors := map[types.ChainSelector]sdk.Executor{ + s.ChainA: executor, + } + + // Construct executable + executable, err := mcmslib.NewExecutable(&proposal, executors) + s.Require().NoError(err) + + // SetRoot on the contract + res, err := executable.SetRoot(ctx, s.ChainA) + s.Require().NoError(err) + s.Require().NotEmpty(res.Hash) + + // Wait for transaction to be mined + tx, ok := res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx) + + // Wait and check success + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) + + // Validate Contract State and verify root was set A + rootARoot, rootAValidUntil, err := inspectors[s.ChainA].GetRoot(ctx, s.mcmsAddr) + s.Require().NoError(err) + s.Require().Equal(rootARoot, common.Hash([32]byte(tree.Root.Bytes()))) + s.Require().Equal(rootAValidUntil, proposal.ValidUntil) + + // Execute the proposal + res, err = executable.Execute(ctx, 0) + s.Require().NoError(err) + s.Require().NotEmpty(res.Hash) + + // Wait for transaction to be mined + tx, ok = res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx) + + // Wait and check success + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) + + // Verify the operation count is updated on chain A + newOpCountA, err := inspectors[s.ChainA].GetOpCount(ctx, s.mcmsAddr) + s.Require().NoError(err) + s.Require().Equal(uint64(1), newOpCountA) + + // Check the state of the timelock contract + inspectorT := mcmston.NewTimelockInspector(s.TonClient) + proposers, err := inspectorT.GetProposers(ctx, s.timelockAddr) + s.Require().NoError(err) + s.Require().Len(proposers, 2) + s.Require().Contains(proposers, s.mcmsAddr) +} + +// TestExecuteProposalMultiple executes 2 proposals to check nonce calculation mechanisms are working +func (s *ExecutionTestSuite) TestExecuteProposalMultiple() { + ctx := context.Background() + + // Construct a TON transaction to grant a role + + // Grant role data + grantRoleData, err := tlb.ToCell(rbac.GrantRole{ + QueryID: 0x1, + Role: tlbe.NewUint256(timelock.RoleProposer), + Account: s.wallet.Address(), + }) + s.Require().NoError(err) + + opTX, err := mcmston.NewTransaction( + address.MustParseAddr(s.timelockAddr), + grantRoleData.ToBuilder().ToSlice(), + tlb.MustFromTON("0.1").Nano(), + "RBACTimelock", + []string{"RBACTimelock", "GrantRole"}, + ) + s.Require().NoError(err) + + // Construct a proposal + proposal := mcmslib.Proposal{ + BaseProposal: mcmslib.BaseProposal{ + Version: "v1", + Description: "Grants RBACTimelock 'Proposer' Role to MCMS Contract", + Kind: types.KindProposal, + ValidUntil: 2004259681, + Signatures: []types.Signature{}, + OverridePreviousRoot: false, + ChainMetadata: map[types.ChainSelector]types.ChainMetadata{ + s.ChainA: { + StartingOpCount: 1, + MCMAddress: s.mcmsAddr, + AdditionalFields: testOpAdditionalFields, + }, + }, + }, + Operations: []types.Operation{ + { + ChainSelector: s.ChainA, + Transaction: opTX, + }, + }, + } + + tree, err := proposal.MerkleTree() + s.Require().NotNil(tree) + s.Require().NoError(err) + + // Gen caller map for easy access (we can use geth chainselector for anvil) + inspectors := map[types.ChainSelector]sdk.Inspector{ + s.ChainA: mcmston.NewInspector(s.TonClient), + } + + // Construct executor + signable, err := mcmslib.NewSignable(&proposal, inspectors) + s.Require().NoError(err) + s.Require().NotNil(signable) + + _, err = signable.SignAndAppend(mcmslib.NewPrivateKeySigner(s.signers[1].Key)) + s.Require().NoError(err) + + // Validate the signatures + quorumMet, err := signable.ValidateSignatures(ctx) + s.Require().NoError(err) + s.Require().True(quorumMet) + + // Construct encoders + encoders, err := proposal.GetEncoders() + s.Require().NoError(err) + + // Construct executors + encoder := encoders[s.ChainA].(*mcmston.Encoder) + executor, err := mcmston.NewExecutor(mcmston.ExecutorOpts{ + Encoder: encoder, + Client: s.TonClient, + Wallet: s.wallet, + Amount: tlb.MustFromTON("0.1"), + }) + s.Require().NoError(err) + executors := map[types.ChainSelector]sdk.Executor{ + s.ChainA: executor, + } + + // Construct executable + executable, err := mcmslib.NewExecutable(&proposal, executors) + s.Require().NoError(err) + + // SetRoot on the contract + res, err := executable.SetRoot(ctx, s.ChainA) + s.Require().NoError(err) + s.Require().NotEmpty(res.Hash) + + // Wait for transaction to be mined + tx, ok := res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx) + + // Wait and check success + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) + + // Validate Contract State and verify root was set A + rootARoot, rootAValidUntil, err := inspectors[s.ChainA].GetRoot(ctx, s.mcmsAddr) + s.Require().NoError(err) + s.Require().Equal(rootARoot, common.Hash([32]byte(tree.Root.Bytes()))) + s.Require().Equal(rootAValidUntil, proposal.ValidUntil) + + // Execute the proposal + res, err = executable.Execute(ctx, 0) + s.Require().NoError(err) + s.Require().NotEmpty(res.Hash) + + // Wait for transaction to be mined + tx, ok = res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx) + + // Wait and check success + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) + + // Verify the operation count is updated on chain A + newOpCountA, err := inspectors[s.ChainA].GetOpCount(ctx, s.mcmsAddr) + s.Require().NoError(err) + s.Require().Equal(uint64(2), newOpCountA) + + // Check the state of the timelock contract + inspectorT := mcmston.NewTimelockInspector(s.TonClient) + proposers, err := inspectorT.GetProposers(ctx, s.timelockAddr) + s.Require().NoError(err) + s.Require().Len(proposers, 2) + s.Require().Contains(proposers, s.wallet.Address().String()) + + // Construct 2nd proposal + + // Construct a TON transaction to grant a role + // Grant role data + grantRoleData2, err := tlb.ToCell(rbac.GrantRole{ + QueryID: 0x1, + Role: tlbe.NewUint256(timelock.RoleBypasser), + Account: address.MustParseAddr(s.mcmsAddr), + }) + s.Require().NoError(err) + + opTX2, err := mcmston.NewTransaction( + address.MustParseAddr(s.timelockAddr), + grantRoleData2.ToBuilder().ToSlice(), + tlb.MustFromTON("0.1").Nano(), + "RBACTimelock", + []string{"RBACTimelock", "GrantRole"}, + ) + s.Require().NoError(err) + + proposal2 := mcmslib.Proposal{ + BaseProposal: mcmslib.BaseProposal{ + Version: "v1", + Description: "Grants RBACTimelock 'Proposer' Role to MCMS Contract", + Kind: types.KindProposal, + ValidUntil: 2004259681, + Signatures: []types.Signature{}, + OverridePreviousRoot: false, + ChainMetadata: map[types.ChainSelector]types.ChainMetadata{ + s.ChainA: { + StartingOpCount: 2, + MCMAddress: s.mcmsAddr, + AdditionalFields: testOpAdditionalFields, + }, + }, + }, + Operations: []types.Operation{ + { + ChainSelector: s.ChainA, + Transaction: opTX2, + }, + }, + } + + // Construct executor + signable2, err := mcmslib.NewSignable(&proposal2, inspectors) + s.Require().NoError(err) + s.Require().NotNil(signable2) + + _, err = signable2.SignAndAppend(mcmslib.NewPrivateKeySigner(s.signers[1].Key)) + s.Require().NoError(err) + + // Validate the signatures + quorumMet, err = signable2.ValidateSignatures(ctx) + s.Require().NoError(err) + s.Require().True(quorumMet) + + // Construct executors + executors2 := map[types.ChainSelector]sdk.Executor{ + s.ChainA: executor, + } + + // Construct executable + executable2, err := mcmslib.NewExecutable(&proposal2, executors2) + s.Require().NoError(err) + + // SetRoot on the contract + res, err = executable2.SetRoot(ctx, s.ChainA) + s.Require().NoError(err) + s.Require().NotEmpty(res.Hash) + + // Wait for transaction to be mined + tx, ok = res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx) + + // Wait and check success + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) + + tree2, err := proposal2.MerkleTree() + s.Require().NotNil(tree2) + s.Require().NoError(err) + + // Validate Contract State and verify root was set A + rootARoot, rootAValidUntil, err = inspectors[s.ChainA].GetRoot(ctx, s.mcmsAddr) + s.Require().NoError(err) + s.Require().Equal(rootARoot, common.Hash([32]byte(tree2.Root.Bytes()))) + s.Require().Equal(rootAValidUntil, proposal2.ValidUntil) + + // Execute the proposal + res, err = executable2.Execute(ctx, 0) + s.Require().NoError(err) + s.Require().NotEmpty(res.Hash) + + // Wait for transaction to be mined + tx, ok = res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx) + + // Wait and check success + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) + + // Check the state of the MCMS contract + // Verify the operation count is updated on chain A + newOpCountA, err = inspectors[s.ChainA].GetOpCount(ctx, s.mcmsAddr) + s.Require().NoError(err) + s.Require().Equal(uint64(3), newOpCountA) + + // Check the state of the timelock contract + bypassers, err := inspectorT.GetBypassers(ctx, s.timelockAddr) + s.Require().NoError(err) + s.Require().Len(bypassers, 2) + s.Require().Contains(bypassers, s.mcmsAddr) +} + +// TestExecuteProposalMultipleChains executes a proposal with operations on two different chains +func (s *ExecutionTestSuite) TestExecuteProposalMultipleChains() { + ctx := context.Background() + + // Op counts before execution + inspectorA := mcmston.NewInspector(s.TonClient) + opCountA, err := inspectorA.GetOpCount(ctx, s.mcmsAddr) + s.Require().NoError(err) + opCountB := uint64(0) + opCountC := uint64(0) + + // Construct a TON transaction to grant a role + + // Sends some funds to MCMS contract + opTX, err := mcmston.NewTransaction( + address.MustParseAddr(s.mcmsAddr), + cell.BeginCell().ToSlice(), // empty message (top up) + tlb.MustFromTON("0.1").Nano(), + "RBACTimelock", + []string{"RBACTimelock", "TopUp"}, + ) + s.Require().NoError(err) + + // Dummy transaction for EVM chains B/C + dummyTX := evm.NewTransaction( + s.signers[0].Address(), + []byte("0x13424"), + big.NewInt(0), + "", + []string{""}, + ) + + proposalTimelock := mcmslib.TimelockProposal{ + BaseProposal: mcmslib.BaseProposal{ + Version: "v1", + Kind: types.KindTimelockProposal, + Description: "description", + ValidUntil: 2004259681, + OverridePreviousRoot: true, + Signatures: []types.Signature{}, + ChainMetadata: map[types.ChainSelector]types.ChainMetadata{ + s.ChainA: { + StartingOpCount: opCountA, + MCMAddress: s.mcmsAddr, + AdditionalFields: testOpAdditionalFields, + }, + s.ChainB: { + StartingOpCount: opCountB, + MCMAddress: "0xdead0001", + }, + s.ChainC: { + StartingOpCount: opCountC, + MCMAddress: "0xdead1001", + }, + }, + }, + Action: types.TimelockActionSchedule, + Delay: types.MustParseDuration("0s"), + TimelockAddresses: map[types.ChainSelector]string{ + s.ChainA: s.timelockAddr, + s.ChainB: "0xdead0002", + s.ChainC: "0xdead1002", + }, + Operations: []types.BatchOperation{ + { + ChainSelector: s.ChainA, + Transactions: []types.Transaction{opTX}, + }, + { + ChainSelector: s.ChainB, + Transactions: []types.Transaction{dummyTX}, + }, + { + ChainSelector: s.ChainC, + Transactions: []types.Transaction{dummyTX}, + }, + }, + } + + proposal, _, err := proposalTimelock.Convert(ctx, map[types.ChainSelector]sdk.TimelockConverter{ + s.ChainA: mcmston.NewTimelockConverter(mcmston.DefaultSendAmount), + s.ChainB: &evm.TimelockConverter{}, + s.ChainC: &evm.TimelockConverter{}, + }) + s.Require().NoError(err) + + tree, err := proposal.MerkleTree() + s.Require().NotNil(tree) + s.Require().NoError(err) + + // Sign proposal + inspectors := map[types.ChainSelector]sdk.Inspector{ + s.ChainA: inspectorA, + s.ChainB: s.newMockEVMInspector(proposal.ChainMetadata[s.ChainB]), + s.ChainC: s.newMockEVMInspector(proposal.ChainMetadata[s.ChainC]), + } + + // Construct signable object + signable, err := mcmslib.NewSignable(&proposal, inspectors) + s.Require().NoError(err) + s.Require().NotNil(signable) + + err = signable.ValidateConfigs(ctx) + s.Require().NoError(err) + + _, err = signable.SignAndAppend(mcmslib.NewPrivateKeySigner(s.signers[1].Key)) + s.Require().NoError(err) + + // Validate signatures + quorumMet, err := signable.ValidateSignatures(ctx) + s.Require().NoError(err) + s.Require().True(quorumMet) + + // Construct encoders for both chains + encoders, err := proposal.GetEncoders() + s.Require().NoError(err) + encoderA := encoders[s.ChainA].(*mcmston.Encoder) + encoderB := encoders[s.ChainB].(*evm.Encoder) + encoderC := encoders[s.ChainC].(*evm.Encoder) + + // Construct executors for both chains + executors := map[types.ChainSelector]sdk.Executor{ + s.ChainA: must(mcmston.NewExecutor(mcmston.ExecutorOpts{ + Encoder: encoderA, + Client: s.TonClient, + Wallet: s.wallet, + Amount: tlb.MustFromTON("0.1"), + })), + s.ChainB: evm.NewExecutor(encoderB, nil, nil), // No need to execute on Chain B or C + s.ChainC: evm.NewExecutor(encoderC, nil, nil), + } + + // Notice: simulation not supported for TON executor yet + + // Construct executable object + executable, err := mcmslib.NewExecutable(&proposal, executors) + s.Require().NoError(err) + + // SetRoot on MCMS Contract for Chain A (only) + res, err := executable.SetRoot(ctx, s.ChainA) + s.Require().NoError(err) + s.Require().NotEmpty(res.Hash) + + // Wait for transaction to be mined + tx, ok := res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx) + + // Wait and check success + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) + + // Validate Contract State and verify root was set A + rootARoot, rootAValidUntil, err := inspectors[s.ChainA].GetRoot(ctx, s.mcmsAddr) + s.Require().NoError(err) + s.Require().Equal(rootARoot, common.Hash([32]byte(tree.Root.Bytes()))) + s.Require().Equal(rootAValidUntil, proposal.ValidUntil) + + res, err = executable.Execute(ctx, 0) + s.Require().NoError(err) + s.Require().NotEmpty(res.Hash) + + // Wait for transaction to be mined + tx, ok = res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx) + + // Wait and check success + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) + + // Verify the operation count is updated on chain A + newOpCountA, err := inspectorA.GetOpCount(ctx, s.mcmsAddr) + s.Require().NoError(err) + s.Require().Equal(opCountA+1, newOpCountA) + + // Construct executors + tExecutors := map[types.ChainSelector]sdk.TimelockExecutor{ + s.ChainA: must(mcmston.NewTimelockExecutor(mcmston.TimelockExecutorOpts{ + Client: s.TonClient, + Wallet: s.wallet, + Amount: tlb.MustFromTON("0.2"), + })), + s.ChainB: evm.NewTimelockExecutor(nil, nil), // No need to execute on Chain B or C + s.ChainC: evm.NewTimelockExecutor(nil, nil), + } + + // Create new executable + tExecutable, err := mcmslib.NewTimelockExecutable(ctx, &proposalTimelock, tExecutors) + s.Require().NoError(err) + + // Notice: skipped as fails on sdk/evm.TimelockInspector.IsOperationReady + // Could be enabled with an evm TimelockExecutor/Inspector mock similar + // err = tExecutable.IsReady(ctx) + // s.Require().NoError(err) + + // Execute operation 0 + res, err = tExecutable.Execute(ctx, 0) + s.Require().NoError(err) + + // Wait for transaction to be mined + tx, ok = res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx) + + // Wait and check success + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) +} + +var testOpAdditionalFields = json.RawMessage(fmt.Sprintf(`{"value": %d}`, tlb.MustFromTON("0.1").Nano().Uint64())) + +func (s *ExecutionTestSuite) deployTimelockContract(id uint32) { + ctx := s.T().Context() + amount := tlb.MustFromTON("1.5") // TODO (ton): high gas + + data := timelock.EmptyDataFrom(id) + mcmsAddr := address.MustParseAddr(s.mcmsAddr) + // When deploying the contract, send the Init message to initialize the Timelock contract + accounts := []toncommon.AddressWrap{ + {Val: mcmsAddr}, + {Val: s.wallet.Address()}, + } + body := timelock.Init{ + QueryID: 0, + MinDelay: 0, + Admin: mcmsAddr, + Proposers: accounts, + Executors: accounts, + Cancellers: accounts, + Bypassers: accounts, + ExecutorRoleCheckEnabled: true, + OpFinalizationTimeout: 0, + } + + timelockAddr, err := DeployTimelockContract(ctx, s.TonClient, s.wallet, amount, data, body) + s.Require().NoError(err) + s.timelockAddr = timelockAddr.String() +} + +func (s *ExecutionTestSuite) deployMCMSContract(id uint32) { + ctx := s.T().Context() + + // TODO (ton): when MCMS is out of gas, executions fail silently + // - trace doesn't return error, but opCount doesn't increase + amount := tlb.MustFromTON("10") + chainID, err := strconv.ParseInt(s.TonBlockchain.ChainID, 10, 64) + s.Require().NoError(err) + data := mcms.EmptyDataFrom(id, s.wallet.Address(), chainID) + mcmsAddr, err := DeployMCMSContract(ctx, s.TonClient, s.wallet, amount, data) + s.Require().NoError(err) + s.mcmsAddr = mcmsAddr.String() + + // Set configuration + configurerTON, err := mcmston.NewConfigurer(s.wallet, amount) + s.Require().NoError(err) + + config := GenSimpleTestMCMSConfig(s.signers) + clearRoot := true + res, err := configurerTON.SetConfig(ctx, s.mcmsAddr, config, clearRoot) + s.Require().NoError(err, "Failed to set contract configuration") + s.Require().NotNil(res) + + tx, ok := res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx.Description) + + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) +} + +func (s *ExecutionTestSuite) newMockEVMInspector(rootMetadata types.ChainMetadata) sdk.Inspector { + return mockEVMInspector{ + config: GenSimpleTestMCMSConfig(s.signers), + opCount: 0, + root: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), + rootMetadata: rootMetadata, + } +} + +// Implements sdk.Inspector +type mockEVMInspector struct { + config *types.Config + opCount uint64 + root common.Hash + rootMetadata types.ChainMetadata +} + +func (i mockEVMInspector) GetConfig(ctx context.Context, mcmAddr string) (*types.Config, error) { + return i.config, nil +} + +func (i mockEVMInspector) GetOpCount(ctx context.Context, mcmAddr string) (uint64, error) { + return i.opCount, nil +} + +func (i mockEVMInspector) GetRoot(ctx context.Context, mcmAddr string) (common.Hash, uint32, error) { + return i.root, 0, nil +} + +func (i mockEVMInspector) GetRootMetadata(ctx context.Context, mcmAddr string) (types.ChainMetadata, error) { + return i.rootMetadata, nil +} diff --git a/e2e/tests/ton/inspection.go b/e2e/tests/ton/inspection.go new file mode 100644 index 00000000..9ade6124 --- /dev/null +++ b/e2e/tests/ton/inspection.go @@ -0,0 +1,130 @@ +//go:build e2e + +package tone2e + +import ( + "strconv" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/suite" + + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton/wallet" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/hash" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tracetracking" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + e2e "github.com/smartcontractkit/mcms/e2e/tests" + "github.com/smartcontractkit/mcms/internal/testutils" + mcmston "github.com/smartcontractkit/mcms/sdk/ton" +) + +// InspectionTestSuite defines the test suite +type InspectionTestSuite struct { + suite.Suite + e2e.TestSetup + + wallet *wallet.Wallet + mcmsAddr string + + signers []testutils.ECDSASigner +} + +// SetupSuite runs before the test suite +func (s *InspectionTestSuite) SetupSuite() { + s.TestSetup = *e2e.InitializeSharedTestSetup(s.T()) + + // Generate few test signers + s.signers = testutils.MakeNewECDSASigners(2) + + var err error + s.wallet, err = tvm.MyLocalTONWalletDefault(s.TonClient) + s.Require().NoError(err) + + s.deployMCMSContract() +} + +func (s *InspectionTestSuite) deployMCMSContract() { + ctx := s.T().Context() + + amount := tlb.MustFromTON("0.3") + chainID, err := strconv.ParseInt(s.TonBlockchain.ChainID, 10, 64) + s.Require().NoError(err) + data := mcms.EmptyDataFrom(hash.CRC32("test.inspection.mcms"), s.wallet.Address(), chainID) + mcmsAddr, err := DeployMCMSContract(ctx, s.TonClient, s.wallet, amount, data) + s.Require().NoError(err) + s.mcmsAddr = mcmsAddr.String() + + // Set configuration + configurerTON, err := mcmston.NewConfigurer(s.wallet, amount) + s.Require().NoError(err) + + config := GenSimpleTestMCMSConfig(s.signers) + clearRoot := true + res, err := configurerTON.SetConfig(ctx, s.mcmsAddr, config, clearRoot) + s.Require().NoError(err, "Failed to set contract configuration") + s.Require().NotNil(res) + + tx, ok := res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx.Description) + + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) +} + +// TestGetConfig checks contract configuration +func (s *InspectionTestSuite) TestGetConfig() { + ctx := s.T().Context() + + inspector := mcmston.NewInspector(s.TonClient) + config, err := inspector.GetConfig(ctx, s.mcmsAddr) + + s.Require().NoError(err, "Failed to get contract configuration") + s.Require().NotNil(config, "Contract configuration is nil") + + // Check first group + s.Require().Equal(uint8(1), config.Quorum, "Quorum does not match") + s.Require().Equal(s.signers[0].Address(), config.Signers[0], "Signers do not match") + + // Check second group + s.Require().Equal(uint8(1), config.GroupSigners[0].Quorum, "Group quorum does not match") + s.Require().Equal(s.signers[1].Address(), config.GroupSigners[0].Signers[0], "Group signers do not match") +} + +// TestGetOpCount checks contract operation count +func (s *InspectionTestSuite) TestGetOpCount() { + ctx := s.T().Context() + + inspector := mcmston.NewInspector(s.TonClient) + opCount, err := inspector.GetOpCount(ctx, s.mcmsAddr) + + s.Require().NoError(err, "Failed to get op count") + s.Require().Equal(uint64(0), opCount, "Operation count does not match") +} + +// TestGetRoot checks contract root +func (s *InspectionTestSuite) TestGetRoot() { + ctx := s.T().Context() + + inspector := mcmston.NewInspector(s.TonClient) + root, validUntil, err := inspector.GetRoot(ctx, s.mcmsAddr) + + s.Require().NoError(err, "Failed to get root from contract") + s.Require().Equal(common.Hash{}, root, "Roots do not match") + s.Require().Equal(uint32(0), validUntil, "ValidUntil does not match") +} + +// TestGetRootMetadata checks contract root metadata +func (s *InspectionTestSuite) TestGetRootMetadata() { + ctx := s.T().Context() + + inspector := mcmston.NewInspector(s.TonClient) + metadata, err := inspector.GetRootMetadata(ctx, s.mcmsAddr) + + s.Require().NoError(err, "Failed to get root metadata from contract") + s.Require().Equal(metadata.MCMAddress, s.mcmsAddr, "MCMAddress does not match") + s.Require().Equal(uint64(0), metadata.StartingOpCount, "StartingOpCount does not match") +} diff --git a/e2e/tests/ton/set_config.go b/e2e/tests/ton/set_config.go new file mode 100644 index 00000000..8e368000 --- /dev/null +++ b/e2e/tests/ton/set_config.go @@ -0,0 +1,235 @@ +//go:build e2e + +package tone2e + +import ( + "strconv" + + "github.com/ethereum/go-ethereum/common" + + "github.com/stretchr/testify/suite" + + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton/wallet" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/hash" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tracetracking" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + e2e "github.com/smartcontractkit/mcms/e2e/tests" + "github.com/smartcontractkit/mcms/internal/testutils" + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/types" + + mcmston "github.com/smartcontractkit/mcms/sdk/ton" +) + +// SetConfigTestSuite tests signing a proposal and converting back to a file +type SetConfigTestSuite struct { + suite.Suite + e2e.TestSetup + + wallet *wallet.Wallet + mcmsAddr string +} + +// SetupSuite runs before the test suite +func (s *SetConfigTestSuite) SetupSuite() { + s.TestSetup = *e2e.InitializeSharedTestSetup(s.T()) + + var err error + s.wallet, err = tvm.MyLocalTONWalletDefault(s.TonClient) + s.Require().NoError(err) + + s.deployMCMSContract() +} + +func (s *SetConfigTestSuite) deployMCMSContract() { + ctx := s.T().Context() + + amount := tlb.MustFromTON("0.3") + chainID, err := strconv.ParseInt(s.TonBlockchain.ChainID, 10, 64) + s.Require().NoError(err) + data := mcms.EmptyDataFrom(hash.CRC32("test.set_config.mcms"), s.wallet.Address(), chainID) + mcmsAddr, err := DeployMCMSContract(ctx, s.TonClient, s.wallet, amount, data) + s.Require().NoError(err) + s.mcmsAddr = mcmsAddr.String() +} + +func (s *SetConfigTestSuite) TestSetConfigInspect() { + // Signers in each group need to be sorted alphabetically + signers := testutils.MakeNewECDSASigners(30) + + amount := tlb.MustFromTON("0.3") + configurerTON, err := mcmston.NewConfigurer(s.wallet, amount) + s.Require().NoError(err) + + inspectorTON := mcmston.NewInspector(s.TonClient) + + tests := []struct { + name string + config types.Config + configurer sdk.Configurer + inspector sdk.Inspector + wantErr error + }{ + { + name: "config small/default", + config: types.Config{ + Quorum: 1, + Signers: []common.Address{signers[0].Address()}, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{signers[1].Address()}, + GroupSigners: []types.Config{}, + }, + }, + }, + configurer: configurerTON, + inspector: inspectorTON, + }, + { + name: "config proposer", + config: types.Config{ + Quorum: 2, + Signers: []common.Address{ + signers[0].Address(), + signers[1].Address(), + signers[2].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 4, + Signers: []common.Address{ + signers[3].Address(), + signers[4].Address(), + signers[5].Address(), + signers[6].Address(), + signers[7].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{ + signers[8].Address(), + signers[9].Address(), + }, + GroupSigners: []types.Config{}, + }, + }, + }, + { + Quorum: 3, + Signers: []common.Address{ + signers[10].Address(), + signers[11].Address(), + signers[12].Address(), + signers[13].Address(), + }, + GroupSigners: []types.Config{}, + }, + }, + }, + configurer: configurerTON, + inspector: inspectorTON, + }, + { + name: "config canceller", + config: types.Config{ + Quorum: 1, + Signers: []common.Address{ + signers[14].Address(), + signers[15].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 2, + Signers: []common.Address{ + signers[16].Address(), + signers[17].Address(), + signers[18].Address(), + signers[19].Address(), + }, + GroupSigners: []types.Config{}, + }, + }, + }, + configurer: configurerTON, + inspector: inspectorTON, + }, + { + name: "config proposer", + config: types.Config{ + Quorum: 2, + Signers: []common.Address{}, + GroupSigners: []types.Config{ + { + Quorum: 2, + Signers: []common.Address{ + signers[20].Address(), + signers[21].Address(), + signers[22].Address(), + signers[23].Address(), + }, + GroupSigners: []types.Config{}, + }, { + Quorum: 2, + Signers: []common.Address{ + signers[24].Address(), + signers[25].Address(), + signers[26].Address(), + signers[27].Address(), + }, + GroupSigners: []types.Config{}, + }, { + Quorum: 1, + Signers: []common.Address{ + signers[28].Address(), + signers[29].Address(), + }, + GroupSigners: []types.Config{}, + }, + }, + }, + configurer: configurerTON, + inspector: inspectorTON, + }, + } + + for _, tt := range tests { + s.Run(tt.name, func() { + ctx := s.T().Context() + // Set config + { + res, err := tt.configurer.SetConfig(ctx, s.mcmsAddr, &tt.config, true) + s.Require().NoError(err, "setting config on MCMS contract") + + s.Require().NotNil(res.Hash) + s.Require().NotNil(res.RawData) + + tx, ok := res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx.Description) + + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) + } + + { + gotCount, err := tt.inspector.GetOpCount(ctx, s.mcmsAddr) + s.Require().NoError(err, "getting config on MCMS contract") + s.Require().Equal(uint64(0), gotCount) + } + + // Assert that config has been set + { + gotConfig, err := tt.inspector.GetConfig(ctx, s.mcmsAddr) + s.Require().NoError(err, "getting config on MCMS contract") + s.Require().NotNil(gotConfig) + s.Require().Equal(&tt.config, gotConfig) + } + }) + } +} diff --git a/e2e/tests/ton/set_root.go b/e2e/tests/ton/set_root.go new file mode 100644 index 00000000..22c8ba01 --- /dev/null +++ b/e2e/tests/ton/set_root.go @@ -0,0 +1,279 @@ +//go:build e2e + +package tone2e + +import ( + "context" + "encoding/json" + "strconv" + + "github.com/stretchr/testify/suite" + + cselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/hash" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tracetracking" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/ton/wallet" + "github.com/xssnick/tonutils-go/tvm/cell" + + mcmslib "github.com/smartcontractkit/mcms" + e2e "github.com/smartcontractkit/mcms/e2e/tests" + "github.com/smartcontractkit/mcms/internal/testutils" + "github.com/smartcontractkit/mcms/internal/testutils/chaintest" + "github.com/smartcontractkit/mcms/sdk" + mcmston "github.com/smartcontractkit/mcms/sdk/ton" + "github.com/smartcontractkit/mcms/types" +) + +const ( + AddrTimelock = "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8" // static mock address +) + +// SetRootTestSuite tests the SetRoot functionality +type SetRootTestSuite struct { + suite.Suite + e2e.TestSetup + + wallet *wallet.Wallet + mcmsAddr string + + signers []testutils.ECDSASigner + chainSelector types.ChainSelector + accounts []*address.Address +} + +// SetupSuite runs before the test suite +func (s *SetRootTestSuite) SetupSuite() { + s.TestSetup = *e2e.InitializeSharedTestSetup(s.T()) + + // Generate few test signers + s.signers = testutils.MakeNewECDSASigners(2) + + // Generate few test wallets + var chainID = chaintest.Chain7TONID + var client *ton.APIClient + s.accounts = []*address.Address{ + must(tvm.NewRandomV5R1TestWallet(client, chainID)).Address(), + must(tvm.NewRandomV5R1TestWallet(client, chainID)).Address(), + } + + var err error + s.wallet, err = tvm.MyLocalTONWalletDefault(s.TonClient) + s.Require().NoError(err) + + s.deployMCMSContract() + + chainDetails, err := cselectors.GetChainDetailsByChainIDAndFamily(s.TonBlockchain.ChainID, s.TonBlockchain.Family) + s.Require().NoError(err) + s.chainSelector = types.ChainSelector(chainDetails.ChainSelector) +} + +func (s *SetRootTestSuite) deployMCMSContract() { + ctx := s.T().Context() + + amount := tlb.MustFromTON("0.3") + chainID, err := strconv.ParseInt(s.TonBlockchain.ChainID, 10, 64) + s.Require().NoError(err) + data := mcms.EmptyDataFrom(hash.CRC32("test.set_root.mcms"), s.wallet.Address(), chainID) + mcmsAddr, err := DeployMCMSContract(ctx, s.TonClient, s.wallet, amount, data) + s.Require().NoError(err) + s.mcmsAddr = mcmsAddr.String() + + // Set configuration + configurerTON, err := mcmston.NewConfigurer(s.wallet, amount) + s.Require().NoError(err) + + config := GenSimpleTestMCMSConfig(s.signers) + clearRoot := true + res, err := configurerTON.SetConfig(ctx, s.mcmsAddr, config, clearRoot) + s.Require().NoError(err, "Failed to set contract configuration") + s.Require().NotNil(res) + + tx, ok := res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx.Description) + + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) +} + +// TestGetConfig checks contract configuration +func (s *SetRootTestSuite) TestGetConfig() { + ctx := s.T().Context() + + inspector := mcmston.NewInspector(s.TonClient) + config, err := inspector.GetConfig(ctx, s.mcmsAddr) + s.Require().NoError(err, "Failed to get contract configuration") + s.Require().NotNil(config, "Contract configuration is nil") + + // Check first group + s.Require().Equal(uint8(1), config.Quorum, "Quorum does not match") + s.Require().Equal(s.signers[0].Address(), config.Signers[0], "Signers do not match") + + // Check second group + s.Require().Equal(uint8(1), config.GroupSigners[0].Quorum, "Group quorum does not match") + s.Require().Equal(s.signers[1].Address(), config.GroupSigners[0].Signers[0], "Group signers do not match") +} + +// TestSetRootProposal sets the root of the MCMS contract +func (s *SetRootTestSuite) TestSetRootProposal() { + ctx := context.Background() + builder := mcmslib.NewProposalBuilder() + builder. + SetVersion("v1"). + SetValidUntil(1794610529). + SetDescription("proposal to test SetRoot"). + SetOverridePreviousRoot(true). + AddChainMetadata( + s.chainSelector, + types.ChainMetadata{MCMAddress: s.mcmsAddr}, + ). + AddOperation(types.Operation{ + ChainSelector: s.chainSelector, + Transaction: types.Transaction{ + To: s.accounts[0].String(), + Data: cell.BeginCell().EndCell().ToBOC(), + AdditionalFields: json.RawMessage(`{"value": 0}`), + }, + }) + proposal, err := builder.Build() + s.Require().NoError(err) + + // Sign the proposal + inspectors := map[types.ChainSelector]sdk.Inspector{ + s.chainSelector: mcmston.NewInspector(s.TonClient), + } + signable, err := mcmslib.NewSignable(proposal, inspectors) + s.Require().NoError(err) + s.Require().NotNil(signable) + _, err = signable.SignAndAppend(mcmslib.NewPrivateKeySigner(s.signers[0].Key)) + s.Require().NoError(err) + + // Validate the signatures + quorumMet, err := signable.ValidateSignatures(ctx) + s.Require().NoError(err) + s.Require().True(quorumMet) + + // Create the chain MCMS proposal executor + encoders, err := proposal.GetEncoders() + s.Require().NoError(err) + encoder := encoders[s.chainSelector].(*mcmston.Encoder) + + executor, err := mcmston.NewExecutor(mcmston.ExecutorOpts{ + Encoder: encoder, + Client: s.TonClient, + Wallet: s.wallet, + Amount: tlb.MustFromTON("0.1"), + }) + s.Require().NoError(err) + executorsMap := map[types.ChainSelector]sdk.Executor{ + s.chainSelector: executor, + } + executable, err := mcmslib.NewExecutable(proposal, executorsMap) + s.Require().NoError(err) + + // Call SetRoot + res, err := executable.SetRoot(ctx, s.chainSelector) + s.Require().NoError(err) + s.Require().NotEmpty(res.Hash) + + tx, ok := res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx) + + // Wait and check success + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) +} + +// TestSetRootTimelockProposal sets the root of the MCMS contract from a timelock proposal type. +func (s *SetRootTestSuite) TestSetRootTimelockProposal() { + ctx := context.Background() + + builder := mcmslib.NewTimelockProposalBuilder() + builder. + SetVersion("v1"). + SetValidUntil(1794610529). + SetDescription("proposal to test SetRoot"). + SetOverridePreviousRoot(true). + SetAction(types.TimelockActionSchedule). + SetDelay(types.MustParseDuration("24h")). + SetTimelockAddresses(map[types.ChainSelector]string{ + s.chainSelector: AddrTimelock, + }). + AddChainMetadata( + s.chainSelector, + types.ChainMetadata{MCMAddress: s.mcmsAddr}, + ). + AddOperation(types.BatchOperation{ + ChainSelector: s.chainSelector, + Transactions: []types.Transaction{ + { + To: s.accounts[0].String(), + Data: cell.BeginCell().MustStoreSlice([]byte{0x01}, 8).EndCell().ToBOC(), + AdditionalFields: json.RawMessage(`{"value": 3}`), + }, + { + To: s.accounts[1].String(), + Data: cell.BeginCell().MustStoreSlice([]byte{0x02}, 8).EndCell().ToBOC(), + AdditionalFields: json.RawMessage(`{"value": 4}`), + }, + }, + }) + proposalTimelock, err := builder.Build() + s.Require().NoError(err) + + proposal, _, err := proposalTimelock.Convert(ctx, map[types.ChainSelector]sdk.TimelockConverter{ + s.chainSelector: mcmston.NewTimelockConverter(mcmston.DefaultSendAmount), + }) + s.Require().NoError(err) + + // Sign proposal + inspectors := map[types.ChainSelector]sdk.Inspector{ + s.chainSelector: mcmston.NewInspector(s.TonClient), + } + signable, err := mcmslib.NewSignable(&proposal, inspectors) + s.Require().NoError(err) + s.Require().NotNil(signable) + _, err = signable.SignAndAppend(mcmslib.NewPrivateKeySigner(s.signers[1].Key)) + s.Require().NoError(err) + + encoders, err := proposal.GetEncoders() + s.Require().NoError(err) + encoder := encoders[s.chainSelector].(*mcmston.Encoder) + + executor, err := mcmston.NewExecutor(mcmston.ExecutorOpts{ + Encoder: encoder, + Client: s.TonClient, + Wallet: s.wallet, + Amount: tlb.MustFromTON("0.1"), + }) + s.Require().NoError(err) + executorsMap := map[types.ChainSelector]sdk.Executor{ + s.chainSelector: executor, + } + + // Notice: no simulation on TON (like on EVM) + + // Create the chain MCMS proposal executor + executable, err := mcmslib.NewExecutable(&proposal, executorsMap) + s.Require().NoError(err) + // Call SetRoot + res, err := executable.SetRoot(ctx, s.chainSelector) + s.Require().NoError(err) + s.Require().NotEmpty(res.Hash) + + tx, ok := res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx) + + // Wait and check success + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) +} diff --git a/e2e/tests/ton/signing.go b/e2e/tests/ton/signing.go new file mode 100644 index 00000000..c15902f6 --- /dev/null +++ b/e2e/tests/ton/signing.go @@ -0,0 +1,12 @@ +//go:build e2e + +package tone2e + +import ( + "github.com/smartcontractkit/mcms/e2e/tests/common" +) + +// SigningTestSuite tests signing a proposal and converting back to a file +type SigningTestSuite struct { + common.SigningTestSuite +} diff --git a/e2e/tests/ton/timelock_inspection.go b/e2e/tests/ton/timelock_inspection.go new file mode 100644 index 00000000..618128f9 --- /dev/null +++ b/e2e/tests/ton/timelock_inspection.go @@ -0,0 +1,419 @@ +//go:build e2e + +package tone2e + +import ( + "bytes" + "encoding/json" + "fmt" + "math/big" + "slices" + + "github.com/stretchr/testify/suite" + + "github.com/ethereum/go-ethereum/common" + + cselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton/wallet" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/lib/access/rbac" + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/timelock" + toncommon "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/common" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/hash" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tlbe" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tracetracking" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + e2e "github.com/smartcontractkit/mcms/e2e/tests" + mcmston "github.com/smartcontractkit/mcms/sdk/ton" + "github.com/smartcontractkit/mcms/types" +) + +// TimelockInspectionTestSuite is a suite of tests for the RBACTimelock contract inspection. +type TimelockInspectionTestSuite struct { + suite.Suite + e2e.TestSetup + + wallet *wallet.Wallet + timelockAddr *address.Address + + accounts []*address.Address +} + +func (s *TimelockInspectionTestSuite) grantRole(role [32]byte, acc *address.Address) { + ctx := s.T().Context() + body, err := tlb.ToCell(rbac.GrantRole{ + QueryID: must(tvm.RandomQueryID()), + + Role: tlbe.NewUint256(new(big.Int).SetBytes(role[:])), + Account: acc, + }) + s.Require().NoError(err) + + msg := &wallet.Message{ + Mode: wallet.PayGasSeparately | wallet.IgnoreErrors, + InternalMessage: &tlb.InternalMessage{ + IHRDisabled: true, + Bounce: true, + DstAddr: s.timelockAddr, + Amount: tlb.MustFromTON("0.12"), + Body: body, + }, + } + + tx, _, err := s.wallet.SendWaitTransaction(ctx, msg) + s.Require().NoError(err) + s.Require().NotNil(tx) + + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) +} + +func (s *TimelockInspectionTestSuite) scheduleBatch(timelockAddr *address.Address, calls []timelock.Call, predecessor, salt common.Hash, delay uint32) { + ctx := s.T().Context() + body, err := tlb.ToCell(timelock.ScheduleBatch{ + QueryID: must(tvm.RandomQueryID()), + + Calls: calls, + Predecessor: tlbe.NewUint256(predecessor.Big()), + Salt: tlbe.NewUint256(salt.Big()), + Delay: delay, + }) + s.Require().NoError(err) + + msg := &wallet.Message{ + Mode: wallet.PayGasSeparately | wallet.IgnoreErrors, + InternalMessage: &tlb.InternalMessage{ + IHRDisabled: true, + Bounce: true, + DstAddr: timelockAddr, + Amount: tlb.MustFromTON("0.3"), + Body: body, + }, + } + + tx, _, err := s.wallet.SendWaitTransaction(ctx, msg) + s.Require().NoError(err) + s.Require().NotNil(tx) + + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) +} + +func (s *TimelockInspectionTestSuite) deployTimelockContract(id uint32) (*address.Address, error) { + ctx := s.T().Context() + amount := tlb.MustFromTON("1.5") // TODO (ton): high gas + + data := timelock.EmptyDataFrom(id) + // When deploying the contract, send the Init message to initialize the Timelock contract + // Admin will get all roles (not required, just for testing) + addrs := []toncommon.AddressWrap{ + {Val: s.wallet.Address()}, + } + + body := timelock.Init{ + QueryID: 0, + MinDelay: 0, + Admin: s.wallet.Address(), + Proposers: addrs, + Executors: addrs, + Cancellers: addrs, + Bypassers: addrs, + ExecutorRoleCheckEnabled: true, + OpFinalizationTimeout: 0, + } + + return DeployTimelockContract(ctx, s.TonClient, s.wallet, amount, data, body) +} + +// SetupSuite runs before the test suite +func (s *TimelockInspectionTestSuite) SetupSuite() { + s.TestSetup = *e2e.InitializeSharedTestSetup(s.T()) + + // Generate few test wallets + chainID := cselectors.TON_LOCALNET.ChainID + client := s.TonClient + s.accounts = []*address.Address{ + must(tvm.NewRandomV5R1TestWallet(client, chainID)).Address(), + must(tvm.NewRandomV5R1TestWallet(client, chainID)).Address(), + } + + // Sort accounts to have deterministic order + slices.SortFunc(s.accounts, func(a, b *address.Address) int { + return bytes.Compare(a.Data(), b.Data()) + }) + + var err error + s.wallet, err = tvm.MyLocalTONWalletDefault(client) + s.Require().NoError(err) + + // Deploy Timelock contract + s.timelockAddr, err = s.deployTimelockContract(hash.CRC32("test.timelock_inspection.timelock")) + s.Require().NoError(err) + + // Grant Some Roles for testing + // Proposers + role := [32]byte(timelock.RoleProposer.Bytes()) + s.Require().NoError(err) + s.grantRole(role, s.accounts[0]) + // Executors + role = [32]byte(timelock.RoleExecutor.Bytes()) + s.Require().NoError(err) + s.grantRole(role, s.accounts[0]) + s.grantRole(role, s.accounts[1]) + + // Bypassers + role = [32]byte(timelock.RoleBypasser.Bytes()) + s.Require().NoError(err) + s.grantRole(role, s.accounts[1]) + + // Cancellers + role = [32]byte(timelock.RoleCanceller.Bytes()) + s.Require().NoError(err) + s.grantRole(role, s.accounts[0]) + s.grantRole(role, s.accounts[1]) +} + +// TestGetProposers gets the list of proposers +func (s *TimelockInspectionTestSuite) TestGetProposers() { + ctx := s.T().Context() + inspector := mcmston.NewTimelockInspector(s.TonClient) + + proposers, err := inspector.GetProposers(ctx, s.timelockAddr.String()) + s.Require().NoError(err) + s.Require().Len(proposers, 2) + s.Require().Equal(s.accounts[0].String(), proposers[0]) +} + +// TestGetExecutors gets the list of executors +func (s *TimelockInspectionTestSuite) TestGetExecutors() { + ctx := s.T().Context() + inspector := mcmston.NewTimelockInspector(s.TonClient) + + executors, err := inspector.GetExecutors(ctx, s.timelockAddr.String()) + s.Require().NoError(err) + s.Require().Len(executors, 3) + s.Require().Equal(s.accounts[0].String(), executors[0]) + s.Require().Equal(s.accounts[1].String(), executors[1]) +} + +// TestGetBypassers gets the list of bypassers +func (s *TimelockInspectionTestSuite) TestGetBypassers() { + ctx := s.T().Context() + inspector := mcmston.NewTimelockInspector(s.TonClient) + + bypassers, err := inspector.GetBypassers(ctx, s.timelockAddr.String()) + s.Require().NoError(err) + s.Require().Len(bypassers, 2) // Ensure lengths match + // Check that all elements of signerAddresses are in proposers + s.Require().Contains(bypassers, s.accounts[1].String()) +} + +// TestGetCancellers gets the list of cancellers +func (s *TimelockInspectionTestSuite) TestGetCancellers() { + ctx := s.T().Context() + inspector := mcmston.NewTimelockInspector(s.TonClient) + + cancellers, err := inspector.GetCancellers(ctx, s.timelockAddr.String()) + s.Require().NoError(err) + s.Require().Len(cancellers, 3) + s.Require().Equal(s.accounts[0].String(), cancellers[0]) + s.Require().Equal(s.accounts[1].String(), cancellers[1]) +} + +// TestIsOperation tests the IsOperation method +func (s *TimelockInspectionTestSuite) TestIsOperation() { + ctx := s.T().Context() + inspector := mcmston.NewTimelockInspector(s.TonClient) + + // Schedule a test operation + calls := []timelock.Call{ + { + Target: s.accounts[0], + Value: tlb.MustFromTON("0.1"), // TON implementation enforces min value per call + Data: cell.BeginCell().EndCell(), + }, + } + delay := 3600 + pred := common.Hash([32]byte{0x0}) + salt := common.Hash([32]byte{0x01}) + s.scheduleBatch(s.timelockAddr, calls, pred, salt, uint32(delay)) + + opID, err := mcmston.HashOperationBatch(calls, pred, salt) + s.Require().NoError(err) + isOP, err := inspector.IsOperation(ctx, s.timelockAddr.String(), opID) + s.Require().NoError(err) + s.Require().True(isOP) +} + +// TestIsOperationPending tests the IsOperationPending method +func (s *TimelockInspectionTestSuite) TestIsOperationPending() { + ctx := s.T().Context() + inspector := mcmston.NewTimelockInspector(s.TonClient) + + // Schedule a test operation + calls := []timelock.Call{ + { + Target: s.accounts[0], + Value: tlb.MustFromTON("0.1"), // TON implementation enforces min value per call + Data: cell.BeginCell().EndCell(), + }, + } + delay := 3600 + salt := common.Hash([32]byte{0x01}) + pred, err := mcmston.HashOperationBatch(calls, [32]byte{0x0}, salt) + s.Require().NoError(err) + s.scheduleBatch(s.timelockAddr, calls, pred, salt, uint32(delay)) + + opID, err := mcmston.HashOperationBatch(calls, pred, salt) + s.Require().NoError(err) + isOP, err := inspector.IsOperationPending(ctx, s.timelockAddr.String(), opID) + s.Require().NoError(err) + s.Require().True(isOP) +} + +// TestIsOperationReady tests the IsOperationReady and IsOperationDone methods +func (s *TimelockInspectionTestSuite) TestIsOperationReady() { + ctx := s.T().Context() + inspector := mcmston.NewTimelockInspector(s.TonClient) + + // Schedule a test operation + calls := []timelock.Call{ + { + Target: s.accounts[0], + Value: tlb.MustFromTON("0.1"), // TON implementation enforces min value per call + Data: cell.BeginCell().EndCell(), + }, + } + delay := 0 + salt := common.Hash([32]byte{0x01}) + pred2, err := mcmston.HashOperationBatch(calls, [32]byte{0x0}, salt) + s.Require().NoError(err) + pred, err := mcmston.HashOperationBatch(calls, pred2, salt) + s.Require().NoError(err) + s.scheduleBatch(s.timelockAddr, calls, pred, salt, uint32(delay)) + + opID, err := mcmston.HashOperationBatch(calls, pred, salt) + s.Require().NoError(err) + isOP, err := inspector.IsOperationReady(ctx, s.timelockAddr.String(), opID) + s.Require().NoError(err) + s.Require().True(isOP) +} + +func (s *TimelockInspectionTestSuite) TestIsOperationDone() { + ctx := s.T().Context() + + // Deploy a new timelock for this test + newTimelockAddr, err := s.deployTimelockContract(hash.CRC32("test.timelock_inspection.timelock.1")) + s.Require().NoError(err) + + // Schedule a test operation + calls := []timelock.Call{ + { + Target: s.accounts[1], + Value: tlb.MustFromTON("0.1"), // TON implementation enforces min value per call + Data: cell.BeginCell().EndCell(), + }, + } + delay := 0 + pred := common.Hash([32]byte{0x0}) + salt := common.Hash([32]byte{0x01}) + + id, err := mcmston.HashOperationBatch(calls, pred, salt) + s.Require().NoError(err) + + s.scheduleBatch(newTimelockAddr, calls, pred, salt, uint32(delay)) + + inspector := mcmston.NewTimelockInspector(s.TonClient) + isOp, err := inspector.IsOperation(ctx, newTimelockAddr.String(), id) + s.Require().NoError(err, "Failed to check if operation exists") + s.Require().True(isOp, "Operation should exist") + + isOpPending, err := inspector.IsOperationPending(ctx, newTimelockAddr.String(), id) + s.Require().NoError(err, "Failed to check if operation is pending") + s.Require().True(isOpPending, "Operation should be pending") + + isOpReady, err := inspector.IsOperationReady(ctx, newTimelockAddr.String(), id) + s.Require().NoError(err, "Failed to check if operation is ready") + s.Require().True(isOpReady, "Operation should be ready") + + isOpDone, err := inspector.IsOperationDone(ctx, newTimelockAddr.String(), id) + s.Require().NoError(err, "Failed to check if operation is done") + s.Require().False(isOpDone, "Operation should not be done yet") + + // Attempt to execute the batch + executor, err := mcmston.NewTimelockExecutor(mcmston.TimelockExecutorOpts{ + Client: s.TonClient, + Wallet: s.wallet, + Amount: tlb.MustFromTON("0.1"), + }) + s.Require().NoError(err, "Failed to create TimelockExecutor") + + bop := types.BatchOperation{ + ChainSelector: types.ChainSelector(cselectors.TON_LOCALNET.Selector), + Transactions: []types.Transaction{ + { + To: s.accounts[1].String(), + Data: cell.BeginCell().EndCell().ToBOC(), + AdditionalFields: json.RawMessage(fmt.Sprintf(`{"value": %d}`, tlb.MustFromTON("0.1").Nano().Uint64())), + }, + }, + } + + // Test same ID + _calls, err := mcmston.ConvertBatchToCalls(bop) + s.Require().NoError(err, "Failed to convert batch to calls") + + _id, err := mcmston.HashOperationBatch(_calls, pred, salt) + s.Require().NoError(err, "Failed to compute operation ID") + s.Require().Equal(id, _id, "Operation IDs do not match") + + res, err := executor.Execute(ctx, bop, newTimelockAddr.String(), pred, salt) + s.Require().NoError(err, "Failed to execute batch") + s.Require().NotEmpty(res.Hash, "Transaction hash is empty") + + // Wait for the transaction to be mined + tx, ok := res.RawData.(*tlb.Transaction) + s.Require().True(ok) + s.Require().NotNil(tx.Description) + + err = tracetracking.WaitForTrace(ctx, s.TonClient, tx) + s.Require().NoError(err) + + // Check the operation (still) exists + isOp, err = inspector.IsOperation(ctx, newTimelockAddr.String(), id) + s.Require().NoError(err, "Failed to check if operation exists") + s.Require().True(isOp, "Operation should exist") + + // Check the operation is NOT pending anymore + isOpPending, err = inspector.IsOperationPending(ctx, newTimelockAddr.String(), id) + s.Require().NoError(err, "Failed to check if operation is pending") + s.Require().False(isOpPending, "Operation should NOT be pending") + + // Check the operation is NOT done + isOpDone, err = inspector.IsOperationDone(ctx, newTimelockAddr.String(), id) + s.Require().NoError(err, "Failed to check if operation is done") + s.Require().False(isOpDone, "Operation should NOT be done (in error state)") + + // Check the operation is in error state (bounced from an uninitialized account) + tonInspector, ok := inspector.(*mcmston.TimelockInspector) + s.Require().True(ok, "Inspector is not of type TimelockInspector") + + isOpError, err := tonInspector.IsOperationError(ctx, newTimelockAddr.String(), id) + s.Require().NoError(err, "Failed to check if operation is in error state") + s.Require().True(isOpError, "Operation should be in error state") +} + +// TestGetMinDelay tests the GetMinDelay method +func (s *TimelockInspectionTestSuite) TestGetMinDelay() { + ctx := s.T().Context() + inspector := mcmston.NewTimelockInspector(s.TonClient) + + delay, err := inspector.GetMinDelay(ctx, s.timelockAddr.String()) + s.Require().NoError(err, "Failed to get min delay") + s.Require().EqualValues(0, delay) +} diff --git a/factory.go b/factory.go index 6e172d56..37cc1cb1 100644 --- a/factory.go +++ b/factory.go @@ -10,6 +10,7 @@ import ( "github.com/smartcontractkit/mcms/sdk/evm" "github.com/smartcontractkit/mcms/sdk/solana" "github.com/smartcontractkit/mcms/sdk/sui" + "github.com/smartcontractkit/mcms/sdk/ton" "github.com/smartcontractkit/mcms/types" ) @@ -52,6 +53,12 @@ func newEncoder( txCount, overridePreviousRoot, ) + case cselectors.FamilyTon: + encoder = ton.NewEncoder( + csel, + txCount, + overridePreviousRoot, + ) } return encoder, nil @@ -78,6 +85,11 @@ func newTimelockConverter(csel types.ChainSelector) (sdk.TimelockConverter, erro case cselectors.FamilySui: return &sui.TimelockConverter{}, nil + case cselectors.FamilyTon: + // Notice: we need to define the send amount from MCMS to Timelock, + // to cover gas fees. We use a static default value here for now. + return ton.NewTimelockConverter(ton.DefaultSendAmount), nil + default: return nil, fmt.Errorf("unsupported chain family %s", family) } diff --git a/factory_test.go b/factory_test.go index 5e57e85a..f29df20a 100644 --- a/factory_test.go +++ b/factory_test.go @@ -14,6 +14,7 @@ import ( "github.com/smartcontractkit/mcms/sdk/evm" "github.com/smartcontractkit/mcms/sdk/solana" "github.com/smartcontractkit/mcms/sdk/sui" + "github.com/smartcontractkit/mcms/sdk/ton" ) func TestNewEncoder(t *testing.T) { @@ -84,6 +85,16 @@ func TestNewEncoder(t *testing.T) { OverridePreviousRoot: false, }, }, + { + name: "success: returns a TON encoder", + giveSelector: chaintest.Chain7Selector, + giveIsSim: false, + want: &ton.Encoder{ + TxCount: giveTxCount, + ChainSelector: chaintest.Chain7Selector, + OverridePreviousRoot: false, + }, + }, { name: "failure: chain not found for selector", giveSelector: chaintest.ChainInvalidSelector, @@ -137,6 +148,11 @@ func TestNewTimelockConverter(t *testing.T) { chainSelector: chaintest.Chain6Selector, want: &sui.TimelockConverter{}, }, + { + name: "success: TON executor", + chainSelector: chaintest.Chain7Selector, + want: ton.NewTimelockConverter(ton.DefaultSendAmount), + }, { name: "failure: unknown selector", chainSelector: types.ChainSelector(123456789), diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..f53bdbbc --- /dev/null +++ b/flake.lock @@ -0,0 +1,147 @@ +{ + "nodes": { + "chainlink-ton": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "nixpkgs-release-25-05": "nixpkgs-release-25-05" + }, + "locked": { + "lastModified": 1766053776, + "narHash": "sha256-oLNPlV7FaBe/63FvPgU+l5MKiU40hOfRCMK5GVsbPBI=", + "owner": "smartcontractkit", + "repo": "chainlink-ton", + "rev": "bacca2f28229622e4084e3395cbe4bedb2160310", + "type": "github" + }, + "original": { + "owner": "smartcontractkit", + "repo": "chainlink-ton", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1761373498, + "narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-release-25-05": { + "locked": { + "lastModified": 1761667842, + "narHash": "sha256-p+6l/f8bbMErsxK3JWBtVds+pF1umiBjiA/wXJX6svE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "94db704ecbf4b0371436ea82fef81fd9dcc092d1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1765779637, + "narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "1306659b587dc277866c7b69eb97e5f07864d8c4", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "chainlink-ton": "chainlink-ton", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..56b68aef --- /dev/null +++ b/flake.nix @@ -0,0 +1,38 @@ +{ + description = "MCMS SDK Flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + + chainlink-ton.url = "github:smartcontractkit/chainlink-ton"; + }; + + outputs = inputs @ { + self, + nixpkgs, + flake-utils, + chainlink-ton, + ... + }: + flake-utils.lib.eachDefaultSystem (system: let + # Import nixpkgs with specific configuration + pkgs = import nixpkgs { + inherit system; + }; + + # The rev (git commit hash) of the current flake + rev = self.rev or self.dirtyRev or "-"; + + pkgsContracts = { + chainlink-ton-contracts = chainlink-ton.packages.${system}.contracts; + }; + in rec { + # Output a set of dev environments (shells) + devShells = { + default = pkgs.callPackage ./shell.nix {inherit pkgs pkgsContracts;}; + }; + + packages = {} // pkgsContracts; + }); +} diff --git a/go.mod b/go.mod index f1556fe5..baa43f88 100644 --- a/go.mod +++ b/go.mod @@ -15,20 +15,22 @@ require ( github.com/google/go-cmp v0.7.0 github.com/joho/godotenv v1.5.1 github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52 - github.com/samber/lo v1.49.1 + github.com/samber/lo v1.52.0 github.com/smartcontractkit/chain-selectors v1.0.85 github.com/smartcontractkit/chainlink-aptos v0.0.0-20251024142440-51f2ad2652a2 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7f8a0f403c3a github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a github.com/smartcontractkit/chainlink-sui v0.0.0-20251104205009-00bd79b81471 github.com/smartcontractkit/chainlink-testing-framework/framework v0.12.1 + github.com/smartcontractkit/chainlink-ton v0.0.0-20251229193709-08e5eefac63c github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e github.com/spf13/cast v1.10.0 github.com/stretchr/testify v1.11.1 + github.com/xssnick/tonutils-go v1.14.1 github.com/zksync-sdk/zksync2-go v1.1.1-0.20250620124214-2c742ee399c6 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.43.0 - golang.org/x/tools v0.37.0 + golang.org/x/crypto v0.45.0 + golang.org/x/tools v0.38.0 gotest.tools/v3 v3.5.2 ) @@ -196,6 +198,7 @@ require ( github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/shirou/gopsutil/v4 v4.25.5 // indirect github.com/shopspring/decimal v1.4.0 // indirect + github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20251024142759-093ed1b4017f // indirect github.com/smartcontractkit/chainlink-common v0.9.6-0.20251003171904-99a82a53b142 // indirect @@ -246,11 +249,11 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.3.1 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect - golang.org/x/net v0.46.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.36.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.12.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect diff --git a/go.sum b/go.sum index efbd7119..a1b32408 100644 --- a/go.sum +++ b/go.sum @@ -606,8 +606,8 @@ github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= -github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -622,6 +622,8 @@ github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFR github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= +github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -646,6 +648,8 @@ github.com/smartcontractkit/chainlink-sui v0.0.0-20251104205009-00bd79b81471 h1: github.com/smartcontractkit/chainlink-sui v0.0.0-20251104205009-00bd79b81471/go.mod h1:VlyZhVw+a93Sk8rVHOIH6tpiXrMzuWLZrjs1eTIExW8= github.com/smartcontractkit/chainlink-testing-framework/framework v0.12.1 h1:Ld3OrOQfLubJ+os0/oau2V6RISgsEdBg+Q002zkgXpQ= github.com/smartcontractkit/chainlink-testing-framework/framework v0.12.1/go.mod h1:r6KXRM1u9ch5KFR2jspkgtyWEC1X+gxPCL8mR63U990= +github.com/smartcontractkit/chainlink-ton v0.0.0-20251229193709-08e5eefac63c h1:bB48mLz7vh9lGE7G7cgtMVtthZvjO9TfzcmqXReuxOU= +github.com/smartcontractkit/chainlink-ton v0.0.0-20251229193709-08e5eefac63c/go.mod h1:w1Xn7qMKvnPYNTdg3nJFk8TiNKfK0/3n3Trl2qO0KL8= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= @@ -716,6 +720,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/xssnick/tonutils-go v1.14.1 h1:zV/iVYl/h3hArS+tPsd9XrSFfGert3r21caMltPSeHg= +github.com/xssnick/tonutils-go v1.14.1/go.mod h1:68xwWjpoGGqiTbLJ0gT63sKu1Z1moCnDLLzA+DKanIg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -819,8 +825,8 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc= golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= @@ -836,8 +842,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -867,8 +873,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -878,8 +884,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -931,10 +937,10 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251208220230-2638a1023523 h1:H52Mhyrc44wBgLTGzq6+0cmuVuF3LURCSXsLMOqfFos= +golang.org/x/telemetry v0.0.0-20251208220230-2638a1023523/go.mod h1:ArQvPJS723nJQietgilmZA+shuB3CZxH1n2iXq9VSfs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -944,8 +950,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -957,8 +963,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -981,8 +987,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/testutils/chaintest/testchain.go b/internal/testutils/chaintest/testchain.go index 210cfd97..0c27fb07 100644 --- a/internal/testutils/chaintest/testchain.go +++ b/internal/testutils/chaintest/testchain.go @@ -31,6 +31,10 @@ var ( Chain6Selector = types.ChainSelector(Chain6RawSelector) Chain6SuiID = cselectors.SUI_TESTNET.ChainID + Chain7RawSelector = cselectors.TON_TESTNET.Selector + Chain7Selector = types.ChainSelector(Chain7RawSelector) + Chain7TONID = cselectors.TON_TESTNET.ChainID + // ChainInvalidSelector is a chain selector that doesn't exist. ChainInvalidSelector = types.ChainSelector(0) ) diff --git a/internal/testutils/signer.go b/internal/testutils/signer.go index a54b8e2a..1750d8bc 100644 --- a/internal/testutils/signer.go +++ b/internal/testutils/signer.go @@ -3,7 +3,6 @@ package testutils import ( "crypto/ecdsa" "slices" - "strings" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -30,7 +29,7 @@ func MakeNewECDSASigners(n int) []ECDSASigner { } // Signers need to be sorted alphabetically slices.SortFunc(signers[:], func(a, b ECDSASigner) int { - return strings.Compare(strings.ToLower(a.Address().Hex()), strings.ToLower(b.Address().Hex())) + return a.Address().Cmp(b.Address()) }) return signers diff --git a/sdk/evm/executor_test.go b/sdk/evm/executor_test.go index 828a8e22..85182114 100644 --- a/sdk/evm/executor_test.go +++ b/sdk/evm/executor_test.go @@ -537,7 +537,7 @@ func TestExecutor_ExecuteOperationRBACTimelockUnderlyingRevert(t *testing.T) { } } -func TestExecutor_SetRoot(t *testing.T) { +func TestExecutorSetRoot(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/sdk/evm/timelock_converter.go b/sdk/evm/timelock_converter.go index 20ec1a23..ced6796f 100644 --- a/sdk/evm/timelock_converter.go +++ b/sdk/evm/timelock_converter.go @@ -3,6 +3,7 @@ package evm import ( "context" "encoding/json" + "fmt" "math/big" "github.com/ethereum/go-ethereum/common" @@ -44,7 +45,7 @@ func (t *TimelockConverter) ConvertBatchToChainOperations( // Unmarshal the additional fields var additionalFields AdditionalFields if err := json.Unmarshal(tx.AdditionalFields, &additionalFields); err != nil { - return []types.Operation{}, common.Hash{}, err + return []types.Operation{}, common.Hash{}, fmt.Errorf("failed to unmarshal EVM additional fields: %w", err) } calls = append(calls, bindings.RBACTimelockCall{ @@ -81,7 +82,7 @@ func (t *TimelockConverter) ConvertBatchToChainOperations( } if err != nil { - return []types.Operation{}, common.Hash{}, err + return []types.Operation{}, common.Hash{}, fmt.Errorf("failed to encode timelock action data: %w", err) } op := types.Operation{ diff --git a/sdk/evm/timelock_converter_test.go b/sdk/evm/timelock_converter_test.go index ed2277d8..30c6199c 100644 --- a/sdk/evm/timelock_converter_test.go +++ b/sdk/evm/timelock_converter_test.go @@ -2,7 +2,6 @@ package evm import ( "context" - "encoding/json" "math/big" "testing" @@ -31,7 +30,7 @@ func TestTimelockConverter_ConvertBatchToChainOperation(t *testing.T) { operation types.TimelockAction predecessor common.Hash salt common.Hash - expectedError error + expectedError string expectedOpType string }{ { @@ -52,7 +51,6 @@ func TestTimelockConverter_ConvertBatchToChainOperation(t *testing.T) { operation: types.TimelockActionSchedule, predecessor: zeroHash, salt: zeroHash, - expectedError: nil, expectedOpType: "RBACTimelock", }, { @@ -73,7 +71,6 @@ func TestTimelockConverter_ConvertBatchToChainOperation(t *testing.T) { operation: types.TimelockActionCancel, predecessor: zeroHash, salt: zeroHash, - expectedError: nil, expectedOpType: "RBACTimelock", }, { @@ -94,7 +91,7 @@ func TestTimelockConverter_ConvertBatchToChainOperation(t *testing.T) { operation: types.TimelockAction("invalid"), predecessor: zeroHash, salt: zeroHash, - expectedError: sdkerrors.NewInvalidTimelockOperationError("invalid"), + expectedError: sdkerrors.NewInvalidTimelockOperationError("invalid").Error(), expectedOpType: "", }, { @@ -112,7 +109,7 @@ func TestTimelockConverter_ConvertBatchToChainOperation(t *testing.T) { operation: types.TimelockActionSchedule, predecessor: zeroHash, salt: zeroHash, - expectedError: &json.SyntaxError{}, + expectedError: "failed to unmarshal EVM additional fields: invalid character 'i' looking for beginning of value", }, } @@ -133,10 +130,9 @@ func TestTimelockConverter_ConvertBatchToChainOperation(t *testing.T) { tc.salt, ) - if tc.expectedError != nil { + if tc.expectedError != "" { require.Error(t, err) - //nolint:testifylint // Allow IsType for error type checking - require.IsType(t, tc.expectedError, err) + require.EqualError(t, err, tc.expectedError) } else { require.NoError(t, err) require.NotEqual(t, common.Hash{}, operationID) diff --git a/sdk/solana/chain_metadata_test.go b/sdk/solana/chain_metadata_test.go index 819e4fb2..e1fbbc14 100644 --- a/sdk/solana/chain_metadata_test.go +++ b/sdk/solana/chain_metadata_test.go @@ -109,7 +109,7 @@ func TestNewChainMetadataFromTimelock(t *testing.T) { } } -func TestAdditionalFieldsMetadata_Validate(t *testing.T) { +func TestAdditionalFields_MetadataValidate(t *testing.T) { t.Parallel() // Create valid public keys for testing. diff --git a/sdk/sui/chain_metadata_test.go b/sdk/sui/chain_metadata_test.go index 67c777d7..480f2489 100644 --- a/sdk/sui/chain_metadata_test.go +++ b/sdk/sui/chain_metadata_test.go @@ -92,7 +92,7 @@ func TestTimelockRole_Constants(t *testing.T) { assert.Equal(t, TimelockRoleProposer, TimelockRole(2)) } -func TestAdditionalFieldsMetadata_JSON(t *testing.T) { +func TestAdditionalFields_MetadataJSON(t *testing.T) { t.Parallel() tests := []struct { @@ -156,7 +156,7 @@ func TestAdditionalFieldsMetadata_JSON(t *testing.T) { } } -func TestAdditionalFieldsMetadata_RoundTrip(t *testing.T) { +func TestAdditionalFields_MetadataRoundTrip(t *testing.T) { t.Parallel() original := AdditionalFieldsMetadata{ @@ -187,7 +187,7 @@ func TestAdditionalFieldsMetadata_RoundTrip(t *testing.T) { assert.Equal(t, original.DeployerStateObj, roundTrip.DeployerStateObj) } -func TestAdditionalFieldsMetadata_Validate(t *testing.T) { +func TestAdditionalFields_MetadataValidate(t *testing.T) { t.Parallel() tests := []struct { diff --git a/sdk/sui/transaction_test.go b/sdk/sui/transaction_test.go index 2a9098d8..7e27406c 100644 --- a/sdk/sui/transaction_test.go +++ b/sdk/sui/transaction_test.go @@ -513,7 +513,7 @@ func TestNewTransactionWithManyStateObj(t *testing.T) { } } -func TestAdditionalFieldsJSONMarshaling(t *testing.T) { +func TestAdditionalFields_JSONMarshaling(t *testing.T) { t.Parallel() tests := []struct { diff --git a/sdk/ton/common.go b/sdk/ton/common.go new file mode 100644 index 00000000..39e9accb --- /dev/null +++ b/sdk/ton/common.go @@ -0,0 +1,62 @@ +package ton + +import ( + "context" + "encoding/hex" + "fmt" + + cselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton/wallet" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/mcms/types" +) + +type TxOpts struct { + Wallet *wallet.Wallet + DstAddr *address.Address + Amount tlb.Coins + Body *cell.Cell + SkipSend bool +} + +func SendTx(ctx context.Context, opts TxOpts) (types.TransactionResult, error) { + if opts.SkipSend { + var tx types.Transaction + tx, err := NewTransaction(opts.DstAddr, opts.Body.ToBuilder().ToSlice(), opts.Amount.Nano(), "", nil) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("error encoding transaction: %w", err) + } + + return types.TransactionResult{ + Hash: "", // Returning no hash since the transaction hasn't been sent yet. + ChainFamily: cselectors.FamilyTon, + RawData: tx, // will be of type types.Transaction + }, nil + } + + msg := &wallet.Message{ + Mode: wallet.PayGasSeparately | wallet.IgnoreErrors, + InternalMessage: &tlb.InternalMessage{ + IHRDisabled: true, + Bounce: true, + DstAddr: opts.DstAddr, + Amount: opts.Amount, + Body: opts.Body, + }, + } + + tx, _, err := opts.Wallet.SendWaitTransaction(ctx, msg) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to send transaction: %w", err) + } + + return types.TransactionResult{ + Hash: hex.EncodeToString(tx.Hash), + ChainFamily: cselectors.FamilyTon, + RawData: tx, // *tlb.Transaction + }, nil +} diff --git a/sdk/ton/common_test.go b/sdk/ton/common_test.go new file mode 100644 index 00000000..8a77b223 --- /dev/null +++ b/sdk/ton/common_test.go @@ -0,0 +1,19 @@ +package ton_test + +import ( + "github.com/smartcontractkit/mcms/internal/testutils" + + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tlbe" +) + +func must[E any](out E, err error) E { + if err != nil { + panic(err) + } + + return out +} + +func AsUint160Addr(s testutils.ECDSASigner) *tlbe.Uint160 { + return tlbe.NewUint160(s.Address().Big()) +} diff --git a/sdk/ton/config_transformer.go b/sdk/ton/config_transformer.go new file mode 100644 index 00000000..69349c2c --- /dev/null +++ b/sdk/ton/config_transformer.go @@ -0,0 +1,206 @@ +package ton + +import ( + "fmt" + "math" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tlbe" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + "github.com/smartcontractkit/mcms/sdk" + + sdkerrors "github.com/smartcontractkit/mcms/sdk/errors" + "github.com/smartcontractkit/mcms/sdk/evm" + "github.com/smartcontractkit/mcms/sdk/evm/bindings" + "github.com/smartcontractkit/mcms/types" +) + +type ConfigTransformer = sdk.ConfigTransformer[mcms.Config, any] + +var _ ConfigTransformer = &configTransformer{} + +type configTransformer struct { + evmTransformer evm.ConfigTransformer +} + +func NewConfigTransformer() ConfigTransformer { return &configTransformer{} } + +// ToChainConfig converts the chain agnostic config to the chain-specific config +func (e *configTransformer) ToChainConfig(cfg types.Config, _ any) (mcms.Config, error) { + groupQuorum, groupParents, signerAddrs, signerGroups, err := evm.ExtractSetConfigInputs(&cfg) + if err != nil { + return mcms.Config{}, fmt.Errorf("unable to extract set config inputs: %w", err) + } + + // Check the length of signerAddresses up-front + if len(signerAddrs) > math.MaxUint8 { + return mcms.Config{}, sdkerrors.NewTooManySignersError(uint64(len(signerAddrs))) + } + + // Figure out the number of groups + var groupMax uint8 + for _, v := range signerGroups { + if v > groupMax { + groupMax = v + } + } + + // Convert to the binding config + signers := make([]mcms.Signer, len(signerAddrs)) + idx := uint8(0) + for i, signerAddr := range signerAddrs { + signers[i] = mcms.Signer{ + Address: tlbe.NewUint160(signerAddr.Big()), // represented as big.Int on TON + Group: signerGroups[i], + Index: idx, + } + idx++ + } + + keySz := uint(tvm.SizeUINT8) + signersDict := cell.NewDict(keySz) + for i, s := range signers { + var sc *cell.Cell + sc, err = tlb.ToCell(s) + if err != nil { + return mcms.Config{}, fmt.Errorf("unable to encode signer %d: %w", i, err) + } + + err = signersDict.SetIntKey(big.NewInt(int64(i)), sc) + if err != nil { + return mcms.Config{}, fmt.Errorf("unable to dict.set signer %d: %w", i, err) + } + } + + sz := uint(tvm.SizeUINT8) + gqDict := cell.NewDict(keySz) + for i, g := range groupQuorum { + //nolint:gosec // G115 conversion safe, max 32 groups + if uint8(i) <= groupMax { // don't set unnecessary groups + v := cell.BeginCell().MustStoreUInt(uint64(g), sz).EndCell() + err = gqDict.SetIntKey(big.NewInt(int64(i)), v) + if err != nil { + return mcms.Config{}, fmt.Errorf("unable to dict.set group quorum %d: %w", i, err) + } + } + } + + gpDict := cell.NewDict(keySz) + for i, g := range groupParents { + //nolint:gosec // G115 conversion safe, max 32 groups + if uint8(i) <= groupMax { // don't set unnecessary groups + v := cell.BeginCell().MustStoreUInt(uint64(g), sz).EndCell() + err = gpDict.SetIntKey(big.NewInt(int64(i)), v) + if err != nil { + return mcms.Config{}, fmt.Errorf("unable to dict.set group parent %d: %w", i, err) + } + } + } + + // TODO (ton): this fn can be optimized to avoid double dict creation + _signersDict, err := tlbe.NewDictFromDictionary[uint8, mcms.Signer](signersDict) + if err != nil { + return mcms.Config{}, fmt.Errorf("unable to create signers dict: %w", err) + } + + _gqDict, err := tlbe.NewDictFromDictionary[uint8, uint8](gqDict) + if err != nil { + return mcms.Config{}, fmt.Errorf("unable to create group quorums dict: %w", err) + } + + _gpDict, err := tlbe.NewDictFromDictionary[uint8, uint8](gpDict) + if err != nil { + return mcms.Config{}, fmt.Errorf("unable to create group parents dict: %w", err) + } + + return mcms.Config{ + Signers: _signersDict, + GroupQuorums: _gqDict, + GroupParents: _gpDict, + }, nil +} + +// ToConfig Maps the chain-specific config to the chain-agnostic config +func (e *configTransformer) ToConfig(config mcms.Config) (*types.Config, error) { + _signers, err := config.Signers.AsDictionary() + if err != nil { + return nil, fmt.Errorf("unable to get signers as Dictionary: %w", err) + } + kvSigners, err := _signers.LoadAll() + if err != nil { + return nil, fmt.Errorf("unable to load signers: %w", err) + } + + // Re-using the EVM implementation here, but need to convert input first + evmConfig := bindings.ManyChainMultiSigConfig{ + Signers: make([]bindings.ManyChainMultiSigSigner, len(kvSigners)), + GroupQuorums: [32]uint8{}, + GroupParents: [32]uint8{}, + } + + for i, kvSigner := range kvSigners { + var signer mcms.Signer + err = tlb.LoadFromCell(&signer, kvSigner.Value) + if err != nil { + return nil, fmt.Errorf("unable to decode signer: %w", err) + } + + addrBytes := make([]byte, common.AddressLength) + signer.Address.Value().FillBytes(addrBytes) // TODO: tvm.KeyUINT160 + + evmConfig.Signers[i] = bindings.ManyChainMultiSigSigner{ + Addr: common.Address(addrBytes), + Index: signer.Index, + Group: signer.Group, + } + } + + _groupQuorums, err := config.GroupQuorums.AsDictionary() + if err != nil { + return nil, fmt.Errorf("unable to get group quorums as Dictionary: %w", err) + } + kvGroupQuorums, err := _groupQuorums.LoadAll() + if err != nil { + return nil, fmt.Errorf("unable to load all group quorums: %w", err) + } + + for i, kvGroupQuorum := range kvGroupQuorums { + var val uint64 + val, err = kvGroupQuorum.Value.LoadUInt(tvm.SizeUINT8) + if err != nil { + return nil, fmt.Errorf("unable to load group quorum value: %w", err) + } + + //nolint:gosec // G115 conversion safe + evmConfig.GroupQuorums[i] = uint8(val) + } + + _groupParents, err := config.GroupParents.AsDictionary() + if err != nil { + return nil, fmt.Errorf("unable to get group parents as Dictionary: %w", err) + } + + kvGroupParents, err := _groupParents.LoadAll() + if err != nil { + return nil, fmt.Errorf("unable to load group parents: %w", err) + } + + for i, kvGroupParent := range kvGroupParents { + var val uint64 + val, err = kvGroupParent.Value.LoadUInt(tvm.SizeUINT8) + if err != nil { + return nil, fmt.Errorf("unable to load group parent value: %w", err) + } + + //nolint:gosec // G115 conversion safe + evmConfig.GroupParents[i] = uint8(val) + } + + return e.evmTransformer.ToConfig(evmConfig) +} diff --git a/sdk/ton/config_transformer_test.go b/sdk/ton/config_transformer_test.go new file mode 100644 index 00000000..91df59b5 --- /dev/null +++ b/sdk/ton/config_transformer_test.go @@ -0,0 +1,428 @@ +package ton_test + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/mcms/internal/testutils" + "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tlbe" + + mcmston "github.com/smartcontractkit/mcms/sdk/ton" +) + +func TestConfigTransformer_ToConfig(t *testing.T) { + t.Parallel() + + signers := testutils.MakeNewECDSASigners(16) + + tests := []struct { + name string + give mcms.Config + want *types.Config + wantErr string + }{ + { + name: "success: converts binding config to config", + give: mcms.Config{ + Signers: must(tlbe.NewDictFromSlice[uint8]([]mcms.Signer{ + {Address: AsUint160Addr(signers[0]), Group: 0, Index: 0}, + {Address: AsUint160Addr(signers[1]), Group: 1, Index: 1}, + })), + GroupQuorums: must(tlbe.NewDictFromSlice[uint8]([]uint8{1, 1})), + GroupParents: must(tlbe.NewDictFromSlice[uint8]([]uint8{0, 0})), + }, + want: &types.Config{ + Quorum: 1, + Signers: []common.Address{signers[0].Address()}, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{signers[1].Address()}, + GroupSigners: []types.Config{}, + }, + }, + }, + }, + { + name: "success: nested configs", + give: mcms.Config{ + GroupQuorums: must(tlbe.NewDictFromSlice[uint8]([]uint8{2, 4, 1, 1, 3, 1})), + GroupParents: must(tlbe.NewDictFromSlice[uint8]([]uint8{0, 0, 1, 2, 0, 4})), + Signers: must(tlbe.NewDictFromSlice[uint8]([]mcms.Signer{ + {Address: AsUint160Addr(signers[0]), Index: 0, Group: 0}, + {Address: AsUint160Addr(signers[1]), Index: 1, Group: 0}, + {Address: AsUint160Addr(signers[2]), Index: 2, Group: 0}, + {Address: AsUint160Addr(signers[3]), Index: 3, Group: 1}, + {Address: AsUint160Addr(signers[4]), Index: 4, Group: 1}, + {Address: AsUint160Addr(signers[5]), Index: 5, Group: 1}, + {Address: AsUint160Addr(signers[6]), Index: 6, Group: 1}, + {Address: AsUint160Addr(signers[7]), Index: 7, Group: 1}, + {Address: AsUint160Addr(signers[8]), Index: 8, Group: 2}, + {Address: AsUint160Addr(signers[9]), Index: 9, Group: 2}, + {Address: AsUint160Addr(signers[10]), Index: 10, Group: 3}, + {Address: AsUint160Addr(signers[11]), Index: 11, Group: 4}, + {Address: AsUint160Addr(signers[12]), Index: 12, Group: 4}, + {Address: AsUint160Addr(signers[13]), Index: 13, Group: 4}, + {Address: AsUint160Addr(signers[14]), Index: 14, Group: 4}, + {Address: AsUint160Addr(signers[15]), Index: 15, Group: 5}, + })), + }, + want: &types.Config{ + Quorum: 2, + Signers: []common.Address{ + signers[0].Address(), + signers[1].Address(), + signers[2].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 4, + Signers: []common.Address{ + signers[3].Address(), + signers[4].Address(), + signers[5].Address(), + signers[6].Address(), + signers[7].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{ + signers[8].Address(), + signers[9].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{ + signers[10].Address(), + }, + GroupSigners: []types.Config{}, + }, + }, + }, + }, + }, + { + Quorum: 3, + Signers: []common.Address{ + signers[11].Address(), + signers[12].Address(), + signers[13].Address(), + signers[14].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{ + signers[15].Address(), + }, + GroupSigners: []types.Config{}, + }, + }, + }, + }, + }, + }, + { + name: "failure: validation error on resulting config", + give: mcms.Config{ + Signers: must(tlbe.NewDictFromSlice[uint8]([]mcms.Signer{ + {Address: AsUint160Addr(signers[0]), Group: 0, Index: 0}, + {Address: AsUint160Addr(signers[1]), Group: 1, Index: 1}, + })), + GroupQuorums: must(tlbe.NewDictFromSlice[uint8]([]uint8{ + 0, // A zero quorum makes this invalid + 1, + })), + GroupParents: must(tlbe.NewDictFromSlice[uint8]([]uint8{0, 0})), + }, + wantErr: "invalid MCMS config: Quorum must be greater than 0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + transformer := mcmston.NewConfigTransformer() + got, err := transformer.ToConfig(tt.give) + + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestSetConfigInputs(t *testing.T) { + t.Parallel() + + signers := testutils.MakeNewECDSASigners(5) + + tests := []struct { + name string + giveConfig types.Config + want mcms.Config + wantErr string + }{ + { + name: "success: root signers with some groups", + giveConfig: types.Config{ + Quorum: 1, + Signers: []common.Address{ + signers[0].Address(), + signers[1].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{signers[2].Address()}, + GroupSigners: []types.Config{}, + }, + }, + }, + want: mcms.Config{ + Signers: must(tlbe.NewDictFromSlice[uint8]([]mcms.Signer{ + {Address: AsUint160Addr(signers[0]), Group: 0, Index: 0}, + {Address: AsUint160Addr(signers[1]), Group: 0, Index: 1}, + {Address: AsUint160Addr(signers[2]), Group: 1, Index: 2}, + })), + GroupQuorums: must(tlbe.NewDictFromSlice[uint8]([]uint8{1, 1})), + GroupParents: must(tlbe.NewDictFromSlice[uint8]([]uint8{0, 0})), + }, + }, + { + name: "success: root signers with some groups and increased quorum", + giveConfig: types.Config{ + Quorum: 2, + Signers: []common.Address{ + signers[0].Address(), + signers[1].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{signers[2].Address()}, + GroupSigners: []types.Config{}, + }, + }, + }, + want: mcms.Config{ + Signers: must(tlbe.NewDictFromSlice[uint8]([]mcms.Signer{ + {Address: AsUint160Addr(signers[0]), Group: 0, Index: 0}, + {Address: AsUint160Addr(signers[1]), Group: 0, Index: 1}, + {Address: AsUint160Addr(signers[2]), Group: 1, Index: 2}, + })), + GroupQuorums: must(tlbe.NewDictFromSlice[uint8]([]uint8{2, 1})), + GroupParents: must(tlbe.NewDictFromSlice[uint8]([]uint8{0, 0})), + }, + }, + { + name: "success: only root signers", + giveConfig: types.Config{ + Quorum: 1, + Signers: []common.Address{ + signers[0].Address(), + signers[1].Address(), + }, + GroupSigners: []types.Config{}, + }, + want: mcms.Config{ + Signers: must(tlbe.NewDictFromSlice[uint8]([]mcms.Signer{ + {Address: AsUint160Addr(signers[0]), Group: 0, Index: 0}, + {Address: AsUint160Addr(signers[1]), Group: 0, Index: 1}, + })), + GroupQuorums: must(tlbe.NewDictFromSlice[uint8]([]uint8{ + 1, + })), + GroupParents: must(tlbe.NewDictFromSlice[uint8]([]uint8{ + 0, + })), + }, + }, + { + name: "success: only groups", + giveConfig: types.Config{ + Quorum: 2, + Signers: []common.Address{}, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{signers[0].Address()}, + GroupSigners: []types.Config{}, + }, + { + Quorum: 1, + Signers: []common.Address{signers[1].Address()}, + GroupSigners: []types.Config{}, + }, + { + Quorum: 1, + Signers: []common.Address{signers[2].Address()}, + GroupSigners: []types.Config{}, + }, + }, + }, + want: mcms.Config{ + Signers: must(tlbe.NewDictFromSlice[uint8]([]mcms.Signer{ + {Address: AsUint160Addr(signers[0]), Group: 1, Index: 0}, + {Address: AsUint160Addr(signers[1]), Group: 2, Index: 1}, + {Address: AsUint160Addr(signers[2]), Group: 3, Index: 2}, + })), + GroupQuorums: must(tlbe.NewDictFromSlice[uint8]([]uint8{ + 2, + 1, + 1, + 1, + })), + GroupParents: must(tlbe.NewDictFromSlice[uint8]([]uint8{ + 0, + 0, + 0, + 0, + })), + }, + }, + { + name: "success: nested signers and groups", + giveConfig: types.Config{ + Quorum: 2, + Signers: []common.Address{ + signers[0].Address(), + signers[1].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{signers[2].Address()}, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{signers[3].Address()}, + GroupSigners: []types.Config{}, + }, + }, + }, + { + Quorum: 1, + Signers: []common.Address{signers[4].Address()}, + GroupSigners: []types.Config{}, + }, + }, + }, + want: mcms.Config{ + Signers: must(tlbe.NewDictFromSlice[uint8]([]mcms.Signer{ + {Address: AsUint160Addr(signers[0]), Group: 0, Index: 0}, + {Address: AsUint160Addr(signers[1]), Group: 0, Index: 1}, + {Address: AsUint160Addr(signers[2]), Group: 1, Index: 2}, + {Address: AsUint160Addr(signers[3]), Group: 2, Index: 3}, + {Address: AsUint160Addr(signers[4]), Group: 3, Index: 4}, + })), + GroupQuorums: must(tlbe.NewDictFromSlice[uint8]([]uint8{ + 2, + 1, + 1, + 1, + })), + GroupParents: must(tlbe.NewDictFromSlice[uint8]([]uint8{ + 0, + 0, + 1, + 0, + })), + }, + }, + { + name: "success: unsorted signers and groups", + giveConfig: types.Config{ + Quorum: 2, + // Root signers are out of order (signer2 is before signer1) + Signers: []common.Address{ + signers[1].Address(), + signers[0].Address(), + }, + // Group signers are out of order (signer5 is before the signer4 group) + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{signers[2].Address()}, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{signers[4].Address()}, + GroupSigners: []types.Config{}, + }, + }, + }, + { + Quorum: 1, + Signers: []common.Address{signers[3].Address()}, + GroupSigners: []types.Config{}, + }, + }, + }, + want: mcms.Config{ + Signers: must(tlbe.NewDictFromSlice[uint8]([]mcms.Signer{ + {Address: AsUint160Addr(signers[0]), Group: 0, Index: 0}, + {Address: AsUint160Addr(signers[1]), Group: 0, Index: 1}, + {Address: AsUint160Addr(signers[2]), Group: 1, Index: 2}, + {Address: AsUint160Addr(signers[3]), Group: 3, Index: 3}, + {Address: AsUint160Addr(signers[4]), Group: 2, Index: 4}, + })), + GroupQuorums: must(tlbe.NewDictFromSlice[uint8]([]uint8{ + 2, + 1, + 1, + 1, + })), + GroupParents: must(tlbe.NewDictFromSlice[uint8]([]uint8{ + 0, + 0, + 1, + 0, + })), + }, + }, + { + name: "failure: signer count cannot exceed 255", + giveConfig: types.Config{ + Quorum: 1, + Signers: make([]common.Address, math.MaxUint8+1), + }, + wantErr: "too many signers: 256 max number is 255", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + transformer := mcmston.NewConfigTransformer() + got, err := transformer.ToChainConfig(tt.giveConfig, nil) + + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + + recovered, err := transformer.ToConfig(got) + require.NoError(t, err) + // Notice: compare .GroupSigners to avoid issues with recovering case unsorted_signers_and_groups + assert.Equal(t, tt.giveConfig.GroupSigners, recovered.GroupSigners) + } + }) + } +} diff --git a/sdk/ton/configurer.go b/sdk/ton/configurer.go new file mode 100644 index 00000000..d49ccc5a --- /dev/null +++ b/sdk/ton/configurer.go @@ -0,0 +1,117 @@ +package ton + +import ( + "context" + "fmt" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tlbe" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/sdk/evm" + "github.com/smartcontractkit/mcms/types" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton/wallet" +) + +var _ sdk.Configurer = &configurer{} + +type configurer struct { + wallet *wallet.Wallet + + // Transaction opts + amount tlb.Coins + + skipSend bool +} + +// NewConfigurer creates a new Configurer for TON chains. +// +// options: +// +// WithDoNotSendInstructionsOnChain: when selected, the Configurer instance will not +// send the TON instructions to the blockchain. +func NewConfigurer(w *wallet.Wallet, amount tlb.Coins, opts ...ConfigurerOption) (sdk.Configurer, error) { + c := configurer{w, amount, false} + + for _, o := range opts { + o(&c) + } + + return c, nil +} + +type ConfigurerOption func(*configurer) + +// WithDoNotSendInstructionsOnChain sets the configurer to not sign and send the configuration transaction +// but rather make it return a prepared MCMS types.Transaction instead. +// If set, the Hash field in the result will be empty. +func WithDoNotSendInstructionsOnChain() ConfigurerOption { + return func(c *configurer) { + c.skipSend = true + } +} + +func (c configurer) SetConfig(ctx context.Context, mcmsAddr string, cfg *types.Config, clearRoot bool) (types.TransactionResult, error) { + // Map to Ton Address type + dstAddr, err := address.ParseAddr(mcmsAddr) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("invalid mcms address: %w", err) + } + + groupQuorum, groupParents, signerAddresses, _signerGroups, err := evm.ExtractSetConfigInputs(cfg) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("unable to extract set config inputs: %w", err) + } + + signerKeys := make([]mcms.SignerAddress, len(signerAddresses)) + for i, addr := range signerAddresses { + signerKeys[i] = mcms.SignerAddress{Val: tlbe.NewUint160(addr.Big())} + } + + signerGroups := make([]mcms.SignerGroup, len(_signerGroups)) + for i, g := range _signerGroups { + signerGroups[i] = mcms.SignerGroup{Val: g} + } + + // Encode SetConfig message + gqDict, err := tlbe.NewDictFromSlice[uint8](groupQuorum[:]) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("unable to create group quorum dict: %w", err) + } + + gpDict, err := tlbe.NewDictFromSlice[uint8](groupParents[:]) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("unable to create group parents dict: %w", err) + } + + qID, err := tvm.RandomQueryID() + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to generate random query ID: %w", err) + } + + body, err := tlb.ToCell(mcms.SetConfig{ + QueryID: qID, + + SignerAddresses: signerKeys, + SignerGroups: signerGroups, + GroupQuorums: gqDict, + GroupParents: gpDict, + + ClearRoot: clearRoot, + }) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to encode SetConfig body: %w", err) + } + + return SendTx(ctx, TxOpts{ + Wallet: c.wallet, + DstAddr: dstAddr, + Amount: c.amount, + Body: body, + SkipSend: c.skipSend, + }) +} diff --git a/sdk/ton/configurer_test.go b/sdk/ton/configurer_test.go new file mode 100644 index 00000000..3c1c6ce2 --- /dev/null +++ b/sdk/ton/configurer_test.go @@ -0,0 +1,195 @@ +package ton_test + +import ( + "context" + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/mcms/internal/testutils" + "github.com/smartcontractkit/mcms/internal/testutils/chaintest" + "github.com/smartcontractkit/mcms/types" + + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton" + + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + mcmston "github.com/smartcontractkit/mcms/sdk/ton" + ton_mocks "github.com/smartcontractkit/mcms/sdk/ton/mocks" +) + +// TestConfigurer_SetConfig tests the SetConfig method of the Configurer. +func TestConfigurer_SetConfig(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + signers := testutils.MakeNewECDSASigners(16) + + tests := []struct { + name string + options []mcmston.ConfigurerOption + mcmAddr string + cfg *types.Config + clearRoot bool + mockSetup func(m *ton_mocks.TonAPI) + want string + wantErr error + }{ + { + name: "success", + options: []mcmston.ConfigurerOption{}, + mcmAddr: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + cfg: &types.Config{ + Quorum: 2, + Signers: []common.Address{ + signers[1].Address(), + signers[2].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{ + signers[3].Address(), + }, + GroupSigners: nil, + }, + }, + }, + clearRoot: true, + mockSetup: func(m *ton_mocks.TonAPI) { + // Mock CurrentMasterchainInfo + m.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + // Mock WaitForBlock + apiw := ton_mocks.NewAPIClientWrapped(t) + apiw.EXPECT().GetAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&tlb.Account{}, nil) + + apiw.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(ton.NewExecutionResult([]any{big.NewInt(5)}), nil) + + m.EXPECT().WaitForBlock(mock.Anything). + Return(apiw) + + m.EXPECT().SendExternalMessageWaitTransaction(mock.Anything, mock.Anything). + Return(&tlb.Transaction{Hash: []byte{1, 2, 3, 4, 14}}, &ton.BlockIDExt{}, []byte{}, nil) + }, + want: "010203040e", + wantErr: nil, + }, + { + name: "success - WithDoNotSendInstructionsOnChain option", + options: []mcmston.ConfigurerOption{ + mcmston.WithDoNotSendInstructionsOnChain(), + }, + mcmAddr: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + cfg: &types.Config{ + Quorum: 2, + Signers: []common.Address{ + signers[1].Address(), + signers[2].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{ + signers[3].Address(), + }, + GroupSigners: nil, + }, + }, + }, + clearRoot: true, + mockSetup: func(m *ton_mocks.TonAPI) { + // No mocks needed as transaction won't be sent + }, + want: "", // Hash is empty when not sending transaction + wantErr: nil, + }, + { + name: "failure - SendTransaction fails", + options: []mcmston.ConfigurerOption{}, + mcmAddr: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + cfg: &types.Config{ + Quorum: 2, + Signers: []common.Address{ + signers[1].Address(), + signers[2].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{ + signers[3].Address(), + }, + GroupSigners: nil, + }, + }, + }, + clearRoot: false, + mockSetup: func(m *ton_mocks.TonAPI) { + // Mock CurrentMasterchainInfo + m.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + // Mock WaitForBlock + apiw := ton_mocks.NewAPIClientWrapped(t) + apiw.EXPECT().GetAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&tlb.Account{}, nil) + + apiw.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(ton.NewExecutionResult([]any{big.NewInt(5)}), nil) + + m.EXPECT().WaitForBlock(mock.Anything). + Return(apiw) + + // Mock SendTransaction to return an error + m.EXPECT().SendExternalMessageWaitTransaction(mock.Anything, mock.Anything). + Return(&tlb.Transaction{Hash: []byte{1, 2, 3, 4, 14}}, &ton.BlockIDExt{}, []byte{}, errors.New("transaction failed")) + }, + want: "", + wantErr: errors.New("transaction failed"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _api := ton_mocks.NewTonAPI(t) + chainID := chaintest.Chain7TONID + walletOperator := must(tvm.NewRandomV5R1TestWallet(_api, chainID)) + + // Apply the mock setup for the ContractDeployBackend + if tt.mockSetup != nil { + tt.mockSetup(_api) + } + + // Create the Configurer instance + configurer, err := mcmston.NewConfigurer(walletOperator, tlb.MustFromTON("0.1"), tt.options...) + require.NoError(t, err) + + // Call SetConfig + tx, err := configurer.SetConfig(ctx, tt.mcmAddr, tt.cfg, tt.clearRoot) + + // Assert the results + if tt.wantErr != nil { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr.Error()) + assert.Empty(t, tx.Hash) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, tx.Hash) + } + }) + } +} diff --git a/sdk/ton/decoded_operation.go b/sdk/ton/decoded_operation.go new file mode 100644 index 00000000..3c07513b --- /dev/null +++ b/sdk/ton/decoded_operation.go @@ -0,0 +1,59 @@ +package ton + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/smartcontractkit/mcms/sdk" +) + +type DecodedOperation struct { + ContractType string + MsgType string + MsgOpcode uint64 + + // Message data + MsgDecoded any // normalized and decoded cell structure + InputKeys []string + InputArgs []any +} + +var _ sdk.DecodedOperation = &DecodedOperation{} + +func NewDecodedOperation(contractType string, msgType string, msgOpcode uint64, msgDecoded any, inputKeys []string, inputArgs []any) (sdk.DecodedOperation, error) { + if len(inputKeys) != len(inputArgs) { + return nil, errors.New("input keys and input args must have the same length") + } + + return &DecodedOperation{contractType, msgType, msgOpcode, msgDecoded, inputKeys, inputArgs}, nil +} + +func (o *DecodedOperation) MethodName() string { + return fmt.Sprintf("%s::%s(0x%x)", o.ContractType, o.MsgType, o.MsgOpcode) +} + +func (o *DecodedOperation) Keys() []string { + return o.InputKeys +} + +func (o *DecodedOperation) Args() []any { + return o.InputArgs +} + +func (o *DecodedOperation) String() (string, string, error) { + // Create a human readable representation of the decoded operation + // by displaying a map of input keys to input values + // e.g. {"key1": "value1", "key2": "value2"} + + // Notice: cell is an encoded tree structure, where args can be nested so we print + // out the full decoded structure here, but we only return the first layer of keys + // and args via Keys() and Args() respective funcs. + + byteMap, err := json.MarshalIndent(o.MsgDecoded, "", " ") + if err != nil { + return "", "", fmt.Errorf("failed to JSON marshal the decoded op: %w", err) + } + + return o.MethodName(), string(byteMap), nil +} diff --git a/sdk/ton/decoded_operation_test.go b/sdk/ton/decoded_operation_test.go new file mode 100644 index 00000000..da4c140a --- /dev/null +++ b/sdk/ton/decoded_operation_test.go @@ -0,0 +1,92 @@ +package ton_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/mcms/sdk/ton" +) + +func TestNewDecodedOperation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + contractType string + msgType string + msgOpcode uint64 + msgDecoded map[string]any + inputKeys []string + inputArgs []any + wantMethod string + wantErr string + }{ + { + name: "success", + contractType: "com.foo.acc", + msgType: "functionName", + msgOpcode: 0x1, + msgDecoded: map[string]any{ + "inputKey1": "inputArg1", + "inputKey2": "inputArg2", + }, + inputKeys: []string{"inputKey1", "inputKey2"}, + inputArgs: []any{"inputArg1", "inputArg2"}, + wantMethod: "com.foo.acc::functionName(0x1)", + }, + { + name: "success with empty input keys and args", + contractType: "com.foo.acc", + msgType: "functionName", + msgOpcode: 0x7362d09c, + msgDecoded: map[string]any{}, + inputKeys: []string{}, + inputArgs: []any{}, + wantMethod: "com.foo.acc::functionName(0x7362d09c)", + }, + { + name: "error with mismatched input keys and args", + contractType: "com.foo.acc", + msgType: "functionName", + msgOpcode: 0x1, + msgDecoded: map[string]any{ + "inputKey1": "inputArg1", + "inputKey2": "inputArg2", + }, + inputKeys: []string{"inputKey1", "inputKey2"}, + inputArgs: []any{"inputArg1"}, + wantMethod: "com.foo.acc::functionName(0x1)", + wantErr: "input keys and input args must have the same length", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := ton.NewDecodedOperation(tt.contractType, tt.msgType, tt.msgOpcode, tt.msgDecoded, tt.inputKeys, tt.inputArgs) + if tt.wantErr != "" { + require.Error(t, err) + assert.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + + // Test member functions + assert.Equal(t, tt.wantMethod, got.MethodName()) + assert.Equal(t, tt.inputKeys, got.Keys()) + assert.Equal(t, tt.inputArgs, got.Args()) + + // Test String() + fn, inputs, err := got.String() + require.NoError(t, err) + assert.Equal(t, tt.wantMethod, fn) + for i := range tt.inputKeys { + assert.Contains(t, inputs, fmt.Sprintf(`"%s": "%v"`, tt.inputKeys[i], tt.inputArgs[i])) + } + } + }) + } +} diff --git a/sdk/ton/decoder.go b/sdk/ton/decoder.go new file mode 100644 index 00000000..5f023d69 --- /dev/null +++ b/sdk/ton/decoder.go @@ -0,0 +1,77 @@ +package ton + +import ( + "fmt" + + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" +) + +type decoder struct { + // Map of contract type to TL-B definitions (type -> opcode -> TL-B struct) + TypeToTLBMap map[string]tvm.TLBMap +} + +var _ sdk.Decoder = &decoder{} + +func NewDecoder(tlbs map[string]tvm.TLBMap) sdk.Decoder { + return &decoder{ + TypeToTLBMap: tlbs, + } +} + +func (d *decoder) Decode(tx types.Transaction, contractInterfaces string) (sdk.DecodedOperation, error) { + contractType := contractInterfaces + tlbs, ok := d.TypeToTLBMap[contractType] + if !ok { + return nil, fmt.Errorf("decoding failed - unknown contract interface: %s", contractType) + } + + datac, err := cell.FromBOC(tx.Data) + if err != nil { + return nil, fmt.Errorf("invalid cell BOC data: %w", err) + } + + // Handle message with no body - empty cell + isEmpty := datac.RefsNum() == 0 && datac.BitsSize() == 0 + if isEmpty { + return NewDecodedOperation(contractType, "", 0, map[string]any{}, []string{}, []any{}) + } + + msgType, msgDecoded, err := codec.DecodeTLBValToJSON(datac, tlbs) + if err != nil { + return nil, fmt.Errorf("error while JSON decoding message (cell) for contract %s: %w", contractType, err) + } + + if msgType == "Cell" || msgType == "" { // on decoder fallback (not decoded) + return nil, fmt.Errorf("failed to decode message for contract %s: %w", contractType, err) + } + + // Extract the input keys and args (tree/map lvl 0) + keys, err := codec.DecodeTLBStructKeys(datac, tlbs) + if err != nil { + return nil, fmt.Errorf("error while (struct) decoding message (cell) for contract %s: %w", contractType, err) + } + inputKeys := make([]string, len(keys)) + inputArgs := make([]any, len(keys)) + + m, ok := msgDecoded.(map[string]any) // JSON normalized + if !ok { + return nil, fmt.Errorf("failed to cast as map %s: %w", contractType, err) + } + + // Notice: sorting keys based on TL-B order (decoded map is unsorted) + for i, k := range keys { + inputKeys[i] = k + inputArgs[i] = m[k] + } + + msgOpcode := uint64(0) // TODO (ton): not exposed currently + + return NewDecodedOperation(contractType, msgType, msgOpcode, msgDecoded, inputKeys, inputArgs) +} diff --git a/sdk/ton/decoder_test.go b/sdk/ton/decoder_test.go new file mode 100644 index 00000000..080e4fd2 --- /dev/null +++ b/sdk/ton/decoder_test.go @@ -0,0 +1,119 @@ +package ton_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "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/lib/access/rbac" + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/timelock" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tlbe" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + "github.com/smartcontractkit/mcms/sdk/ton" + "github.com/smartcontractkit/mcms/types" +) + +// Map of contract type to TL-B definitions (type -> opcode -> TL-B struct) +var typeToTLBMap = map[string]tvm.TLBMap{ + // MCMS contract types + "com.chainlink.ton.lib.access.RBAC": rbac.TLBs, + "com.chainlink.ton.mcms.MCMS": mcms.TLBs, + "com.chainlink.ton.mcms.Timelock": timelock.TLBs, +} + +func TestDecoder(t *testing.T) { + t.Parallel() + + exampleRole := crypto.Keccak256Hash([]byte("EXAMPLE_ROLE")) + // Notice: need to convert to Uint256 (big.Int) + exampleRoleBig := tlbe.NewUint256(exampleRole.Big()) + + // Grant role data + grantRoleData, err := tlb.ToCell(rbac.GrantRole{ + QueryID: 0x1, + Role: exampleRoleBig, + Account: address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8"), + }) + require.NoError(t, err) + + tests := []struct { + name string + give types.Operation + contractInterfaces string + want *ton.DecodedOperation + wantErr string + }{ + { + name: "success - empty message", + give: types.Operation{ + ChainSelector: 1, + Transaction: must(ton.NewTransaction( + address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8"), + cell.BeginCell().ToSlice(), + big.NewInt(0), + "RBACTimelock", + []string{"topUp"}, + )), + }, + contractInterfaces: "com.chainlink.ton.lib.access.RBAC", + want: &ton.DecodedOperation{ + ContractType: "com.chainlink.ton.lib.access.RBAC", + MsgType: "", + MsgDecoded: map[string]any{}, + InputKeys: []string{}, + InputArgs: []any{}, + }, + wantErr: "", + }, + { + name: "success - message with body", + give: types.Operation{ + ChainSelector: 1, + Transaction: must(ton.NewTransaction( + address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8"), + grantRoleData.ToBuilder().ToSlice(), + big.NewInt(0), + "RBACTimelock", + []string{"grantRole"}, + )), + }, + contractInterfaces: "com.chainlink.ton.lib.access.RBAC", + want: &ton.DecodedOperation{ + ContractType: "com.chainlink.ton.lib.access.RBAC", + MsgType: "GrantRole", + MsgDecoded: map[string]any{ + "QueryID": uint64(0x1), + "Role": exampleRoleBig, + "Account": address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8"), + }, + InputKeys: []string{"QueryID", "Role", "Account"}, + InputArgs: []any{uint64(0x1), exampleRoleBig, address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8")}, + }, + wantErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + d := ton.NewDecoder(typeToTLBMap) + got, err := d.Decode(tt.give.Transaction, tt.contractInterfaces) + if tt.wantErr != "" { + require.Error(t, err) + assert.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} diff --git a/sdk/ton/encoder.go b/sdk/ton/encoder.go new file mode 100644 index 00000000..f400ef15 --- /dev/null +++ b/sdk/ton/encoder.go @@ -0,0 +1,229 @@ +package ton + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" + + chain_selectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tlbe" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + "github.com/smartcontractkit/mcms/sdk" + sdkerrors "github.com/smartcontractkit/mcms/sdk/errors" + "github.com/smartcontractkit/mcms/types" +) + +var _ sdk.Encoder = &Encoder{} + +// Implementations of various encoding interfaces for TON MCMS +var _ RootMetadataEncoder[mcms.RootMetadata] = &Encoder{} +var _ OperationEncoder[mcms.Op] = &Encoder{} +var _ ProofEncoder[mcms.Proof] = &Encoder{} +var _ SignaturesEncoder[mcms.Signature] = &Encoder{} + +// TODO: bubble up to sdk, use in evm as well +// Defines encoding from sdk types.ChainMetadata to chain type RootMetadata T +type RootMetadataEncoder[T any] interface { + ToRootMetadata(metadata types.ChainMetadata) (T, error) +} + +// TODO: bubble up to sdk, use in evm as well +// Defines encoding from sdk types.ChainMetadata + types.Operation to chain type Operation T +type OperationEncoder[T any] interface { + ToOperation(opCount uint32, metadata types.ChainMetadata, op types.Operation) (T, error) +} + +// TODO: bubble up to sdk, use in evm as well +// Defines encoding from sdk []common.Hash to chain type Proof []T +type ProofEncoder[T any] interface { + ToProof(p []common.Hash) ([]T, error) +} + +// TODO: bubble up to sdk, use in evm as well +// Defines encoding from sdk []types.Signature to chain type Signature []T +type SignaturesEncoder[T any] interface { + ToSignatures(s []types.Signature, hash common.Hash) ([]T, error) +} + +// Encoder encoding MCMS operations and metadata into hashes. +type Encoder struct { + ChainSelector types.ChainSelector + TxCount uint64 + OverridePreviousRoot bool +} + +func NewEncoder(chainSelector types.ChainSelector, txCount uint64, overridePreviousRoot bool) sdk.Encoder { + return &Encoder{ + ChainSelector: chainSelector, + TxCount: txCount, + OverridePreviousRoot: overridePreviousRoot, + } +} + +func (e *Encoder) HashOperation(opCount uint32, metadata types.ChainMetadata, op types.Operation) (common.Hash, error) { + opBind, err := e.ToOperation(opCount, metadata, op) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to convert operation: %w", err) + } + + opCell, err := tlb.ToCell(opBind) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to encode op: %w", err) + } + + // Hash operation according to TON specs + // @dev we use the standard sha256 (cell) hash function to hash the leaf. + b := cell.BeginCell() + if err := b.StoreBigUInt(new(big.Int).SetBytes(mcms.ManyChainMultiSigDomainSeparatorOp[:]), tvm.SizeUINT256); err != nil { + return common.Hash{}, fmt.Errorf("failed to store domain separator: %w", err) + } + if err := b.StoreRef(opCell); err != nil { + return common.Hash{}, fmt.Errorf("failed to store op cell ref: %w", err) + } + + var hash common.Hash + copy(hash[:], b.EndCell().Hash()[:32]) + + return hash, nil +} + +func (e *Encoder) HashMetadata(metadata types.ChainMetadata) (common.Hash, error) { + rm, err := e.ToRootMetadata(metadata) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to convert to root metadata: %w", err) + } + + // Encode metadata according to TON specs + metaCell, err := tlb.ToCell(rm) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to encode op: %w", err) + } + + // Hash metadata according to TON specs + // @dev we use the standard sha256 (cell) hash function to hash the leaf. + b := cell.BeginCell() + if err := b.StoreBigUInt(new(big.Int).SetBytes(mcms.ManyChainMultiSigDomainSeparatorMetadata[:]), tvm.SizeUINT256); err != nil { + return common.Hash{}, fmt.Errorf("failed to store domain separator: %w", err) + } + if err := b.StoreBuilder(metaCell.ToBuilder()); err != nil { + return common.Hash{}, fmt.Errorf("failed to store metadata bytes: %w", err) + } + + var hash common.Hash + copy(hash[:], b.EndCell().Hash()[:32]) + + return hash, nil +} + +func (e *Encoder) ToOperation(opCount uint32, metadata types.ChainMetadata, op types.Operation) (mcms.Op, error) { + chainID, err := chain_selectors.TonChainIdFromSelector(uint64(e.ChainSelector)) + if err != nil { + return mcms.Op{}, &sdkerrors.InvalidChainIDError{ReceivedChainID: e.ChainSelector} + } + + chainID = fixMyLocalTONChainID(chainID) + + // Unmarshal the AdditionalFields from the operation + var additionalFields AdditionalFields + if err = json.Unmarshal(op.Transaction.AdditionalFields, &additionalFields); err != nil { + return mcms.Op{}, err + } + + // Map to Ton Address types + mcmsAddr, err := address.ParseAddr(metadata.MCMAddress) + if err != nil { + return mcms.Op{}, fmt.Errorf("invalid mcms address: %w", err) + } + + toAddr, err := address.ParseAddr(op.Transaction.To) + if err != nil { + return mcms.Op{}, fmt.Errorf("invalid to address: %w", err) + } + + datac, err := cell.FromBOC(op.Transaction.Data) + if err != nil { + return mcms.Op{}, fmt.Errorf("invalid cell BOC data: %w", err) + } + + return mcms.Op{ + ChainID: new(big.Int).SetInt64(int64(chainID)), + MultiSig: mcmsAddr, + Nonce: uint64(opCount), + To: toAddr, + Data: datac, + Value: tlb.FromNanoTON(additionalFields.Value), + }, nil +} + +func (e *Encoder) ToRootMetadata(metadata types.ChainMetadata) (mcms.RootMetadata, error) { + chainID, err := chain_selectors.TonChainIdFromSelector(uint64(e.ChainSelector)) + if err != nil { + return mcms.RootMetadata{}, &sdkerrors.InvalidChainIDError{ReceivedChainID: e.ChainSelector} + } + + chainID = fixMyLocalTONChainID(chainID) + + // Map to Ton Address type (mcms.address) + mcmsAddr, err := address.ParseAddr(metadata.MCMAddress) + if err != nil { + return mcms.RootMetadata{}, fmt.Errorf("invalid mcms address: %w", err) + } + + return mcms.RootMetadata{ + ChainID: new(big.Int).SetInt64(int64(chainID)), + MultiSig: mcmsAddr, + PreOpCount: metadata.StartingOpCount, + PostOpCount: metadata.StartingOpCount + e.TxCount, + OverridePreviousRoot: e.OverridePreviousRoot, + }, nil +} + +func (e *Encoder) ToProof(p []common.Hash) ([]mcms.Proof, error) { + proofs := make([]mcms.Proof, 0, len(p)) + for _, hash := range p { + proofs = append(proofs, mcms.Proof{Val: tlbe.NewUint256(hash.Big())}) + } + + return proofs, nil +} + +const ( + SignatureVOffset = 27 + SignatureVThreshold = 2 +) + +func (e *Encoder) ToSignatures(ss []types.Signature, hash common.Hash) ([]mcms.Signature, error) { + bindSignatures := make([]mcms.Signature, 0, len(ss)) + for _, s := range ss { + if s.V < SignatureVThreshold { + s.V += SignatureVOffset + } + + bindSignatures = append(bindSignatures, mcms.Signature{ + V: s.V, + R: new(big.Int).SetBytes(s.R.Bytes()), + S: new(big.Int).SetBytes(s.S.Bytes()), + }) + } + + return bindSignatures, nil +} + +// TODO (ton): fix me, GLOBAL_ID -217 for mulocalton is not applied and -1 default is returned on-chain +func fixMyLocalTONChainID(chainID int32) int32 { + if chainID == chain_selectors.TON_LOCALNET.ChainID { + chainID = -1 + //nolint:forbidigo // only used in tests, needs to be fixed properly + fmt.Println("WARNING (fix me): Using TON chainID -1 for localton instead of -217 from GLOBAL_ID") + } + + return chainID +} diff --git a/sdk/ton/encoder_test.go b/sdk/ton/encoder_test.go new file mode 100644 index 00000000..7757975d --- /dev/null +++ b/sdk/ton/encoder_test.go @@ -0,0 +1,269 @@ +package ton_test + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/mcms/internal/testutils/chaintest" + "github.com/smartcontractkit/mcms/sdk/ton" + "github.com/smartcontractkit/mcms/types" + + "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/mcms/mcms" +) + +func TestEncoder_HashOperation(t *testing.T) { + t.Parallel() + + var ( + // Static argument values to HashOperation since they don't affect the test + giveOpCount = uint32(0) + giveMetadata = types.ChainMetadata{ + StartingOpCount: 0, + MCMAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + } + ) + + tests := []struct { + name string + giveOp types.Operation + want string + wantErr string + }{ + { + name: "success: hash operation", + giveOp: types.Operation{ + ChainSelector: chaintest.Chain7Selector, + Transaction: must(ton.NewTransaction( + address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8"), + cell.BeginCell().MustStoreBinarySnake([]byte("data")).ToSlice(), + new(big.Int).SetUint64(1000000000000000000), + "", + []string{}, + )), + }, + want: "0x5f473c83be95f5666ec098506843a1b03878dab0dba84bb5ba9fa52172b87138", + }, + { + name: "failure: cannot unmarshal additional fields", + giveOp: types.Operation{ + ChainSelector: chaintest.Chain7Selector, + Transaction: types.Transaction{ + AdditionalFields: []byte("invalid"), + }, + }, + wantErr: "failed to convert operation: invalid character 'i' looking for beginning of value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + encoder := ton.NewEncoder(chaintest.Chain7Selector, 5, false) + got, err := encoder.HashOperation(giveOpCount, giveMetadata, tt.giveOp) + + if tt.wantErr != "" { + require.Error(t, err) + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got.Hex()) + } + }) + } +} + +func TestEncoder_HashMetadata(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + giveSelector types.ChainSelector + giveMeta types.ChainMetadata + want string + wantErr string + }{ + { + name: "success: hash metadata", + giveSelector: chaintest.Chain7Selector, + giveMeta: types.ChainMetadata{ + StartingOpCount: 0, + MCMAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + }, + want: "0xdc28983fc828ede55ccd16567b1e0b7ae74e28f96c4e6383357c175550d363fb", + // TODO (ton): fix me, the above hash is bc we replace -217 chain id with -1 (mylocalton bug) + // want: "0x4e376893226a88f610e5e741ec88ada4deff4c29b7c0611c7f7c80ce0a847924", + }, + { + name: "failure: could not get TON chain id", + giveSelector: chaintest.ChainInvalidSelector, + giveMeta: types.ChainMetadata{}, + wantErr: "failed to convert to root metadata: invalid chain ID: 0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + encoder := ton.NewEncoder(tt.giveSelector, 1, false) + got, err := encoder.HashMetadata(tt.giveMeta) + + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got.Hex()) + } + }) + } +} + +func TestEncoder_ToOperation(t *testing.T) { + t.Parallel() + + var ( + chainID = int32(-217) + chainSelector = types.ChainSelector(cselectors.TonChainIdToChainSelector()[chainID]) + + // Static argument values to ToGethOperation since they don't affect the test + giveOpCount = uint32(0) + giveMetadata = types.ChainMetadata{ + StartingOpCount: 0, + MCMAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + } + ) + + tests := []struct { + name string + giveSelector types.ChainSelector + giveOp types.Operation + want mcms.Op + wantErr string + }{ + { + name: "success: converts to a TON mcms.Op operations", + giveSelector: chaintest.Chain7Selector, + giveOp: types.Operation{ + ChainSelector: chainSelector, + Transaction: must(ton.NewTransaction( + address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8"), + cell.BeginCell().MustStoreBinarySnake([]byte("data")).ToSlice(), + new(big.Int).SetUint64(1000000000000000000), + "", + []string{}, + )), + }, + want: mcms.Op{ + ChainID: new(big.Int).SetInt64(int64(chaintest.Chain7TONID)), + MultiSig: address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8"), + Nonce: uint64(0), + To: address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8"), + // Notice: we wrap in BOC as it decodes differently to pass the equality test + // - refs: ([]*cell.Cell) + // + refs: ([]*cell.Cell) {} + Data: must(cell.FromBOC(cell.BeginCell().MustStoreBinarySnake([]byte("data")).EndCell().ToBOC())), + Value: tlb.MustFromTON("1000000000"), + }, + }, + { + name: "failure: invalid chain selector", + giveSelector: chaintest.ChainInvalidSelector, + giveOp: types.Operation{}, + wantErr: "invalid chain ID: 0", + }, + { + name: "failure: cannot unmarshal additional fields", + giveSelector: chaintest.Chain7Selector, + giveOp: types.Operation{ + ChainSelector: chainSelector, + Transaction: types.Transaction{ + AdditionalFields: []byte("invalid"), + }, + }, + wantErr: "invalid character 'i' looking for beginning of value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + encoder := ton.NewEncoder(tt.giveSelector, 5, false) + got, err := encoder.(ton.OperationEncoder[mcms.Op]).ToOperation(giveOpCount, giveMetadata, tt.giveOp) + + if tt.wantErr != "" { + require.Error(t, err) + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestEncoder_ToRootMetadata(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + giveSelector types.ChainSelector + giveMetadata types.ChainMetadata + want mcms.RootMetadata + wantErr string + }{ + { + name: "success: converts to a geth root metadata", + giveSelector: chaintest.Chain7Selector, + giveMetadata: types.ChainMetadata{ + StartingOpCount: 0, + MCMAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + }, + want: mcms.RootMetadata{ + ChainID: new(big.Int).SetInt64(int64(chaintest.Chain7TONID)), + MultiSig: address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8"), + PreOpCount: uint64(0), + PostOpCount: uint64(5), + OverridePreviousRoot: false, + }, + }, + { + name: "faiure: invalid chain selector", + giveSelector: chaintest.ChainInvalidSelector, + giveMetadata: types.ChainMetadata{}, + wantErr: "invalid chain ID: 0", + }, + { + name: "faiure: invalid mcms address", + giveSelector: chaintest.Chain7Selector, + giveMetadata: types.ChainMetadata{ + StartingOpCount: 0, + MCMAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-", // invalid address + }, + wantErr: "invalid mcms address: incorrect address data", + }, + } + + txCount := uint64(5) + for _, tt := range tests { + encoder := ton.NewEncoder(tt.giveSelector, txCount, false) + got, err := encoder.(ton.RootMetadataEncoder[mcms.RootMetadata]).ToRootMetadata(tt.giveMetadata) + + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + } +} diff --git a/sdk/ton/executor.go b/sdk/ton/executor.go new file mode 100644 index 00000000..b88be218 --- /dev/null +++ b/sdk/ton/executor.go @@ -0,0 +1,199 @@ +package ton + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/samber/lo" + + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/types" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/ton/wallet" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tlbe" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" +) + +// sdk.Executor implementation for TON chains, allowing for the execution of operations on the MCMS contract +type executor struct { + sdk.Encoder + sdk.Inspector + + wallet *wallet.Wallet + + // Transaction opts + amount tlb.Coins +} + +type ExecutorOpts struct { + Encoder sdk.Encoder + Client ton.APIClientWrapped + Wallet *wallet.Wallet + Amount tlb.Coins +} + +// NewExecutor creates a new Executor for TON chains +func NewExecutor(opts ExecutorOpts) (sdk.Executor, error) { + if lo.IsNil(opts.Encoder) { + return nil, errors.New("failed to create sdk.Executor - encoder (sdk.Encoder) is nil") + } + + if lo.IsNil(opts.Client) { + return nil, errors.New("failed to create sdk.Executor - client (ton.APIClientWrapped) is nil") + } + + if opts.Wallet == nil { + return nil, errors.New("failed to create sdk.Executor - wallet (*wallet.Wallet) is nil") + } + + return &executor{ + Encoder: opts.Encoder, + Inspector: NewInspector(opts.Client), + wallet: opts.Wallet, + amount: opts.Amount, + }, nil +} + +func (e *executor) ExecuteOperation( + ctx context.Context, + metadata types.ChainMetadata, + nonce uint32, + proof []common.Hash, + op types.Operation, +) (types.TransactionResult, error) { + oe, ok := e.Encoder.(OperationEncoder[mcms.Op]) + if !ok { + return types.TransactionResult{}, errors.New("failed to assert OperationEncoder") + } + + bindOp, err := oe.ToOperation(nonce, metadata, op) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to convert to operation: %w", err) + } + + // Encode proofs + pe, ok := e.Encoder.(ProofEncoder[mcms.Proof]) + if !ok { + return types.TransactionResult{}, errors.New("failed to assert ProofEncoder") + } + + bindProof, err := pe.ToProof(proof) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to encode proof: %w", err) + } + + qID, err := tvm.RandomQueryID() + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to generate random query ID: %w", err) + } + + body, err := tlb.ToCell(mcms.Execute{ + QueryID: qID, + + Op: bindOp, + Proof: bindProof, + }) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to encode ExecuteBatch body: %w", err) + } + + // Map to Ton Address type + dstAddr, err := address.ParseAddr(metadata.MCMAddress) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("invalid mcms address: %w", err) + } + + skipSend := false // TODO: expose via executor options + + return SendTx(ctx, TxOpts{ + Wallet: e.wallet, + DstAddr: dstAddr, + Amount: e.amount, + Body: body, + SkipSend: skipSend, + }) +} + +func (e *executor) SetRoot( + ctx context.Context, + metadata types.ChainMetadata, + proof []common.Hash, + root [32]byte, + validUntil uint32, + sortedSignatures []types.Signature, +) (types.TransactionResult, error) { + rme, ok := e.Encoder.(RootMetadataEncoder[mcms.RootMetadata]) + if !ok { + return types.TransactionResult{}, errors.New("failed to assert RootMetadataEncoder") + } + + rm, err := rme.ToRootMetadata(metadata) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to convert to root metadata: %w", err) + } + + // Map to Ton Address type + dstAddr, err := address.ParseAddr(metadata.MCMAddress) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("invalid mcms address: %w", err) + } + + // Encode proofs + pe, ok := e.Encoder.(ProofEncoder[mcms.Proof]) + if !ok { + return types.TransactionResult{}, errors.New("failed to assert ProofEncoder") + } + + bindProof, err := pe.ToProof(proof) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to encode proof: %w", err) + } + + // Encode signatures + se, ok := e.Encoder.(SignaturesEncoder[mcms.Signature]) + if !ok { + return types.TransactionResult{}, errors.New("failed to assert SignatureEncoder") + } + + bindSignatures, err := se.ToSignatures(sortedSignatures, root) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to encode signatures: %w", err) + } + + qID, err := tvm.RandomQueryID() + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to generate random query ID: %w", err) + } + + body, err := tlb.ToCell(mcms.SetRoot{ + QueryID: qID, + + Root: tlbe.NewUint256(new(big.Int).SetBytes(root[:])), + ValidUntil: validUntil, + Metadata: rm, + + MetadataProof: bindProof, + Signatures: bindSignatures, + }) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to encode ExecuteBatch body: %w", err) + } + + skipSend := false // TODO: expose via executor options + + return SendTx(ctx, TxOpts{ + Wallet: e.wallet, + DstAddr: dstAddr, + Amount: e.amount, + Body: body, + SkipSend: skipSend, + }) +} diff --git a/sdk/ton/executor_test.go b/sdk/ton/executor_test.go new file mode 100644 index 00000000..221c9288 --- /dev/null +++ b/sdk/ton/executor_test.go @@ -0,0 +1,425 @@ +package ton_test + +import ( + "context" + "encoding/json" + "errors" + "math/big" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + "github.com/smartcontractkit/mcms/internal/testutils/chaintest" + "github.com/smartcontractkit/mcms/types" + + mcmston "github.com/smartcontractkit/mcms/sdk/ton" + + ton_mocks "github.com/smartcontractkit/mcms/sdk/ton/mocks" +) + +func TestExecutor_NewExecutor(t *testing.T) { + t.Parallel() + + amount := tlb.MustFromTON("0.1") + chainID := chaintest.Chain7TONID + + tests := []struct { + name string + mutate func(opts mcmston.ExecutorOpts) mcmston.ExecutorOpts + wantErr string + }{ + { + name: "success", + mutate: func(opts mcmston.ExecutorOpts) mcmston.ExecutorOpts { + return opts + }, + wantErr: "", + }, + { + name: "nil encoder", + mutate: func(opts mcmston.ExecutorOpts) mcmston.ExecutorOpts { + opts.Encoder = nil + + return opts + }, + wantErr: "failed to create sdk.Executor - encoder (sdk.Encoder) is nil", + }, + { + name: "nil client", + mutate: func(opts mcmston.ExecutorOpts) mcmston.ExecutorOpts { + opts.Client = nil + + return opts + }, + wantErr: "failed to create sdk.Executor - client (ton.APIClientWrapped) is nil", + }, + { + name: "nil wallet", + mutate: func(opts mcmston.ExecutorOpts) mcmston.ExecutorOpts { + opts.Wallet = nil + + return opts + }, + wantErr: "failed to create sdk.Executor - wallet (*wallet.Wallet) is nil", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _api := ton_mocks.NewTonAPI(t) + walletOperator := must(tvm.NewRandomV5R1TestWallet(_api, chainID)) + var client ton.APIClientWrapped = ton_mocks.NewAPIClientWrapped(t) + var encoder = mcmston.NewEncoder(chaintest.Chain7Selector, 0, false) + + opts := tt.mutate(mcmston.ExecutorOpts{ + Encoder: encoder, + Client: client, + Wallet: walletOperator, + Amount: amount, + }) + + exec, err := mcmston.NewExecutor(opts) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + require.Nil(t, exec) + + return + } + + require.NoError(t, err) + require.NotNil(t, exec) + }) + } +} + +func TestExecutor_ExecuteOperation(t *testing.T) { + t.Parallel() + + ctx := context.Background() + tests := []struct { + name string + encoder *mcmston.Encoder + metadata types.ChainMetadata + nonce uint32 + proof []common.Hash + op types.Operation + mockSetup func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) + wantTxHash string + wantErrNew error + wantErr error + }{ + { + name: "success", + encoder: &mcmston.Encoder{ + ChainSelector: chaintest.Chain7Selector, + }, + metadata: types.ChainMetadata{ + MCMAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + }, + nonce: 1, + op: types.Operation{ + ChainSelector: chaintest.Chain7Selector, + Transaction: types.Transaction{ + To: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + Data: cell.BeginCell().MustStoreBinarySnake([]byte{1, 2, 3}).EndCell().ToBOC(), + AdditionalFields: json.RawMessage(`{"value": 0}`)}, + }, + mockSetup: func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) { + // Mock CurrentMasterchainInfo + api.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + // Mock WaitForBlock + client.EXPECT().GetAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&tlb.Account{}, nil) + + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(ton.NewExecutionResult([]any{big.NewInt(5)}), nil) + + api.EXPECT().WaitForBlock(mock.Anything). + Return(client) + + // Mock SendTransaction to return an error + api.EXPECT().SendExternalMessageWaitTransaction(mock.Anything, mock.Anything). + Return(&tlb.Transaction{Hash: []byte{1, 2, 3, 4, 14}}, &ton.BlockIDExt{}, []byte{}, nil) + }, + wantTxHash: "010203040e", + wantErr: nil, + }, + { + name: "failure in tx execution", + encoder: &mcmston.Encoder{ + ChainSelector: chaintest.Chain7Selector, + }, + metadata: types.ChainMetadata{ + MCMAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + }, + nonce: 1, + op: types.Operation{ + ChainSelector: chaintest.Chain7Selector, + Transaction: types.Transaction{ + To: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + Data: cell.BeginCell().MustStoreBinarySnake([]byte{1, 2, 3}).EndCell().ToBOC(), + AdditionalFields: json.RawMessage(`{"value": 0}`)}, + }, + mockSetup: func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) { + // Mock CurrentMasterchainInfo + api.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + // Mock WaitForBlock + client.EXPECT().GetAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&tlb.Account{}, nil) + + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(ton.NewExecutionResult([]any{big.NewInt(5)}), nil) + + api.EXPECT().WaitForBlock(mock.Anything). + Return(client) + + // Mock SendTransaction to return an error + api.EXPECT().SendExternalMessageWaitTransaction(mock.Anything, mock.Anything). + Return(&tlb.Transaction{Hash: []byte{1, 2, 3, 4, 14}}, &ton.BlockIDExt{}, []byte{}, errors.New("error during tx send")) + }, + wantTxHash: "", + wantErr: errors.New("failed to send transaction: error during tx send"), + }, + { + name: "failure - nil encoder", + encoder: nil, + mockSetup: func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) {}, + wantTxHash: "", + wantErrNew: errors.New("failed to create sdk.Executor - encoder (sdk.Encoder) is nil"), + }, + { + name: "failure in operation conversion due to invalid chain ID", + encoder: &mcmston.Encoder{ + ChainSelector: types.ChainSelector(1), + }, + op: types.Operation{ + ChainSelector: types.ChainSelector(1), + Transaction: types.Transaction{ + To: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + Data: cell.BeginCell().MustStoreBinarySnake([]byte{1, 2, 3}).EndCell().ToBOC(), + AdditionalFields: json.RawMessage(`{"value": 0}`)}, + }, + mockSetup: func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) {}, + wantTxHash: "", + wantErr: errors.New("failed to convert to operation: invalid chain ID: 1"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Initialize the mock + chainID := chaintest.Chain7TONID + _api := ton_mocks.NewTonAPI(t) + walletOperator := must(tvm.NewRandomV5R1TestWallet(_api, chainID)) + + client := ton_mocks.NewAPIClientWrapped(t) + + if tt.mockSetup != nil { + tt.mockSetup(_api, client) + } + + executor, err := mcmston.NewExecutor(mcmston.ExecutorOpts{ + Encoder: tt.encoder, + Client: client, + Wallet: walletOperator, + Amount: tlb.MustFromTON("0.1"), + }) + if tt.wantErrNew != nil { + require.EqualError(t, err, tt.wantErrNew.Error()) + return + } + require.NoError(t, err) + + tx, err := executor.ExecuteOperation(ctx, tt.metadata, tt.nonce, tt.proof, tt.op) + + if tt.wantErr != nil { + require.EqualError(t, err, tt.wantErr.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tt.wantTxHash, tx.Hash) + } + }) + } +} + +func TestExecutor_SetRoot(t *testing.T) { + t.Parallel() + + ctx := context.Background() + tests := []struct { + name string + encoder *mcmston.Encoder + metadata types.ChainMetadata + proof []common.Hash + root [32]byte + validUntil uint32 + sortedSignatures []types.Signature + mockSetup func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) + wantTxHash string + wantErrNew error + wantErr error + }{ + { + name: "success", + encoder: &mcmston.Encoder{ + ChainSelector: chaintest.Chain7Selector, + }, + metadata: types.ChainMetadata{ + MCMAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + }, + root: [32]byte{1, 2, 3}, + validUntil: 4130013354, + sortedSignatures: []types.Signature{ + makeTestSignature("0xabcdef1234567890"), + makeTestSignature("0xabcdef1234567890"), + }, + mockSetup: func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) { + // Mock CurrentMasterchainInfo + api.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + // Mock WaitForBlock + client.EXPECT().GetAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&tlb.Account{}, nil) + + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(ton.NewExecutionResult([]any{big.NewInt(5)}), nil) + + api.EXPECT().WaitForBlock(mock.Anything). + Return(client) + + // Mock SendTransaction to return an error + api.EXPECT().SendExternalMessageWaitTransaction(mock.Anything, mock.Anything). + Return(&tlb.Transaction{Hash: []byte{1, 2, 3, 4, 14}}, &ton.BlockIDExt{}, []byte{}, nil) + }, + wantTxHash: "010203040e", + wantErr: nil, + }, + { + name: "failure in tx send", + encoder: &mcmston.Encoder{ + ChainSelector: chaintest.Chain7Selector, + }, + metadata: types.ChainMetadata{ + MCMAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + }, + root: [32]byte{1, 2, 3}, + validUntil: 4130013354, + sortedSignatures: []types.Signature{ // TODO: "failed to encode signatures: failed to recover public key: recovery failed" + makeTestSignature("0xabcdef1234567890"), + makeTestSignature("0xabcdef1234567890"), + }, + mockSetup: func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) { + // Mock CurrentMasterchainInfo + api.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + // Mock WaitForBlock + client.EXPECT().GetAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&tlb.Account{}, nil) + + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(ton.NewExecutionResult([]any{big.NewInt(5)}), nil) + + api.EXPECT().WaitForBlock(mock.Anything). + Return(client) + + // Mock SendTransaction to return an error + api.EXPECT().SendExternalMessageWaitTransaction(mock.Anything, mock.Anything). + Return(&tlb.Transaction{Hash: []byte{1, 2, 3, 4, 14}}, &ton.BlockIDExt{}, []byte{}, errors.New("error during tx send")) + }, + wantTxHash: "", + wantErr: errors.New("failed to send transaction: error during tx send"), + }, + { + name: "failure - nil encoder", + encoder: nil, + mockSetup: func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) {}, + wantTxHash: "", + wantErrNew: errors.New("failed to create sdk.Executor - encoder (sdk.Encoder) is nil"), + }, + { + name: "failure in operation conversion due to invalid chain ID", + encoder: &mcmston.Encoder{ + ChainSelector: types.ChainSelector(1), + }, + mockSetup: func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) {}, + wantTxHash: "", + wantErr: errors.New("failed to convert to root metadata: invalid chain ID: 1"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Initialize the mock + chainID := chaintest.Chain7TONID + _api := ton_mocks.NewTonAPI(t) + walletOperator := must(tvm.NewRandomV5R1TestWallet(_api, chainID)) + + client := ton_mocks.NewAPIClientWrapped(t) + + if tt.mockSetup != nil { + tt.mockSetup(_api, client) + } + + executor, err := mcmston.NewExecutor(mcmston.ExecutorOpts{ + Encoder: tt.encoder, + Client: client, + Wallet: walletOperator, + Amount: tlb.MustFromTON("0.1"), + }) + if tt.wantErrNew != nil { + require.EqualError(t, err, tt.wantErrNew.Error()) + return + } + require.NoError(t, err) + + tx, err := executor.SetRoot(ctx, tt.metadata, + tt.proof, + tt.root, + tt.validUntil, + tt.sortedSignatures) + + require.Equal(t, tt.wantTxHash, tx.Hash) + if tt.wantErr != nil { + require.EqualError(t, err, tt.wantErr.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func makeTestSignature(hexStr string) types.Signature { + // Private key to use for signing + pk, _ := crypto.GenerateKey() + + // Hash to sign + hash := common.HexToHash(hexStr) + sigBytes, _ := crypto.Sign(hash.Bytes(), pk) + + // Signature object for the hash + sig, _ := types.NewSignatureFromBytes(sigBytes) + + return sig +} diff --git a/sdk/ton/inspector.go b/sdk/ton/inspector.go new file mode 100644 index 00000000..f4a1550e --- /dev/null +++ b/sdk/ton/inspector.go @@ -0,0 +1,104 @@ +package ton + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/ton" + + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" +) + +var _ sdk.Inspector = (*Inspector)(nil) + +// Inspector is an interface for inspecting on chain state of MCMS contracts. +type Inspector struct { + client ton.APIClientWrapped + + configTransformer ConfigTransformer +} + +// NewInspector creates a new Inspector for TON chains +func NewInspector(client ton.APIClientWrapped) sdk.Inspector { + return &Inspector{ + client: client, + configTransformer: NewConfigTransformer(), + } +} + +// ParseAddrGetBlock parses the given address string into a TON address and retrieves the current masterchain block info. +func ParseAddrGetBlock(ctx context.Context, client ton.APIClientWrapped, _address string) (*address.Address, *ton.BlockIDExt, error) { + // Map to Ton Address type (mcms.address) + addr, err := address.ParseAddr(_address) + if err != nil { + return nil, &ton.BlockIDExt{}, fmt.Errorf("invalid address: %w", err) + } + + block, err := client.CurrentMasterchainInfo(ctx) + if err != nil { + return nil, &ton.BlockIDExt{}, fmt.Errorf("failed to get current masterchain info: %w", err) + } + + return addr, block, nil +} + +func (i Inspector) GetConfig(ctx context.Context, _address string) (*types.Config, error) { + addr, block, err := ParseAddrGetBlock(ctx, i.client, _address) + if err != nil { + return nil, err + } + + _config, err := tvm.CallGetter(ctx, i.client, block, addr, mcms.GetConfig) + if err != nil { + return nil, err + } + + return i.configTransformer.ToConfig(_config) +} + +func (i Inspector) GetOpCount(ctx context.Context, _address string) (uint64, error) { + addr, block, err := ParseAddrGetBlock(ctx, i.client, _address) + if err != nil { + return 0, err + } + + return tvm.CallGetter(ctx, i.client, block, addr, mcms.GetOpCount) +} + +func (i Inspector) GetRoot(ctx context.Context, _address string) (common.Hash, uint32, error) { + addr, block, err := ParseAddrGetBlock(ctx, i.client, _address) + if err != nil { + return [32]byte{}, 0, err + } + + r, err := tvm.CallGetter(ctx, i.client, block, addr, mcms.GetRoot) + if err != nil { + return [32]byte{}, 0, err + } + + return common.BigToHash(r.Root), r.ValidUntil, nil +} + +func (i Inspector) GetRootMetadata(ctx context.Context, _address string) (types.ChainMetadata, error) { + addr, block, err := ParseAddrGetBlock(ctx, i.client, _address) + if err != nil { + return types.ChainMetadata{}, err + } + + rm, err := tvm.CallGetter(ctx, i.client, block, addr, mcms.GetRootMetadata) + if err != nil { + return types.ChainMetadata{}, err + } + + return types.ChainMetadata{ + StartingOpCount: rm.PreOpCount, + MCMAddress: _address, + }, nil +} diff --git a/sdk/ton/inspector_test.go b/sdk/ton/inspector_test.go new file mode 100644 index 00000000..a115d3ac --- /dev/null +++ b/sdk/ton/inspector_test.go @@ -0,0 +1,386 @@ +package ton_test + +import ( + "context" + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/mcms" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tlbe" + + "github.com/smartcontractkit/mcms/internal/testutils" + "github.com/smartcontractkit/mcms/types" + + mcmston "github.com/smartcontractkit/mcms/sdk/ton" + ton_mocks "github.com/smartcontractkit/mcms/sdk/ton/mocks" +) + +func TestInspector_GetConfig(t *testing.T) { + t.Parallel() + + signers := testutils.MakeNewECDSASigners(8) + + ctx := context.Background() + tests := []struct { + name string + address string + mockResult mcms.Config + mockError error + want *types.Config + wantErr error + }{ + { + name: "getConfig call success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockResult: mcms.Config{ + Signers: must(tlbe.NewDictFromSlice[uint8]([]mcms.Signer{ + {Address: AsUint160Addr(signers[0]), Index: 0, Group: 0}, + {Address: AsUint160Addr(signers[1]), Index: 1, Group: 0}, + {Address: AsUint160Addr(signers[2]), Index: 2, Group: 0}, + {Address: AsUint160Addr(signers[3]), Index: 0, Group: 1}, + {Address: AsUint160Addr(signers[4]), Index: 1, Group: 1}, + {Address: AsUint160Addr(signers[5]), Index: 2, Group: 1}, + })), + GroupQuorums: must(tlbe.NewDictFromSlice[uint8]([]uint8{ + 3, + 2, + })), // Valid configuration + GroupParents: must(tlbe.NewDictFromSlice[uint8]([]uint8{ + 0, + 0, + })), + }, + want: &types.Config{ + Quorum: 3, + Signers: []common.Address{ + signers[0].Address(), + signers[1].Address(), + signers[2].Address(), + }, + GroupSigners: []types.Config{ + { + Quorum: 2, + Signers: []common.Address{ + signers[3].Address(), + signers[4].Address(), + signers[5].Address(), + }, + GroupSigners: []types.Config{}, + }, + }, + }, + }, + { + name: "CallContract error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockError: errors.New("call to contract failed"), + want: nil, + wantErr: errors.New("tvm: failed to run get method \"getConfig\": call to contract failed"), + }, + { + name: "Empty Signers list", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockResult: mcms.Config{ + Signers: must(tlbe.NewDictFromSlice[uint8]([]mcms.Signer{})), + GroupQuorums: must(tlbe.NewDictFromSlice[uint8]([]uint8{ + 3, + 2, + })), + GroupParents: must(tlbe.NewDictFromSlice[uint8]([]uint8{ + 0, + 0, + })), + }, + want: nil, + wantErr: errors.New("invalid MCMS config: Quorum must be less than or equal to the number of signers and groups"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create a new mock client and inspector for each test case + client := ton_mocks.NewAPIClientWrapped(t) + + // Mock the contract call based on the test case + // Mock CurrentMasterchainInfo + client.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + if tt.mockError == nil { + // Encode the expected return value for a successful call + r := ton.NewExecutionResult([]any{ + must(tt.mockResult.Signers.AsDictionary()).AsCell(), + must(tt.mockResult.GroupQuorums.AsDictionary()).AsCell(), + must(tt.mockResult.GroupParents.AsDictionary()).AsCell(), + }) + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(r, nil).Once() + } else { + // If there's an error, mock it on the first CallContract call + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, tt.mockError).Once() + } + + // Instantiate Inspector with the mock client + inspector := mcmston.NewInspector(client) + + // Call GetConfig and capture the got + got, err := inspector.GetConfig(ctx, tt.address) + + // Assertions for want error or successful got + if tt.wantErr != nil { + require.Error(t, err) + require.EqualError(t, err, tt.wantErr.Error()) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + + // Verify CallContract was called as want + client.AssertExpectations(t) + }) + } +} + +func TestInspector_GetOpCount(t *testing.T) { + t.Parallel() + + ctx := context.Background() + tests := []struct { + name string + address string + mockResult *big.Int + mockError error + want uint64 + wantErr error + }{ + { + name: "GetOpCount success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockResult: big.NewInt(42), // Arbitrary successful op count + want: 42, + }, + { + name: "CallContract error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockError: errors.New("call to contract failed"), + want: 0, + wantErr: errors.New("tvm: failed to run get method \"getOpCount\": call to contract failed"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create a new mock client and inspector for each test case + client := ton_mocks.NewAPIClientWrapped(t) + + // Mock the contract call based on the test case + // Mock CurrentMasterchainInfo + client.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + if tt.mockError == nil { + // Encode the expected return value for a successful call + r := ton.NewExecutionResult([]any{tt.mockResult}) + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(r, nil).Once() + } else { + // If there's an error, mock it on the first CallContract call + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, tt.mockError).Once() + } + + // Instantiate Inspector with the mock client + inspector := mcmston.NewInspector(client) + + // Call GetOpCount and capture the got + got, err := inspector.GetOpCount(ctx, tt.address) + + // Assertions for want error or successful got + if tt.wantErr != nil { + require.Error(t, err) + require.EqualError(t, err, tt.wantErr.Error()) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + + // Verify CallContract was called as want + client.AssertExpectations(t) + }) + } +} + +func TestInspector_GetRoot(t *testing.T) { + t.Parallel() + + ctx := context.Background() + tests := []struct { + name string + address string + mockResult []*big.Int + mockError error + wantRoot common.Hash + wantValidUntil uint32 + wantErr error + }{ + { + name: "GetRoot success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockResult: []*big.Int{ + new(big.Int).SetBytes(common.HexToHash("0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef").Bytes()), + big.NewInt(1234567890), + }, + wantRoot: common.HexToHash("0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef"), + wantValidUntil: 1234567890, + }, + { + name: "CallContract error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockError: errors.New("call to contract failed"), + wantErr: errors.New("tvm: failed to run get method \"getRoot\": call to contract failed"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create a new mock client and inspector for each test case + client := ton_mocks.NewAPIClientWrapped(t) + + // Mock the contract call based on the test case + // Mock CurrentMasterchainInfo + client.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + if tt.mockError == nil { + // Encode the expected return value for a successful call + r := ton.NewExecutionResult([]any{tt.mockResult[0], tt.mockResult[1]}) + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(r, nil).Once() + } else { + // If there's an error, mock it on the first CallContract call + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, tt.mockError).Once() + } + + // Instantiate Inspector with the mock client + inspector := mcmston.NewInspector(client) + + // Call GetRoot and capture the result + got, validUntil, err := inspector.GetRoot(ctx, tt.address) + + // Assertions for want error or successful result + if tt.wantErr != nil { + require.Error(t, err) + require.EqualError(t, err, tt.wantErr.Error()) + } else { + require.NoError(t, err) + assert.Equal(t, tt.wantRoot, got) + assert.Equal(t, tt.wantValidUntil, validUntil) + } + + // Verify CallContract was called as want + client.AssertExpectations(t) + }) + } +} + +func TestInspector_GetRootMetadata(t *testing.T) { + t.Parallel() + + ctx := context.Background() + tests := []struct { + name string + address string + mockResult mcms.RootMetadata + mockError error + want types.ChainMetadata + wantErr error + }{ + { + name: "GetRootMetadata success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockResult: mcms.RootMetadata{ + ChainID: big.NewInt(1), + MultiSig: address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8"), + PreOpCount: 123, + PostOpCount: 456, + OverridePreviousRoot: false, + }, + want: types.ChainMetadata{ + StartingOpCount: 123, + MCMAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + }, + }, + { + name: "CallContract error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockError: errors.New("call to contract failed"), + wantErr: errors.New("tvm: failed to run get method \"getRootMetadata\": call to contract failed"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create a new mock client and inspector for each test case + client := ton_mocks.NewAPIClientWrapped(t) + + // Mock the contract call based on the test case + // Mock CurrentMasterchainInfo + client.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + if tt.mockError == nil { + r := ton.NewExecutionResult([]any{ + tt.mockResult.ChainID, + cell.BeginCell().MustStoreAddr(tt.mockResult.MultiSig).ToSlice(), + new(big.Int).SetUint64(tt.mockResult.PreOpCount), + new(big.Int).SetUint64(tt.mockResult.PostOpCount), + big.NewInt(0), // OverridePreviousRoot as int (ignored) + }) + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(r, nil).Once() + } else { + // If there's an error, mock it on the first CallContract call + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, tt.mockError).Once() + } + + // Instantiate Inspector with the mock client + inspector := mcmston.NewInspector(client) + + // Call GetRootMetadata and capture the got + got, err := inspector.GetRootMetadata(ctx, tt.address) + + // Assertions for want error or successful got + if tt.wantErr != nil { + require.Error(t, err) + require.EqualError(t, err, tt.wantErr.Error()) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + + // Verify CallContract was called as want + client.AssertExpectations(t) + }) + } +} diff --git a/sdk/ton/mocks/api.go b/sdk/ton/mocks/api.go new file mode 100644 index 00000000..f37411f1 --- /dev/null +++ b/sdk/ton/mocks/api.go @@ -0,0 +1,1570 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package mock_ton + +import ( + address "github.com/xssnick/tonutils-go/address" + cell "github.com/xssnick/tonutils-go/tvm/cell" + + context "context" + + liteclient "github.com/xssnick/tonutils-go/liteclient" + + mock "github.com/stretchr/testify/mock" + + time "time" + + tlb "github.com/xssnick/tonutils-go/tlb" + + ton "github.com/xssnick/tonutils-go/ton" +) + +// APIClientWrapped is an autogenerated mock type for the APIClientWrapped type +type APIClientWrapped struct { + mock.Mock +} + +type APIClientWrapped_Expecter struct { + mock *mock.Mock +} + +func (_m *APIClientWrapped) EXPECT() *APIClientWrapped_Expecter { + return &APIClientWrapped_Expecter{mock: &_m.Mock} +} + +// Client provides a mock function with no fields +func (_m *APIClientWrapped) Client() ton.LiteClient { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Client") + } + + var r0 ton.LiteClient + if rf, ok := ret.Get(0).(func() ton.LiteClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ton.LiteClient) + } + } + + return r0 +} + +// APIClientWrapped_Client_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Client' +type APIClientWrapped_Client_Call struct { + *mock.Call +} + +// Client is a helper method to define mock.On call +func (_e *APIClientWrapped_Expecter) Client() *APIClientWrapped_Client_Call { + return &APIClientWrapped_Client_Call{Call: _e.mock.On("Client")} +} + +func (_c *APIClientWrapped_Client_Call) Run(run func()) *APIClientWrapped_Client_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *APIClientWrapped_Client_Call) Return(_a0 ton.LiteClient) *APIClientWrapped_Client_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *APIClientWrapped_Client_Call) RunAndReturn(run func() ton.LiteClient) *APIClientWrapped_Client_Call { + _c.Call.Return(run) + return _c +} + +// CurrentMasterchainInfo provides a mock function with given fields: ctx +func (_m *APIClientWrapped) CurrentMasterchainInfo(ctx context.Context) (*tlb.BlockInfo, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for CurrentMasterchainInfo") + } + + var r0 *tlb.BlockInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*tlb.BlockInfo, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *tlb.BlockInfo); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*tlb.BlockInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_CurrentMasterchainInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentMasterchainInfo' +type APIClientWrapped_CurrentMasterchainInfo_Call struct { + *mock.Call +} + +// CurrentMasterchainInfo is a helper method to define mock.On call +// - ctx context.Context +func (_e *APIClientWrapped_Expecter) CurrentMasterchainInfo(ctx interface{}) *APIClientWrapped_CurrentMasterchainInfo_Call { + return &APIClientWrapped_CurrentMasterchainInfo_Call{Call: _e.mock.On("CurrentMasterchainInfo", ctx)} +} + +func (_c *APIClientWrapped_CurrentMasterchainInfo_Call) Run(run func(ctx context.Context)) *APIClientWrapped_CurrentMasterchainInfo_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *APIClientWrapped_CurrentMasterchainInfo_Call) Return(_a0 *tlb.BlockInfo, err error) *APIClientWrapped_CurrentMasterchainInfo_Call { + _c.Call.Return(_a0, err) + return _c +} + +func (_c *APIClientWrapped_CurrentMasterchainInfo_Call) RunAndReturn(run func(context.Context) (*tlb.BlockInfo, error)) *APIClientWrapped_CurrentMasterchainInfo_Call { + _c.Call.Return(run) + return _c +} + +// FindLastTransactionByInMsgHash provides a mock function with given fields: ctx, addr, msgHash, maxTxNumToScan +func (_m *APIClientWrapped) FindLastTransactionByInMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) { + _va := make([]interface{}, len(maxTxNumToScan)) + for _i := range maxTxNumToScan { + _va[_i] = maxTxNumToScan[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, addr, msgHash) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for FindLastTransactionByInMsgHash") + } + + var r0 *tlb.Transaction + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *address.Address, []byte, ...int) (*tlb.Transaction, error)); ok { + return rf(ctx, addr, msgHash, maxTxNumToScan...) + } + if rf, ok := ret.Get(0).(func(context.Context, *address.Address, []byte, ...int) *tlb.Transaction); ok { + r0 = rf(ctx, addr, msgHash, maxTxNumToScan...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*tlb.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *address.Address, []byte, ...int) error); ok { + r1 = rf(ctx, addr, msgHash, maxTxNumToScan...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_FindLastTransactionByInMsgHash_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindLastTransactionByInMsgHash' +type APIClientWrapped_FindLastTransactionByInMsgHash_Call struct { + *mock.Call +} + +// FindLastTransactionByInMsgHash is a helper method to define mock.On call +// - ctx context.Context +// - addr *address.Address +// - msgHash []byte +// - maxTxNumToScan ...int +func (_e *APIClientWrapped_Expecter) FindLastTransactionByInMsgHash(ctx interface{}, addr interface{}, msgHash interface{}, maxTxNumToScan ...interface{}) *APIClientWrapped_FindLastTransactionByInMsgHash_Call { + return &APIClientWrapped_FindLastTransactionByInMsgHash_Call{Call: _e.mock.On("FindLastTransactionByInMsgHash", + append([]interface{}{ctx, addr, msgHash}, maxTxNumToScan...)...)} +} + +func (_c *APIClientWrapped_FindLastTransactionByInMsgHash_Call) Run(run func(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int)) *APIClientWrapped_FindLastTransactionByInMsgHash_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]int, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(int) + } + } + run(args[0].(context.Context), args[1].(*address.Address), args[2].([]byte), variadicArgs...) + }) + return _c +} + +func (_c *APIClientWrapped_FindLastTransactionByInMsgHash_Call) Return(_a0 *tlb.Transaction, _a1 error) *APIClientWrapped_FindLastTransactionByInMsgHash_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_FindLastTransactionByInMsgHash_Call) RunAndReturn(run func(context.Context, *address.Address, []byte, ...int) (*tlb.Transaction, error)) *APIClientWrapped_FindLastTransactionByInMsgHash_Call { + _c.Call.Return(run) + return _c +} + +// FindLastTransactionByOutMsgHash provides a mock function with given fields: ctx, addr, msgHash, maxTxNumToScan +func (_m *APIClientWrapped) FindLastTransactionByOutMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) { + _va := make([]interface{}, len(maxTxNumToScan)) + for _i := range maxTxNumToScan { + _va[_i] = maxTxNumToScan[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, addr, msgHash) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for FindLastTransactionByOutMsgHash") + } + + var r0 *tlb.Transaction + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *address.Address, []byte, ...int) (*tlb.Transaction, error)); ok { + return rf(ctx, addr, msgHash, maxTxNumToScan...) + } + if rf, ok := ret.Get(0).(func(context.Context, *address.Address, []byte, ...int) *tlb.Transaction); ok { + r0 = rf(ctx, addr, msgHash, maxTxNumToScan...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*tlb.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *address.Address, []byte, ...int) error); ok { + r1 = rf(ctx, addr, msgHash, maxTxNumToScan...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_FindLastTransactionByOutMsgHash_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindLastTransactionByOutMsgHash' +type APIClientWrapped_FindLastTransactionByOutMsgHash_Call struct { + *mock.Call +} + +// FindLastTransactionByOutMsgHash is a helper method to define mock.On call +// - ctx context.Context +// - addr *address.Address +// - msgHash []byte +// - maxTxNumToScan ...int +func (_e *APIClientWrapped_Expecter) FindLastTransactionByOutMsgHash(ctx interface{}, addr interface{}, msgHash interface{}, maxTxNumToScan ...interface{}) *APIClientWrapped_FindLastTransactionByOutMsgHash_Call { + return &APIClientWrapped_FindLastTransactionByOutMsgHash_Call{Call: _e.mock.On("FindLastTransactionByOutMsgHash", + append([]interface{}{ctx, addr, msgHash}, maxTxNumToScan...)...)} +} + +func (_c *APIClientWrapped_FindLastTransactionByOutMsgHash_Call) Run(run func(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int)) *APIClientWrapped_FindLastTransactionByOutMsgHash_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]int, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(int) + } + } + run(args[0].(context.Context), args[1].(*address.Address), args[2].([]byte), variadicArgs...) + }) + return _c +} + +func (_c *APIClientWrapped_FindLastTransactionByOutMsgHash_Call) Return(_a0 *tlb.Transaction, _a1 error) *APIClientWrapped_FindLastTransactionByOutMsgHash_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_FindLastTransactionByOutMsgHash_Call) RunAndReturn(run func(context.Context, *address.Address, []byte, ...int) (*tlb.Transaction, error)) *APIClientWrapped_FindLastTransactionByOutMsgHash_Call { + _c.Call.Return(run) + return _c +} + +// GetAccount provides a mock function with given fields: ctx, block, addr +func (_m *APIClientWrapped) GetAccount(ctx context.Context, block *tlb.BlockInfo, addr *address.Address) (*tlb.Account, error) { + ret := _m.Called(ctx, block, addr) + + if len(ret) == 0 { + panic("no return value specified for GetAccount") + } + + var r0 *tlb.Account + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo, *address.Address) (*tlb.Account, error)); ok { + return rf(ctx, block, addr) + } + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo, *address.Address) *tlb.Account); ok { + r0 = rf(ctx, block, addr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*tlb.Account) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *tlb.BlockInfo, *address.Address) error); ok { + r1 = rf(ctx, block, addr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_GetAccount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAccount' +type APIClientWrapped_GetAccount_Call struct { + *mock.Call +} + +// GetAccount is a helper method to define mock.On call +// - ctx context.Context +// - block *tlb.BlockInfo +// - addr *address.Address +func (_e *APIClientWrapped_Expecter) GetAccount(ctx interface{}, block interface{}, addr interface{}) *APIClientWrapped_GetAccount_Call { + return &APIClientWrapped_GetAccount_Call{Call: _e.mock.On("GetAccount", ctx, block, addr)} +} + +func (_c *APIClientWrapped_GetAccount_Call) Run(run func(ctx context.Context, block *tlb.BlockInfo, addr *address.Address)) *APIClientWrapped_GetAccount_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*tlb.BlockInfo), args[2].(*address.Address)) + }) + return _c +} + +func (_c *APIClientWrapped_GetAccount_Call) Return(_a0 *tlb.Account, _a1 error) *APIClientWrapped_GetAccount_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_GetAccount_Call) RunAndReturn(run func(context.Context, *tlb.BlockInfo, *address.Address) (*tlb.Account, error)) *APIClientWrapped_GetAccount_Call { + _c.Call.Return(run) + return _c +} + +// GetBlockData provides a mock function with given fields: ctx, block +func (_m *APIClientWrapped) GetBlockData(ctx context.Context, block *tlb.BlockInfo) (*tlb.Block, error) { + ret := _m.Called(ctx, block) + + if len(ret) == 0 { + panic("no return value specified for GetBlockData") + } + + var r0 *tlb.Block + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo) (*tlb.Block, error)); ok { + return rf(ctx, block) + } + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo) *tlb.Block); ok { + r0 = rf(ctx, block) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*tlb.Block) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *tlb.BlockInfo) error); ok { + r1 = rf(ctx, block) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_GetBlockData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBlockData' +type APIClientWrapped_GetBlockData_Call struct { + *mock.Call +} + +// GetBlockData is a helper method to define mock.On call +// - ctx context.Context +// - block *tlb.BlockInfo +func (_e *APIClientWrapped_Expecter) GetBlockData(ctx interface{}, block interface{}) *APIClientWrapped_GetBlockData_Call { + return &APIClientWrapped_GetBlockData_Call{Call: _e.mock.On("GetBlockData", ctx, block)} +} + +func (_c *APIClientWrapped_GetBlockData_Call) Run(run func(ctx context.Context, block *tlb.BlockInfo)) *APIClientWrapped_GetBlockData_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*tlb.BlockInfo)) + }) + return _c +} + +func (_c *APIClientWrapped_GetBlockData_Call) Return(_a0 *tlb.Block, _a1 error) *APIClientWrapped_GetBlockData_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_GetBlockData_Call) RunAndReturn(run func(context.Context, *tlb.BlockInfo) (*tlb.Block, error)) *APIClientWrapped_GetBlockData_Call { + _c.Call.Return(run) + return _c +} + +// GetBlockProof provides a mock function with given fields: ctx, known, target +func (_m *APIClientWrapped) GetBlockProof(ctx context.Context, known *tlb.BlockInfo, target *tlb.BlockInfo) (*ton.PartialBlockProof, error) { + ret := _m.Called(ctx, known, target) + + if len(ret) == 0 { + panic("no return value specified for GetBlockProof") + } + + var r0 *ton.PartialBlockProof + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo, *tlb.BlockInfo) (*ton.PartialBlockProof, error)); ok { + return rf(ctx, known, target) + } + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo, *tlb.BlockInfo) *ton.PartialBlockProof); ok { + r0 = rf(ctx, known, target) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ton.PartialBlockProof) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *tlb.BlockInfo, *tlb.BlockInfo) error); ok { + r1 = rf(ctx, known, target) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_GetBlockProof_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBlockProof' +type APIClientWrapped_GetBlockProof_Call struct { + *mock.Call +} + +// GetBlockProof is a helper method to define mock.On call +// - ctx context.Context +// - known *tlb.BlockInfo +// - target *tlb.BlockInfo +func (_e *APIClientWrapped_Expecter) GetBlockProof(ctx interface{}, known interface{}, target interface{}) *APIClientWrapped_GetBlockProof_Call { + return &APIClientWrapped_GetBlockProof_Call{Call: _e.mock.On("GetBlockProof", ctx, known, target)} +} + +func (_c *APIClientWrapped_GetBlockProof_Call) Run(run func(ctx context.Context, known *tlb.BlockInfo, target *tlb.BlockInfo)) *APIClientWrapped_GetBlockProof_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*tlb.BlockInfo), args[2].(*tlb.BlockInfo)) + }) + return _c +} + +func (_c *APIClientWrapped_GetBlockProof_Call) Return(_a0 *ton.PartialBlockProof, _a1 error) *APIClientWrapped_GetBlockProof_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_GetBlockProof_Call) RunAndReturn(run func(context.Context, *tlb.BlockInfo, *tlb.BlockInfo) (*ton.PartialBlockProof, error)) *APIClientWrapped_GetBlockProof_Call { + _c.Call.Return(run) + return _c +} + +// GetBlockShardsInfo provides a mock function with given fields: ctx, master +func (_m *APIClientWrapped) GetBlockShardsInfo(ctx context.Context, master *tlb.BlockInfo) ([]*tlb.BlockInfo, error) { + ret := _m.Called(ctx, master) + + if len(ret) == 0 { + panic("no return value specified for GetBlockShardsInfo") + } + + var r0 []*tlb.BlockInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo) ([]*tlb.BlockInfo, error)); ok { + return rf(ctx, master) + } + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo) []*tlb.BlockInfo); ok { + r0 = rf(ctx, master) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*tlb.BlockInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *tlb.BlockInfo) error); ok { + r1 = rf(ctx, master) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_GetBlockShardsInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBlockShardsInfo' +type APIClientWrapped_GetBlockShardsInfo_Call struct { + *mock.Call +} + +// GetBlockShardsInfo is a helper method to define mock.On call +// - ctx context.Context +// - master *tlb.BlockInfo +func (_e *APIClientWrapped_Expecter) GetBlockShardsInfo(ctx interface{}, master interface{}) *APIClientWrapped_GetBlockShardsInfo_Call { + return &APIClientWrapped_GetBlockShardsInfo_Call{Call: _e.mock.On("GetBlockShardsInfo", ctx, master)} +} + +func (_c *APIClientWrapped_GetBlockShardsInfo_Call) Run(run func(ctx context.Context, master *tlb.BlockInfo)) *APIClientWrapped_GetBlockShardsInfo_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*tlb.BlockInfo)) + }) + return _c +} + +func (_c *APIClientWrapped_GetBlockShardsInfo_Call) Return(_a0 []*tlb.BlockInfo, _a1 error) *APIClientWrapped_GetBlockShardsInfo_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_GetBlockShardsInfo_Call) RunAndReturn(run func(context.Context, *tlb.BlockInfo) ([]*tlb.BlockInfo, error)) *APIClientWrapped_GetBlockShardsInfo_Call { + _c.Call.Return(run) + return _c +} + +// GetBlockTransactionsV2 provides a mock function with given fields: ctx, block, count, after +func (_m *APIClientWrapped) GetBlockTransactionsV2(ctx context.Context, block *tlb.BlockInfo, count uint32, after ...*ton.TransactionID3) ([]ton.TransactionShortInfo, bool, error) { + _va := make([]interface{}, len(after)) + for _i := range after { + _va[_i] = after[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, block, count) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetBlockTransactionsV2") + } + + var r0 []ton.TransactionShortInfo + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo, uint32, ...*ton.TransactionID3) ([]ton.TransactionShortInfo, bool, error)); ok { + return rf(ctx, block, count, after...) + } + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo, uint32, ...*ton.TransactionID3) []ton.TransactionShortInfo); ok { + r0 = rf(ctx, block, count, after...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]ton.TransactionShortInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *tlb.BlockInfo, uint32, ...*ton.TransactionID3) bool); ok { + r1 = rf(ctx, block, count, after...) + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func(context.Context, *tlb.BlockInfo, uint32, ...*ton.TransactionID3) error); ok { + r2 = rf(ctx, block, count, after...) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// APIClientWrapped_GetBlockTransactionsV2_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBlockTransactionsV2' +type APIClientWrapped_GetBlockTransactionsV2_Call struct { + *mock.Call +} + +// GetBlockTransactionsV2 is a helper method to define mock.On call +// - ctx context.Context +// - block *tlb.BlockInfo +// - count uint32 +// - after ...*ton.TransactionID3 +func (_e *APIClientWrapped_Expecter) GetBlockTransactionsV2(ctx interface{}, block interface{}, count interface{}, after ...interface{}) *APIClientWrapped_GetBlockTransactionsV2_Call { + return &APIClientWrapped_GetBlockTransactionsV2_Call{Call: _e.mock.On("GetBlockTransactionsV2", + append([]interface{}{ctx, block, count}, after...)...)} +} + +func (_c *APIClientWrapped_GetBlockTransactionsV2_Call) Run(run func(ctx context.Context, block *tlb.BlockInfo, count uint32, after ...*ton.TransactionID3)) *APIClientWrapped_GetBlockTransactionsV2_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]*ton.TransactionID3, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(*ton.TransactionID3) + } + } + run(args[0].(context.Context), args[1].(*tlb.BlockInfo), args[2].(uint32), variadicArgs...) + }) + return _c +} + +func (_c *APIClientWrapped_GetBlockTransactionsV2_Call) Return(_a0 []ton.TransactionShortInfo, _a1 bool, _a2 error) *APIClientWrapped_GetBlockTransactionsV2_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *APIClientWrapped_GetBlockTransactionsV2_Call) RunAndReturn(run func(context.Context, *tlb.BlockInfo, uint32, ...*ton.TransactionID3) ([]ton.TransactionShortInfo, bool, error)) *APIClientWrapped_GetBlockTransactionsV2_Call { + _c.Call.Return(run) + return _c +} + +// GetBlockchainConfig provides a mock function with given fields: ctx, block, onlyParams +func (_m *APIClientWrapped) GetBlockchainConfig(ctx context.Context, block *tlb.BlockInfo, onlyParams ...int32) (*ton.BlockchainConfig, error) { + _va := make([]interface{}, len(onlyParams)) + for _i := range onlyParams { + _va[_i] = onlyParams[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, block) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetBlockchainConfig") + } + + var r0 *ton.BlockchainConfig + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo, ...int32) (*ton.BlockchainConfig, error)); ok { + return rf(ctx, block, onlyParams...) + } + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo, ...int32) *ton.BlockchainConfig); ok { + r0 = rf(ctx, block, onlyParams...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ton.BlockchainConfig) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *tlb.BlockInfo, ...int32) error); ok { + r1 = rf(ctx, block, onlyParams...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_GetBlockchainConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBlockchainConfig' +type APIClientWrapped_GetBlockchainConfig_Call struct { + *mock.Call +} + +// GetBlockchainConfig is a helper method to define mock.On call +// - ctx context.Context +// - block *tlb.BlockInfo +// - onlyParams ...int32 +func (_e *APIClientWrapped_Expecter) GetBlockchainConfig(ctx interface{}, block interface{}, onlyParams ...interface{}) *APIClientWrapped_GetBlockchainConfig_Call { + return &APIClientWrapped_GetBlockchainConfig_Call{Call: _e.mock.On("GetBlockchainConfig", + append([]interface{}{ctx, block}, onlyParams...)...)} +} + +func (_c *APIClientWrapped_GetBlockchainConfig_Call) Run(run func(ctx context.Context, block *tlb.BlockInfo, onlyParams ...int32)) *APIClientWrapped_GetBlockchainConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]int32, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(int32) + } + } + run(args[0].(context.Context), args[1].(*tlb.BlockInfo), variadicArgs...) + }) + return _c +} + +func (_c *APIClientWrapped_GetBlockchainConfig_Call) Return(_a0 *ton.BlockchainConfig, _a1 error) *APIClientWrapped_GetBlockchainConfig_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_GetBlockchainConfig_Call) RunAndReturn(run func(context.Context, *tlb.BlockInfo, ...int32) (*ton.BlockchainConfig, error)) *APIClientWrapped_GetBlockchainConfig_Call { + _c.Call.Return(run) + return _c +} + +// GetLibraries provides a mock function with given fields: ctx, list +func (_m *APIClientWrapped) GetLibraries(ctx context.Context, list ...[]byte) ([]*cell.Cell, error) { + _va := make([]interface{}, len(list)) + for _i := range list { + _va[_i] = list[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetLibraries") + } + + var r0 []*cell.Cell + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ...[]byte) ([]*cell.Cell, error)); ok { + return rf(ctx, list...) + } + if rf, ok := ret.Get(0).(func(context.Context, ...[]byte) []*cell.Cell); ok { + r0 = rf(ctx, list...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*cell.Cell) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ...[]byte) error); ok { + r1 = rf(ctx, list...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_GetLibraries_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLibraries' +type APIClientWrapped_GetLibraries_Call struct { + *mock.Call +} + +// GetLibraries is a helper method to define mock.On call +// - ctx context.Context +// - list ...[]byte +func (_e *APIClientWrapped_Expecter) GetLibraries(ctx interface{}, list ...interface{}) *APIClientWrapped_GetLibraries_Call { + return &APIClientWrapped_GetLibraries_Call{Call: _e.mock.On("GetLibraries", + append([]interface{}{ctx}, list...)...)} +} + +func (_c *APIClientWrapped_GetLibraries_Call) Run(run func(ctx context.Context, list ...[]byte)) *APIClientWrapped_GetLibraries_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([][]byte, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.([]byte) + } + } + run(args[0].(context.Context), variadicArgs...) + }) + return _c +} + +func (_c *APIClientWrapped_GetLibraries_Call) Return(_a0 []*cell.Cell, _a1 error) *APIClientWrapped_GetLibraries_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_GetLibraries_Call) RunAndReturn(run func(context.Context, ...[]byte) ([]*cell.Cell, error)) *APIClientWrapped_GetLibraries_Call { + _c.Call.Return(run) + return _c +} + +// GetMasterchainInfo provides a mock function with given fields: ctx +func (_m *APIClientWrapped) GetMasterchainInfo(ctx context.Context) (*tlb.BlockInfo, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetMasterchainInfo") + } + + var r0 *tlb.BlockInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*tlb.BlockInfo, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *tlb.BlockInfo); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*tlb.BlockInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_GetMasterchainInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMasterchainInfo' +type APIClientWrapped_GetMasterchainInfo_Call struct { + *mock.Call +} + +// GetMasterchainInfo is a helper method to define mock.On call +// - ctx context.Context +func (_e *APIClientWrapped_Expecter) GetMasterchainInfo(ctx interface{}) *APIClientWrapped_GetMasterchainInfo_Call { + return &APIClientWrapped_GetMasterchainInfo_Call{Call: _e.mock.On("GetMasterchainInfo", ctx)} +} + +func (_c *APIClientWrapped_GetMasterchainInfo_Call) Run(run func(ctx context.Context)) *APIClientWrapped_GetMasterchainInfo_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *APIClientWrapped_GetMasterchainInfo_Call) Return(_a0 *tlb.BlockInfo, _a1 error) *APIClientWrapped_GetMasterchainInfo_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_GetMasterchainInfo_Call) RunAndReturn(run func(context.Context) (*tlb.BlockInfo, error)) *APIClientWrapped_GetMasterchainInfo_Call { + _c.Call.Return(run) + return _c +} + +// GetTime provides a mock function with given fields: ctx +func (_m *APIClientWrapped) GetTime(ctx context.Context) (uint32, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetTime") + } + + var r0 uint32 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (uint32, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) uint32); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(uint32) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_GetTime_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTime' +type APIClientWrapped_GetTime_Call struct { + *mock.Call +} + +// GetTime is a helper method to define mock.On call +// - ctx context.Context +func (_e *APIClientWrapped_Expecter) GetTime(ctx interface{}) *APIClientWrapped_GetTime_Call { + return &APIClientWrapped_GetTime_Call{Call: _e.mock.On("GetTime", ctx)} +} + +func (_c *APIClientWrapped_GetTime_Call) Run(run func(ctx context.Context)) *APIClientWrapped_GetTime_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *APIClientWrapped_GetTime_Call) Return(_a0 uint32, _a1 error) *APIClientWrapped_GetTime_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_GetTime_Call) RunAndReturn(run func(context.Context) (uint32, error)) *APIClientWrapped_GetTime_Call { + _c.Call.Return(run) + return _c +} + +// GetTransaction provides a mock function with given fields: ctx, block, addr, lt +func (_m *APIClientWrapped) GetTransaction(ctx context.Context, block *tlb.BlockInfo, addr *address.Address, lt uint64) (*tlb.Transaction, error) { + ret := _m.Called(ctx, block, addr, lt) + + if len(ret) == 0 { + panic("no return value specified for GetTransaction") + } + + var r0 *tlb.Transaction + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo, *address.Address, uint64) (*tlb.Transaction, error)); ok { + return rf(ctx, block, addr, lt) + } + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo, *address.Address, uint64) *tlb.Transaction); ok { + r0 = rf(ctx, block, addr, lt) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*tlb.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *tlb.BlockInfo, *address.Address, uint64) error); ok { + r1 = rf(ctx, block, addr, lt) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_GetTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTransaction' +type APIClientWrapped_GetTransaction_Call struct { + *mock.Call +} + +// GetTransaction is a helper method to define mock.On call +// - ctx context.Context +// - block *tlb.BlockInfo +// - addr *address.Address +// - lt uint64 +func (_e *APIClientWrapped_Expecter) GetTransaction(ctx interface{}, block interface{}, addr interface{}, lt interface{}) *APIClientWrapped_GetTransaction_Call { + return &APIClientWrapped_GetTransaction_Call{Call: _e.mock.On("GetTransaction", ctx, block, addr, lt)} +} + +func (_c *APIClientWrapped_GetTransaction_Call) Run(run func(ctx context.Context, block *tlb.BlockInfo, addr *address.Address, lt uint64)) *APIClientWrapped_GetTransaction_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*tlb.BlockInfo), args[2].(*address.Address), args[3].(uint64)) + }) + return _c +} + +func (_c *APIClientWrapped_GetTransaction_Call) Return(_a0 *tlb.Transaction, _a1 error) *APIClientWrapped_GetTransaction_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_GetTransaction_Call) RunAndReturn(run func(context.Context, *tlb.BlockInfo, *address.Address, uint64) (*tlb.Transaction, error)) *APIClientWrapped_GetTransaction_Call { + _c.Call.Return(run) + return _c +} + +// ListTransactions provides a mock function with given fields: ctx, addr, num, lt, txHash +func (_m *APIClientWrapped) ListTransactions(ctx context.Context, addr *address.Address, num uint32, lt uint64, txHash []byte) ([]*tlb.Transaction, error) { + ret := _m.Called(ctx, addr, num, lt, txHash) + + if len(ret) == 0 { + panic("no return value specified for ListTransactions") + } + + var r0 []*tlb.Transaction + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *address.Address, uint32, uint64, []byte) ([]*tlb.Transaction, error)); ok { + return rf(ctx, addr, num, lt, txHash) + } + if rf, ok := ret.Get(0).(func(context.Context, *address.Address, uint32, uint64, []byte) []*tlb.Transaction); ok { + r0 = rf(ctx, addr, num, lt, txHash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*tlb.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *address.Address, uint32, uint64, []byte) error); ok { + r1 = rf(ctx, addr, num, lt, txHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_ListTransactions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListTransactions' +type APIClientWrapped_ListTransactions_Call struct { + *mock.Call +} + +// ListTransactions is a helper method to define mock.On call +// - ctx context.Context +// - addr *address.Address +// - num uint32 +// - lt uint64 +// - txHash []byte +func (_e *APIClientWrapped_Expecter) ListTransactions(ctx interface{}, addr interface{}, num interface{}, lt interface{}, txHash interface{}) *APIClientWrapped_ListTransactions_Call { + return &APIClientWrapped_ListTransactions_Call{Call: _e.mock.On("ListTransactions", ctx, addr, num, lt, txHash)} +} + +func (_c *APIClientWrapped_ListTransactions_Call) Run(run func(ctx context.Context, addr *address.Address, num uint32, lt uint64, txHash []byte)) *APIClientWrapped_ListTransactions_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*address.Address), args[2].(uint32), args[3].(uint64), args[4].([]byte)) + }) + return _c +} + +func (_c *APIClientWrapped_ListTransactions_Call) Return(_a0 []*tlb.Transaction, _a1 error) *APIClientWrapped_ListTransactions_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_ListTransactions_Call) RunAndReturn(run func(context.Context, *address.Address, uint32, uint64, []byte) ([]*tlb.Transaction, error)) *APIClientWrapped_ListTransactions_Call { + _c.Call.Return(run) + return _c +} + +// LookupBlock provides a mock function with given fields: ctx, workchain, shard, seqno +func (_m *APIClientWrapped) LookupBlock(ctx context.Context, workchain int32, shard int64, seqno uint32) (*tlb.BlockInfo, error) { + ret := _m.Called(ctx, workchain, shard, seqno) + + if len(ret) == 0 { + panic("no return value specified for LookupBlock") + } + + var r0 *tlb.BlockInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int32, int64, uint32) (*tlb.BlockInfo, error)); ok { + return rf(ctx, workchain, shard, seqno) + } + if rf, ok := ret.Get(0).(func(context.Context, int32, int64, uint32) *tlb.BlockInfo); ok { + r0 = rf(ctx, workchain, shard, seqno) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*tlb.BlockInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int32, int64, uint32) error); ok { + r1 = rf(ctx, workchain, shard, seqno) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_LookupBlock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LookupBlock' +type APIClientWrapped_LookupBlock_Call struct { + *mock.Call +} + +// LookupBlock is a helper method to define mock.On call +// - ctx context.Context +// - workchain int32 +// - shard int64 +// - seqno uint32 +func (_e *APIClientWrapped_Expecter) LookupBlock(ctx interface{}, workchain interface{}, shard interface{}, seqno interface{}) *APIClientWrapped_LookupBlock_Call { + return &APIClientWrapped_LookupBlock_Call{Call: _e.mock.On("LookupBlock", ctx, workchain, shard, seqno)} +} + +func (_c *APIClientWrapped_LookupBlock_Call) Run(run func(ctx context.Context, workchain int32, shard int64, seqno uint32)) *APIClientWrapped_LookupBlock_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int32), args[2].(int64), args[3].(uint32)) + }) + return _c +} + +func (_c *APIClientWrapped_LookupBlock_Call) Return(_a0 *tlb.BlockInfo, _a1 error) *APIClientWrapped_LookupBlock_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_LookupBlock_Call) RunAndReturn(run func(context.Context, int32, int64, uint32) (*tlb.BlockInfo, error)) *APIClientWrapped_LookupBlock_Call { + _c.Call.Return(run) + return _c +} + +// RunGetMethod provides a mock function with given fields: ctx, blockInfo, addr, method, params +func (_m *APIClientWrapped) RunGetMethod(ctx context.Context, blockInfo *tlb.BlockInfo, addr *address.Address, method string, params ...interface{}) (*ton.ExecutionResult, error) { + var _ca []interface{} + _ca = append(_ca, ctx, blockInfo, addr, method) + _ca = append(_ca, params...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for RunGetMethod") + } + + var r0 *ton.ExecutionResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo, *address.Address, string, ...interface{}) (*ton.ExecutionResult, error)); ok { + return rf(ctx, blockInfo, addr, method, params...) + } + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo, *address.Address, string, ...interface{}) *ton.ExecutionResult); ok { + r0 = rf(ctx, blockInfo, addr, method, params...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ton.ExecutionResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *tlb.BlockInfo, *address.Address, string, ...interface{}) error); ok { + r1 = rf(ctx, blockInfo, addr, method, params...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// APIClientWrapped_RunGetMethod_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RunGetMethod' +type APIClientWrapped_RunGetMethod_Call struct { + *mock.Call +} + +// RunGetMethod is a helper method to define mock.On call +// - ctx context.Context +// - blockInfo *tlb.BlockInfo +// - addr *address.Address +// - method string +// - params ...interface{} +func (_e *APIClientWrapped_Expecter) RunGetMethod(ctx interface{}, blockInfo interface{}, addr interface{}, method interface{}, params ...interface{}) *APIClientWrapped_RunGetMethod_Call { + return &APIClientWrapped_RunGetMethod_Call{Call: _e.mock.On("RunGetMethod", + append([]interface{}{ctx, blockInfo, addr, method}, params...)...)} +} + +func (_c *APIClientWrapped_RunGetMethod_Call) Run(run func(ctx context.Context, blockInfo *tlb.BlockInfo, addr *address.Address, method string, params ...interface{})) *APIClientWrapped_RunGetMethod_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-4) + for i, a := range args[4:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(*tlb.BlockInfo), args[2].(*address.Address), args[3].(string), variadicArgs...) + }) + return _c +} + +func (_c *APIClientWrapped_RunGetMethod_Call) Return(_a0 *ton.ExecutionResult, _a1 error) *APIClientWrapped_RunGetMethod_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *APIClientWrapped_RunGetMethod_Call) RunAndReturn(run func(context.Context, *tlb.BlockInfo, *address.Address, string, ...interface{}) (*ton.ExecutionResult, error)) *APIClientWrapped_RunGetMethod_Call { + _c.Call.Return(run) + return _c +} + +// SendExternalMessage provides a mock function with given fields: ctx, msg +func (_m *APIClientWrapped) SendExternalMessage(ctx context.Context, msg *tlb.ExternalMessage) error { + ret := _m.Called(ctx, msg) + + if len(ret) == 0 { + panic("no return value specified for SendExternalMessage") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *tlb.ExternalMessage) error); ok { + r0 = rf(ctx, msg) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// APIClientWrapped_SendExternalMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendExternalMessage' +type APIClientWrapped_SendExternalMessage_Call struct { + *mock.Call +} + +// SendExternalMessage is a helper method to define mock.On call +// - ctx context.Context +// - msg *tlb.ExternalMessage +func (_e *APIClientWrapped_Expecter) SendExternalMessage(ctx interface{}, msg interface{}) *APIClientWrapped_SendExternalMessage_Call { + return &APIClientWrapped_SendExternalMessage_Call{Call: _e.mock.On("SendExternalMessage", ctx, msg)} +} + +func (_c *APIClientWrapped_SendExternalMessage_Call) Run(run func(ctx context.Context, msg *tlb.ExternalMessage)) *APIClientWrapped_SendExternalMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*tlb.ExternalMessage)) + }) + return _c +} + +func (_c *APIClientWrapped_SendExternalMessage_Call) Return(_a0 error) *APIClientWrapped_SendExternalMessage_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *APIClientWrapped_SendExternalMessage_Call) RunAndReturn(run func(context.Context, *tlb.ExternalMessage) error) *APIClientWrapped_SendExternalMessage_Call { + _c.Call.Return(run) + return _c +} + +// SendExternalMessageWaitTransaction provides a mock function with given fields: ctx, msg +func (_m *APIClientWrapped) SendExternalMessageWaitTransaction(ctx context.Context, msg *tlb.ExternalMessage) (*tlb.Transaction, *tlb.BlockInfo, []byte, error) { + ret := _m.Called(ctx, msg) + + if len(ret) == 0 { + panic("no return value specified for SendExternalMessageWaitTransaction") + } + + var r0 *tlb.Transaction + var r1 *tlb.BlockInfo + var r2 []byte + var r3 error + if rf, ok := ret.Get(0).(func(context.Context, *tlb.ExternalMessage) (*tlb.Transaction, *tlb.BlockInfo, []byte, error)); ok { + return rf(ctx, msg) + } + if rf, ok := ret.Get(0).(func(context.Context, *tlb.ExternalMessage) *tlb.Transaction); ok { + r0 = rf(ctx, msg) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*tlb.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *tlb.ExternalMessage) *tlb.BlockInfo); ok { + r1 = rf(ctx, msg) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*tlb.BlockInfo) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, *tlb.ExternalMessage) []byte); ok { + r2 = rf(ctx, msg) + } else { + if ret.Get(2) != nil { + r2 = ret.Get(2).([]byte) + } + } + + if rf, ok := ret.Get(3).(func(context.Context, *tlb.ExternalMessage) error); ok { + r3 = rf(ctx, msg) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + +// APIClientWrapped_SendExternalMessageWaitTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendExternalMessageWaitTransaction' +type APIClientWrapped_SendExternalMessageWaitTransaction_Call struct { + *mock.Call +} + +// SendExternalMessageWaitTransaction is a helper method to define mock.On call +// - ctx context.Context +// - msg *tlb.ExternalMessage +func (_e *APIClientWrapped_Expecter) SendExternalMessageWaitTransaction(ctx interface{}, msg interface{}) *APIClientWrapped_SendExternalMessageWaitTransaction_Call { + return &APIClientWrapped_SendExternalMessageWaitTransaction_Call{Call: _e.mock.On("SendExternalMessageWaitTransaction", ctx, msg)} +} + +func (_c *APIClientWrapped_SendExternalMessageWaitTransaction_Call) Run(run func(ctx context.Context, msg *tlb.ExternalMessage)) *APIClientWrapped_SendExternalMessageWaitTransaction_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*tlb.ExternalMessage)) + }) + return _c +} + +func (_c *APIClientWrapped_SendExternalMessageWaitTransaction_Call) Return(_a0 *tlb.Transaction, _a1 *tlb.BlockInfo, _a2 []byte, _a3 error) *APIClientWrapped_SendExternalMessageWaitTransaction_Call { + _c.Call.Return(_a0, _a1, _a2, _a3) + return _c +} + +func (_c *APIClientWrapped_SendExternalMessageWaitTransaction_Call) RunAndReturn(run func(context.Context, *tlb.ExternalMessage) (*tlb.Transaction, *tlb.BlockInfo, []byte, error)) *APIClientWrapped_SendExternalMessageWaitTransaction_Call { + _c.Call.Return(run) + return _c +} + +// SetTrustedBlock provides a mock function with given fields: block +func (_m *APIClientWrapped) SetTrustedBlock(block *tlb.BlockInfo) { + _m.Called(block) +} + +// APIClientWrapped_SetTrustedBlock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetTrustedBlock' +type APIClientWrapped_SetTrustedBlock_Call struct { + *mock.Call +} + +// SetTrustedBlock is a helper method to define mock.On call +// - block *tlb.BlockInfo +func (_e *APIClientWrapped_Expecter) SetTrustedBlock(block interface{}) *APIClientWrapped_SetTrustedBlock_Call { + return &APIClientWrapped_SetTrustedBlock_Call{Call: _e.mock.On("SetTrustedBlock", block)} +} + +func (_c *APIClientWrapped_SetTrustedBlock_Call) Run(run func(block *tlb.BlockInfo)) *APIClientWrapped_SetTrustedBlock_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*tlb.BlockInfo)) + }) + return _c +} + +func (_c *APIClientWrapped_SetTrustedBlock_Call) Return() *APIClientWrapped_SetTrustedBlock_Call { + _c.Call.Return() + return _c +} + +func (_c *APIClientWrapped_SetTrustedBlock_Call) RunAndReturn(run func(*tlb.BlockInfo)) *APIClientWrapped_SetTrustedBlock_Call { + _c.Run(run) + return _c +} + +// SetTrustedBlockFromConfig provides a mock function with given fields: cfg +func (_m *APIClientWrapped) SetTrustedBlockFromConfig(cfg *liteclient.GlobalConfig) { + _m.Called(cfg) +} + +// APIClientWrapped_SetTrustedBlockFromConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetTrustedBlockFromConfig' +type APIClientWrapped_SetTrustedBlockFromConfig_Call struct { + *mock.Call +} + +// SetTrustedBlockFromConfig is a helper method to define mock.On call +// - cfg *liteclient.GlobalConfig +func (_e *APIClientWrapped_Expecter) SetTrustedBlockFromConfig(cfg interface{}) *APIClientWrapped_SetTrustedBlockFromConfig_Call { + return &APIClientWrapped_SetTrustedBlockFromConfig_Call{Call: _e.mock.On("SetTrustedBlockFromConfig", cfg)} +} + +func (_c *APIClientWrapped_SetTrustedBlockFromConfig_Call) Run(run func(cfg *liteclient.GlobalConfig)) *APIClientWrapped_SetTrustedBlockFromConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*liteclient.GlobalConfig)) + }) + return _c +} + +func (_c *APIClientWrapped_SetTrustedBlockFromConfig_Call) Return() *APIClientWrapped_SetTrustedBlockFromConfig_Call { + _c.Call.Return() + return _c +} + +func (_c *APIClientWrapped_SetTrustedBlockFromConfig_Call) RunAndReturn(run func(*liteclient.GlobalConfig)) *APIClientWrapped_SetTrustedBlockFromConfig_Call { + _c.Run(run) + return _c +} + +// SubscribeOnTransactions provides a mock function with given fields: workerCtx, addr, lastProcessedLT, channel +func (_m *APIClientWrapped) SubscribeOnTransactions(workerCtx context.Context, addr *address.Address, lastProcessedLT uint64, channel chan<- *tlb.Transaction) { + _m.Called(workerCtx, addr, lastProcessedLT, channel) +} + +// APIClientWrapped_SubscribeOnTransactions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeOnTransactions' +type APIClientWrapped_SubscribeOnTransactions_Call struct { + *mock.Call +} + +// SubscribeOnTransactions is a helper method to define mock.On call +// - workerCtx context.Context +// - addr *address.Address +// - lastProcessedLT uint64 +// - channel chan<- *tlb.Transaction +func (_e *APIClientWrapped_Expecter) SubscribeOnTransactions(workerCtx interface{}, addr interface{}, lastProcessedLT interface{}, channel interface{}) *APIClientWrapped_SubscribeOnTransactions_Call { + return &APIClientWrapped_SubscribeOnTransactions_Call{Call: _e.mock.On("SubscribeOnTransactions", workerCtx, addr, lastProcessedLT, channel)} +} + +func (_c *APIClientWrapped_SubscribeOnTransactions_Call) Run(run func(workerCtx context.Context, addr *address.Address, lastProcessedLT uint64, channel chan<- *tlb.Transaction)) *APIClientWrapped_SubscribeOnTransactions_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*address.Address), args[2].(uint64), args[3].(chan<- *tlb.Transaction)) + }) + return _c +} + +func (_c *APIClientWrapped_SubscribeOnTransactions_Call) Return() *APIClientWrapped_SubscribeOnTransactions_Call { + _c.Call.Return() + return _c +} + +func (_c *APIClientWrapped_SubscribeOnTransactions_Call) RunAndReturn(run func(context.Context, *address.Address, uint64, chan<- *tlb.Transaction)) *APIClientWrapped_SubscribeOnTransactions_Call { + _c.Run(run) + return _c +} + +// VerifyProofChain provides a mock function with given fields: ctx, from, to +func (_m *APIClientWrapped) VerifyProofChain(ctx context.Context, from *tlb.BlockInfo, to *tlb.BlockInfo) error { + ret := _m.Called(ctx, from, to) + + if len(ret) == 0 { + panic("no return value specified for VerifyProofChain") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *tlb.BlockInfo, *tlb.BlockInfo) error); ok { + r0 = rf(ctx, from, to) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// APIClientWrapped_VerifyProofChain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyProofChain' +type APIClientWrapped_VerifyProofChain_Call struct { + *mock.Call +} + +// VerifyProofChain is a helper method to define mock.On call +// - ctx context.Context +// - from *tlb.BlockInfo +// - to *tlb.BlockInfo +func (_e *APIClientWrapped_Expecter) VerifyProofChain(ctx interface{}, from interface{}, to interface{}) *APIClientWrapped_VerifyProofChain_Call { + return &APIClientWrapped_VerifyProofChain_Call{Call: _e.mock.On("VerifyProofChain", ctx, from, to)} +} + +func (_c *APIClientWrapped_VerifyProofChain_Call) Run(run func(ctx context.Context, from *tlb.BlockInfo, to *tlb.BlockInfo)) *APIClientWrapped_VerifyProofChain_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*tlb.BlockInfo), args[2].(*tlb.BlockInfo)) + }) + return _c +} + +func (_c *APIClientWrapped_VerifyProofChain_Call) Return(_a0 error) *APIClientWrapped_VerifyProofChain_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *APIClientWrapped_VerifyProofChain_Call) RunAndReturn(run func(context.Context, *tlb.BlockInfo, *tlb.BlockInfo) error) *APIClientWrapped_VerifyProofChain_Call { + _c.Call.Return(run) + return _c +} + +// WaitForBlock provides a mock function with given fields: seqno +func (_m *APIClientWrapped) WaitForBlock(seqno uint32) ton.APIClientWrapped { + ret := _m.Called(seqno) + + if len(ret) == 0 { + panic("no return value specified for WaitForBlock") + } + + var r0 ton.APIClientWrapped + if rf, ok := ret.Get(0).(func(uint32) ton.APIClientWrapped); ok { + r0 = rf(seqno) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ton.APIClientWrapped) + } + } + + return r0 +} + +// APIClientWrapped_WaitForBlock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaitForBlock' +type APIClientWrapped_WaitForBlock_Call struct { + *mock.Call +} + +// WaitForBlock is a helper method to define mock.On call +// - seqno uint32 +func (_e *APIClientWrapped_Expecter) WaitForBlock(seqno interface{}) *APIClientWrapped_WaitForBlock_Call { + return &APIClientWrapped_WaitForBlock_Call{Call: _e.mock.On("WaitForBlock", seqno)} +} + +func (_c *APIClientWrapped_WaitForBlock_Call) Run(run func(seqno uint32)) *APIClientWrapped_WaitForBlock_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(uint32)) + }) + return _c +} + +func (_c *APIClientWrapped_WaitForBlock_Call) Return(_a0 ton.APIClientWrapped) *APIClientWrapped_WaitForBlock_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *APIClientWrapped_WaitForBlock_Call) RunAndReturn(run func(uint32) ton.APIClientWrapped) *APIClientWrapped_WaitForBlock_Call { + _c.Call.Return(run) + return _c +} + +// WithRetry provides a mock function with given fields: maxRetries +func (_m *APIClientWrapped) WithRetry(maxRetries ...int) ton.APIClientWrapped { + _va := make([]interface{}, len(maxRetries)) + for _i := range maxRetries { + _va[_i] = maxRetries[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for WithRetry") + } + + var r0 ton.APIClientWrapped + if rf, ok := ret.Get(0).(func(...int) ton.APIClientWrapped); ok { + r0 = rf(maxRetries...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ton.APIClientWrapped) + } + } + + return r0 +} + +// APIClientWrapped_WithRetry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithRetry' +type APIClientWrapped_WithRetry_Call struct { + *mock.Call +} + +// WithRetry is a helper method to define mock.On call +// - maxRetries ...int +func (_e *APIClientWrapped_Expecter) WithRetry(maxRetries ...interface{}) *APIClientWrapped_WithRetry_Call { + return &APIClientWrapped_WithRetry_Call{Call: _e.mock.On("WithRetry", + append([]interface{}{}, maxRetries...)...)} +} + +func (_c *APIClientWrapped_WithRetry_Call) Run(run func(maxRetries ...int)) *APIClientWrapped_WithRetry_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]int, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(int) + } + } + run(variadicArgs...) + }) + return _c +} + +func (_c *APIClientWrapped_WithRetry_Call) Return(_a0 ton.APIClientWrapped) *APIClientWrapped_WithRetry_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *APIClientWrapped_WithRetry_Call) RunAndReturn(run func(...int) ton.APIClientWrapped) *APIClientWrapped_WithRetry_Call { + _c.Call.Return(run) + return _c +} + +// WithTimeout provides a mock function with given fields: timeout +func (_m *APIClientWrapped) WithTimeout(timeout time.Duration) ton.APIClientWrapped { + ret := _m.Called(timeout) + + if len(ret) == 0 { + panic("no return value specified for WithTimeout") + } + + var r0 ton.APIClientWrapped + if rf, ok := ret.Get(0).(func(time.Duration) ton.APIClientWrapped); ok { + r0 = rf(timeout) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ton.APIClientWrapped) + } + } + + return r0 +} + +// APIClientWrapped_WithTimeout_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithTimeout' +type APIClientWrapped_WithTimeout_Call struct { + *mock.Call +} + +// WithTimeout is a helper method to define mock.On call +// - timeout time.Duration +func (_e *APIClientWrapped_Expecter) WithTimeout(timeout interface{}) *APIClientWrapped_WithTimeout_Call { + return &APIClientWrapped_WithTimeout_Call{Call: _e.mock.On("WithTimeout", timeout)} +} + +func (_c *APIClientWrapped_WithTimeout_Call) Run(run func(timeout time.Duration)) *APIClientWrapped_WithTimeout_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Duration)) + }) + return _c +} + +func (_c *APIClientWrapped_WithTimeout_Call) Return(_a0 ton.APIClientWrapped) *APIClientWrapped_WithTimeout_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *APIClientWrapped_WithTimeout_Call) RunAndReturn(run func(time.Duration) ton.APIClientWrapped) *APIClientWrapped_WithTimeout_Call { + _c.Call.Return(run) + return _c +} + +// NewAPIClientWrapped creates a new instance of APIClientWrapped. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewAPIClientWrapped(t interface { + mock.TestingT + Cleanup(func()) +}) *APIClientWrapped { + mock := &APIClientWrapped{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/sdk/ton/mocks/wallet.go b/sdk/ton/mocks/wallet.go new file mode 100644 index 00000000..f70937e2 --- /dev/null +++ b/sdk/ton/mocks/wallet.go @@ -0,0 +1,347 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package mock_ton + +import ( + context "context" + + address "github.com/xssnick/tonutils-go/address" + + mock "github.com/stretchr/testify/mock" + + tlb "github.com/xssnick/tonutils-go/tlb" + + ton "github.com/xssnick/tonutils-go/ton" +) + +// TonAPI is an autogenerated mock type for the TonAPI type +type TonAPI struct { + mock.Mock +} + +type TonAPI_Expecter struct { + mock *mock.Mock +} + +func (_m *TonAPI) EXPECT() *TonAPI_Expecter { + return &TonAPI_Expecter{mock: &_m.Mock} +} + +// CurrentMasterchainInfo provides a mock function with given fields: ctx +func (_m *TonAPI) CurrentMasterchainInfo(ctx context.Context) (*tlb.BlockInfo, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for CurrentMasterchainInfo") + } + + var r0 *tlb.BlockInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*tlb.BlockInfo, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *tlb.BlockInfo); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*tlb.BlockInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TonAPI_CurrentMasterchainInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentMasterchainInfo' +type TonAPI_CurrentMasterchainInfo_Call struct { + *mock.Call +} + +// CurrentMasterchainInfo is a helper method to define mock.On call +// - ctx context.Context +func (_e *TonAPI_Expecter) CurrentMasterchainInfo(ctx interface{}) *TonAPI_CurrentMasterchainInfo_Call { + return &TonAPI_CurrentMasterchainInfo_Call{Call: _e.mock.On("CurrentMasterchainInfo", ctx)} +} + +func (_c *TonAPI_CurrentMasterchainInfo_Call) Run(run func(ctx context.Context)) *TonAPI_CurrentMasterchainInfo_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *TonAPI_CurrentMasterchainInfo_Call) Return(_a0 *tlb.BlockInfo, _a1 error) *TonAPI_CurrentMasterchainInfo_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TonAPI_CurrentMasterchainInfo_Call) RunAndReturn(run func(context.Context) (*tlb.BlockInfo, error)) *TonAPI_CurrentMasterchainInfo_Call { + _c.Call.Return(run) + return _c +} + +// FindLastTransactionByInMsgHash provides a mock function with given fields: ctx, addr, msgHash, maxTxNumToScan +func (_m *TonAPI) FindLastTransactionByInMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) { + _va := make([]interface{}, len(maxTxNumToScan)) + for _i := range maxTxNumToScan { + _va[_i] = maxTxNumToScan[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, addr, msgHash) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for FindLastTransactionByInMsgHash") + } + + var r0 *tlb.Transaction + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *address.Address, []byte, ...int) (*tlb.Transaction, error)); ok { + return rf(ctx, addr, msgHash, maxTxNumToScan...) + } + if rf, ok := ret.Get(0).(func(context.Context, *address.Address, []byte, ...int) *tlb.Transaction); ok { + r0 = rf(ctx, addr, msgHash, maxTxNumToScan...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*tlb.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *address.Address, []byte, ...int) error); ok { + r1 = rf(ctx, addr, msgHash, maxTxNumToScan...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TonAPI_FindLastTransactionByInMsgHash_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindLastTransactionByInMsgHash' +type TonAPI_FindLastTransactionByInMsgHash_Call struct { + *mock.Call +} + +// FindLastTransactionByInMsgHash is a helper method to define mock.On call +// - ctx context.Context +// - addr *address.Address +// - msgHash []byte +// - maxTxNumToScan ...int +func (_e *TonAPI_Expecter) FindLastTransactionByInMsgHash(ctx interface{}, addr interface{}, msgHash interface{}, maxTxNumToScan ...interface{}) *TonAPI_FindLastTransactionByInMsgHash_Call { + return &TonAPI_FindLastTransactionByInMsgHash_Call{Call: _e.mock.On("FindLastTransactionByInMsgHash", + append([]interface{}{ctx, addr, msgHash}, maxTxNumToScan...)...)} +} + +func (_c *TonAPI_FindLastTransactionByInMsgHash_Call) Run(run func(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int)) *TonAPI_FindLastTransactionByInMsgHash_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]int, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(int) + } + } + run(args[0].(context.Context), args[1].(*address.Address), args[2].([]byte), variadicArgs...) + }) + return _c +} + +func (_c *TonAPI_FindLastTransactionByInMsgHash_Call) Return(_a0 *tlb.Transaction, _a1 error) *TonAPI_FindLastTransactionByInMsgHash_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TonAPI_FindLastTransactionByInMsgHash_Call) RunAndReturn(run func(context.Context, *address.Address, []byte, ...int) (*tlb.Transaction, error)) *TonAPI_FindLastTransactionByInMsgHash_Call { + _c.Call.Return(run) + return _c +} + +// SendExternalMessage provides a mock function with given fields: ctx, msg +func (_m *TonAPI) SendExternalMessage(ctx context.Context, msg *tlb.ExternalMessage) error { + ret := _m.Called(ctx, msg) + + if len(ret) == 0 { + panic("no return value specified for SendExternalMessage") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *tlb.ExternalMessage) error); ok { + r0 = rf(ctx, msg) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TonAPI_SendExternalMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendExternalMessage' +type TonAPI_SendExternalMessage_Call struct { + *mock.Call +} + +// SendExternalMessage is a helper method to define mock.On call +// - ctx context.Context +// - msg *tlb.ExternalMessage +func (_e *TonAPI_Expecter) SendExternalMessage(ctx interface{}, msg interface{}) *TonAPI_SendExternalMessage_Call { + return &TonAPI_SendExternalMessage_Call{Call: _e.mock.On("SendExternalMessage", ctx, msg)} +} + +func (_c *TonAPI_SendExternalMessage_Call) Run(run func(ctx context.Context, msg *tlb.ExternalMessage)) *TonAPI_SendExternalMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*tlb.ExternalMessage)) + }) + return _c +} + +func (_c *TonAPI_SendExternalMessage_Call) Return(_a0 error) *TonAPI_SendExternalMessage_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TonAPI_SendExternalMessage_Call) RunAndReturn(run func(context.Context, *tlb.ExternalMessage) error) *TonAPI_SendExternalMessage_Call { + _c.Call.Return(run) + return _c +} + +// SendExternalMessageWaitTransaction provides a mock function with given fields: ctx, ext +func (_m *TonAPI) SendExternalMessageWaitTransaction(ctx context.Context, ext *tlb.ExternalMessage) (*tlb.Transaction, *tlb.BlockInfo, []byte, error) { + ret := _m.Called(ctx, ext) + + if len(ret) == 0 { + panic("no return value specified for SendExternalMessageWaitTransaction") + } + + var r0 *tlb.Transaction + var r1 *tlb.BlockInfo + var r2 []byte + var r3 error + if rf, ok := ret.Get(0).(func(context.Context, *tlb.ExternalMessage) (*tlb.Transaction, *tlb.BlockInfo, []byte, error)); ok { + return rf(ctx, ext) + } + if rf, ok := ret.Get(0).(func(context.Context, *tlb.ExternalMessage) *tlb.Transaction); ok { + r0 = rf(ctx, ext) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*tlb.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *tlb.ExternalMessage) *tlb.BlockInfo); ok { + r1 = rf(ctx, ext) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*tlb.BlockInfo) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, *tlb.ExternalMessage) []byte); ok { + r2 = rf(ctx, ext) + } else { + if ret.Get(2) != nil { + r2 = ret.Get(2).([]byte) + } + } + + if rf, ok := ret.Get(3).(func(context.Context, *tlb.ExternalMessage) error); ok { + r3 = rf(ctx, ext) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + +// TonAPI_SendExternalMessageWaitTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendExternalMessageWaitTransaction' +type TonAPI_SendExternalMessageWaitTransaction_Call struct { + *mock.Call +} + +// SendExternalMessageWaitTransaction is a helper method to define mock.On call +// - ctx context.Context +// - ext *tlb.ExternalMessage +func (_e *TonAPI_Expecter) SendExternalMessageWaitTransaction(ctx interface{}, ext interface{}) *TonAPI_SendExternalMessageWaitTransaction_Call { + return &TonAPI_SendExternalMessageWaitTransaction_Call{Call: _e.mock.On("SendExternalMessageWaitTransaction", ctx, ext)} +} + +func (_c *TonAPI_SendExternalMessageWaitTransaction_Call) Run(run func(ctx context.Context, ext *tlb.ExternalMessage)) *TonAPI_SendExternalMessageWaitTransaction_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*tlb.ExternalMessage)) + }) + return _c +} + +func (_c *TonAPI_SendExternalMessageWaitTransaction_Call) Return(_a0 *tlb.Transaction, _a1 *tlb.BlockInfo, _a2 []byte, _a3 error) *TonAPI_SendExternalMessageWaitTransaction_Call { + _c.Call.Return(_a0, _a1, _a2, _a3) + return _c +} + +func (_c *TonAPI_SendExternalMessageWaitTransaction_Call) RunAndReturn(run func(context.Context, *tlb.ExternalMessage) (*tlb.Transaction, *tlb.BlockInfo, []byte, error)) *TonAPI_SendExternalMessageWaitTransaction_Call { + _c.Call.Return(run) + return _c +} + +// WaitForBlock provides a mock function with given fields: seqno +func (_m *TonAPI) WaitForBlock(seqno uint32) ton.APIClientWrapped { + ret := _m.Called(seqno) + + if len(ret) == 0 { + panic("no return value specified for WaitForBlock") + } + + var r0 ton.APIClientWrapped + if rf, ok := ret.Get(0).(func(uint32) ton.APIClientWrapped); ok { + r0 = rf(seqno) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ton.APIClientWrapped) + } + } + + return r0 +} + +// TonAPI_WaitForBlock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaitForBlock' +type TonAPI_WaitForBlock_Call struct { + *mock.Call +} + +// WaitForBlock is a helper method to define mock.On call +// - seqno uint32 +func (_e *TonAPI_Expecter) WaitForBlock(seqno interface{}) *TonAPI_WaitForBlock_Call { + return &TonAPI_WaitForBlock_Call{Call: _e.mock.On("WaitForBlock", seqno)} +} + +func (_c *TonAPI_WaitForBlock_Call) Run(run func(seqno uint32)) *TonAPI_WaitForBlock_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(uint32)) + }) + return _c +} + +func (_c *TonAPI_WaitForBlock_Call) Return(_a0 ton.APIClientWrapped) *TonAPI_WaitForBlock_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TonAPI_WaitForBlock_Call) RunAndReturn(run func(uint32) ton.APIClientWrapped) *TonAPI_WaitForBlock_Call { + _c.Call.Return(run) + return _c +} + +// NewTonAPI creates a new instance of TonAPI. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewTonAPI(t interface { + mock.TestingT + Cleanup(func()) +}) *TonAPI { + mock := &TonAPI{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/sdk/ton/timelock_converter.go b/sdk/ton/timelock_converter.go new file mode 100644 index 00000000..784cbc58 --- /dev/null +++ b/sdk/ton/timelock_converter.go @@ -0,0 +1,168 @@ +package ton + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/smartcontractkit/mcms/sdk" + sdkerrors "github.com/smartcontractkit/mcms/sdk/errors" + "github.com/smartcontractkit/mcms/types" + + "github.com/ethereum/go-ethereum/common" + + "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/mcms/timelock" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tlbe" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" +) + +// Default amount to send with timelock transactions (to cover gas fees) +var DefaultSendAmount = tlb.MustFromTON("0.15") + +var _ sdk.TimelockConverter = (*timelockConverter)(nil) + +type timelockConverter struct { + // Transaction opts + amount tlb.Coins +} + +// NewTimelockConverter creates a new TimelockConverter +func NewTimelockConverter(amount tlb.Coins) sdk.TimelockConverter { + return &timelockConverter{ + amount: amount, + } +} + +func (t *timelockConverter) ConvertBatchToChainOperations( + _ context.Context, + metadata types.ChainMetadata, + bop types.BatchOperation, + timelockAddress string, + mcmAddress string, + delay types.Duration, + action types.TimelockAction, + predecessor common.Hash, + salt common.Hash, +) ([]types.Operation, common.Hash, error) { + // Create the list of RBACTimelockCall (batch of calls) and tags for the operations + calls, err := ConvertBatchToCalls(bop) + if err != nil { + return []types.Operation{}, common.Hash{}, fmt.Errorf("failed to convert batch to calls: %w", err) + } + + tags := make([]string, 0) + for _, tx := range bop.Transactions { + tags = append(tags, tx.Tags...) + } + + operationID, errHash := HashOperationBatch(calls, predecessor, salt) + if errHash != nil { + return []types.Operation{}, common.Hash{}, errHash + } + + qID, err := tvm.RandomQueryID() + if err != nil { + return []types.Operation{}, common.Hash{}, fmt.Errorf("failed to generate random query ID: %w", err) + } + + // Encode the data based on the operation + var data *cell.Cell + switch action { + case types.TimelockActionSchedule: + data, err = tlb.ToCell(timelock.ScheduleBatch{ + QueryID: qID, + + Calls: calls, + Predecessor: tlbe.NewUint256(predecessor.Big()), + Salt: tlbe.NewUint256(salt.Big()), + Delay: uint32(delay.Seconds()), + }) + case types.TimelockActionCancel: + data, err = tlb.ToCell(timelock.Cancel{ + QueryID: qID, + + ID: tlbe.NewUint256(operationID.Big()), + }) + case types.TimelockActionBypass: + data, err = tlb.ToCell(timelock.BypasserExecuteBatch{ + QueryID: qID, + + Calls: calls, + }) + default: + return []types.Operation{}, common.Hash{}, sdkerrors.NewInvalidTimelockOperationError(string(action)) + } + + if err != nil { + return []types.Operation{}, common.Hash{}, fmt.Errorf("failed to encode timelock action data: %w", err) + } + + // Map to Ton Address type + dstAddr, err := address.ParseAddr(timelockAddress) + if err != nil { + return []types.Operation{}, common.Hash{}, fmt.Errorf("invalid timelock address: %w", err) + } + + // Notice: EVM just sets 0 here, but on TON we need to set some value to cover gas fees + var tx types.Transaction + tx, err = NewTransaction(dstAddr, data.BeginParse(), t.amount.Nano(), "RBACTimelock", tags) + if err != nil { + return []types.Operation{}, common.Hash{}, fmt.Errorf("failed to create transaction: %w", err) + } + + op := types.Operation{ + ChainSelector: bop.ChainSelector, + Transaction: tx, + } + + return []types.Operation{op}, operationID, nil +} + +func ConvertBatchToCalls(bop types.BatchOperation) ([]timelock.Call, error) { + calls := make([]timelock.Call, len(bop.Transactions)) + for i, tx := range bop.Transactions { + // Unmarshal the AdditionalFields from the operation + var additionalFields AdditionalFields + if err := json.Unmarshal(tx.AdditionalFields, &additionalFields); err != nil { + return nil, fmt.Errorf("failed to unmarshal additional fields: %w", err) + } + + // Map to Ton Address type + to, err := address.ParseAddr(tx.To) + if err != nil { + return nil, fmt.Errorf("invalid target address: %w", err) + } + + datac, err := cell.FromBOC(tx.Data) + if err != nil { + return nil, fmt.Errorf("invalid cell BOC data: %w", err) + } + + calls[i] = timelock.Call{ + Target: to, + Data: datac, + Value: tlb.FromNanoTON(additionalFields.Value), + } + } + + return calls, nil +} + +// HashOperationBatch replicates the hash calculation from Solidity +func HashOperationBatch(calls []timelock.Call, predecessor, salt common.Hash) (common.Hash, error) { + ob, err := tlb.ToCell(timelock.OperationBatch{ + Calls: calls, + Predecessor: tlbe.NewUint256(predecessor.Big()), + Salt: tlbe.NewUint256(salt.Big()), + }) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to encode OperationBatch: %w", err) + } + + // Return the hash as a [32]byte array + return common.BytesToHash(ob.Hash()), nil +} diff --git a/sdk/ton/timelock_converter_test.go b/sdk/ton/timelock_converter_test.go new file mode 100644 index 00000000..34b9f31f --- /dev/null +++ b/sdk/ton/timelock_converter_test.go @@ -0,0 +1,156 @@ +package ton_test + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + + cselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tvm/cell" + + sdkerrors "github.com/smartcontractkit/mcms/sdk/errors" + "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/mcms/sdk/ton" +) + +func TestTimelockConverter_ConvertBatchToChainOperation(t *testing.T) { + t.Parallel() + + ctx := context.Background() + timelockAddress := "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8" + mcmAddress := "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8" + zeroHash := common.Hash{} + + testOp := types.BatchOperation{ + Transactions: []types.Transaction{must(ton.NewTransaction( + address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8"), + cell.BeginCell().MustStoreBinarySnake([]byte("data")).ToSlice(), + new(big.Int).SetUint64(1000), + "RBACTimelock", + []string{"tag1", "tag2"}, + ))}, + ChainSelector: types.ChainSelector(cselectors.TON_TESTNET.Selector), + } + + testCases := []struct { + name string + metadata types.ChainMetadata + op types.BatchOperation + delay string + operation types.TimelockAction + predecessor common.Hash + salt common.Hash + wantErr string + expectedOpType string + }{ + { + name: "Schedule operation", + op: testOp, + delay: "1h", + operation: types.TimelockActionSchedule, + predecessor: zeroHash, + salt: zeroHash, + expectedOpType: "RBACTimelock", + }, + { + name: "Cancel operation", + op: testOp, + delay: "1h", + operation: types.TimelockActionCancel, + predecessor: zeroHash, + salt: zeroHash, + expectedOpType: "RBACTimelock", + }, + { + name: "Schedule operation", + op: testOp, + delay: "1h", + operation: types.TimelockActionBypass, + predecessor: zeroHash, + salt: zeroHash, + expectedOpType: "RBACTimelock", + }, + { + name: "Invalid operation", + op: testOp, + delay: "1h", + operation: types.TimelockAction("invalid"), + predecessor: zeroHash, + salt: zeroHash, + wantErr: sdkerrors.NewInvalidTimelockOperationError("invalid").Error(), + expectedOpType: "", + }, + { + name: "Invalid additional fields", + op: types.BatchOperation{ + Transactions: []types.Transaction{{ + OperationMetadata: types.OperationMetadata{ContractType: "RBACTimelock"}, + To: timelockAddress, + Data: []byte("0x1234"), + AdditionalFields: []byte("invalid"), + }}, + ChainSelector: types.ChainSelector(cselectors.TON_TESTNET.Selector), + }, + delay: "1h", + operation: types.TimelockActionSchedule, + predecessor: zeroHash, + salt: zeroHash, + wantErr: "failed to unmarshal additional fields: invalid character 'i' looking for beginning of value", + }, + { + name: "Invalid address in transaction", + op: types.BatchOperation{ + Transactions: []types.Transaction{{ + OperationMetadata: types.OperationMetadata{ContractType: "RBACTimelock"}, + To: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-", // invalid address + Data: []byte("0x1234"), + AdditionalFields: []byte("{\"value\":1000}"), + }}, + ChainSelector: types.ChainSelector(cselectors.TON_TESTNET.Selector), + }, + delay: "1h", + operation: types.TimelockActionSchedule, + predecessor: zeroHash, + salt: zeroHash, + wantErr: "failed to convert batch to calls: invalid target address: incorrect address data", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + converter := ton.NewTimelockConverter(ton.DefaultSendAmount) + chainOperations, operationID, err := converter.ConvertBatchToChainOperations( + ctx, + tc.metadata, + tc.op, + timelockAddress, + mcmAddress, + types.MustParseDuration(tc.delay), + tc.operation, + tc.predecessor, + tc.salt, + ) + + if tc.wantErr != "" { + require.Error(t, err) + require.ErrorContains(t, err, tc.wantErr) + } else { + require.NoError(t, err) + assert.NotEqual(t, common.Hash{}, operationID) + assert.Len(t, chainOperations, 1) + assert.Equal(t, timelockAddress, chainOperations[0].Transaction.To) + assert.Equal(t, tc.op.ChainSelector, chainOperations[0].ChainSelector) + } + }) + } +} diff --git a/sdk/ton/timelock_executor.go b/sdk/ton/timelock_executor.go new file mode 100644 index 00000000..7f0ab4e8 --- /dev/null +++ b/sdk/ton/timelock_executor.go @@ -0,0 +1,97 @@ +package ton + +import ( + "context" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/samber/lo" + + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/types" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/ton/wallet" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/timelock" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tlbe" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" +) + +var _ sdk.TimelockExecutor = (*timelockExecutor)(nil) + +// sdk.TimelockExecutor implementation for TON chains for accessing the RBACTimelock contract +type timelockExecutor struct { + sdk.TimelockInspector + wallet *wallet.Wallet + + // Transaction opts + amount tlb.Coins +} + +type TimelockExecutorOpts struct { + Client ton.APIClientWrapped + Wallet *wallet.Wallet + Amount tlb.Coins +} + +// NewTimelockExecutor creates a new TimelockExecutor +func NewTimelockExecutor(opts TimelockExecutorOpts) (sdk.TimelockExecutor, error) { + if lo.IsNil(opts.Client) { + return nil, errors.New("failed to create sdk.Executor - client (ton.APIClientWrapped) is nil") + } + + if opts.Wallet == nil { + return nil, errors.New("failed to create sdk.Executor - wallet (*wallet.Wallet) is nil") + } + + return &timelockExecutor{ + TimelockInspector: NewTimelockInspector(opts.Client), + wallet: opts.Wallet, + amount: opts.Amount, + }, nil +} + +func (e *timelockExecutor) Execute( + ctx context.Context, bop types.BatchOperation, timelockAddress string, predecessor common.Hash, salt common.Hash, +) (types.TransactionResult, error) { + // Map to Ton Address type + dstAddr, err := address.ParseAddr(timelockAddress) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("invalid timelock address: %w", err) + } + + calls, err := ConvertBatchToCalls(bop) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to convert batch to calls: %w", err) + } + + qID, err := tvm.RandomQueryID() + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to generate random query ID: %w", err) + } + + body, err := tlb.ToCell(timelock.ExecuteBatch{ + QueryID: qID, + + Calls: calls, + Predecessor: tlbe.NewUint256(predecessor.Big()), + Salt: tlbe.NewUint256(salt.Big()), + }) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to encode ExecuteBatch body: %w", err) + } + + skipSend := false // TODO: expose via executor options + + return SendTx(ctx, TxOpts{ + Wallet: e.wallet, + DstAddr: dstAddr, + Amount: e.amount, + Body: body, + SkipSend: skipSend, + }) +} diff --git a/sdk/ton/timelock_executor_test.go b/sdk/ton/timelock_executor_test.go new file mode 100644 index 00000000..fb211d18 --- /dev/null +++ b/sdk/ton/timelock_executor_test.go @@ -0,0 +1,203 @@ +package ton_test + +import ( + "context" + "encoding/json" + "errors" + "math/big" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + "github.com/smartcontractkit/mcms/internal/testutils/chaintest" + "github.com/smartcontractkit/mcms/types" + + mcmston "github.com/smartcontractkit/mcms/sdk/ton" + ton_mocks "github.com/smartcontractkit/mcms/sdk/ton/mocks" +) + +func TestTimelockExecutor_NewTimelockExecutor(t *testing.T) { + t.Parallel() + + chainID := chaintest.Chain7TONID + amount := tlb.MustFromTON("0.1") + + tests := []struct { + name string + mutate func(opts mcmston.TimelockExecutorOpts) mcmston.TimelockExecutorOpts + wantErr string + }{ + { + name: "success", + mutate: func(opts mcmston.TimelockExecutorOpts) mcmston.TimelockExecutorOpts { + return opts + }, + wantErr: "", + }, + { + name: "nil client", + mutate: func(opts mcmston.TimelockExecutorOpts) mcmston.TimelockExecutorOpts { + opts.Client = nil + + return opts + }, + wantErr: "failed to create sdk.Executor - client (ton.APIClientWrapped) is nil", + }, + { + name: "nil wallet", + mutate: func(opts mcmston.TimelockExecutorOpts) mcmston.TimelockExecutorOpts { + opts.Wallet = nil + + return opts + }, + wantErr: "failed to create sdk.Executor - wallet (*wallet.Wallet) is nil", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _api := ton_mocks.NewTonAPI(t) + walletOperator := must(tvm.NewRandomV5R1TestWallet(_api, chainID)) + var client ton.APIClientWrapped = ton_mocks.NewAPIClientWrapped(t) + + opts := tt.mutate(mcmston.TimelockExecutorOpts{ + Client: client, + Wallet: walletOperator, + Amount: amount, + }) + + executor, err := mcmston.NewTimelockExecutor(opts) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + require.Nil(t, executor) + + return + } + + require.NoError(t, err) + require.NotNil(t, executor) + }) + } +} + +func TestTimelockExecutor_Execute(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + sharedMockSetup := func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) { + // Mock CurrentMasterchainInfo + api.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + // Mock WaitForBlock + client.EXPECT().GetAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&tlb.Account{}, nil) + + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(ton.NewExecutionResult([]any{big.NewInt(5)}), nil) + + api.EXPECT().WaitForBlock(mock.Anything). + Return(client) + } + + tests := []struct { + name string + timelockAddress string + bop types.BatchOperation + predecessor common.Hash + salt common.Hash + mockSetup func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) + wantTxHash string + wantErr error + }{ + { + name: "success", + timelockAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + bop: types.BatchOperation{ + ChainSelector: chaintest.Chain7Selector, + Transactions: []types.Transaction{ + { + To: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + Data: cell.BeginCell().MustStoreBinarySnake([]byte{1, 2, 3}).EndCell().ToBOC(), + AdditionalFields: json.RawMessage(`{"value": 0}`)}, + }, + }, + mockSetup: func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) { + // Successful tx send + sharedMockSetup(api, client) + + // Mock SendTransaction to return (no error) + api.EXPECT().SendExternalMessageWaitTransaction(mock.Anything, mock.Anything). + Return(&tlb.Transaction{Hash: []byte{1, 2, 3, 4, 14}}, &ton.BlockIDExt{}, []byte{}, nil) + }, + wantTxHash: "010203040e", + wantErr: nil, + }, + { + name: "failure in tx execution", + timelockAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + bop: types.BatchOperation{ + ChainSelector: chaintest.Chain7Selector, + Transactions: []types.Transaction{ + { + To: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + Data: cell.BeginCell().MustStoreBinarySnake([]byte{1, 2, 3}).EndCell().ToBOC(), + AdditionalFields: json.RawMessage(`{"value": 0}`)}, + }, + }, + mockSetup: func(api *ton_mocks.TonAPI, client *ton_mocks.APIClientWrapped) { + // Error tx send + sharedMockSetup(api, client) + + // Mock SendTransaction to return an error + api.EXPECT().SendExternalMessageWaitTransaction(mock.Anything, mock.Anything). + Return(&tlb.Transaction{Hash: []byte{1, 2, 3, 4, 14}}, &ton.BlockIDExt{}, []byte{}, errors.New("error during tx send")) + }, + wantTxHash: "", + wantErr: errors.New("failed to send transaction: error during tx send"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Initialize the mock + chainID := chaintest.Chain7TONID + _api := ton_mocks.NewTonAPI(t) + walletOperator := must(tvm.NewRandomV5R1TestWallet(_api, chainID)) + + client := ton_mocks.NewAPIClientWrapped(t) + + if tt.mockSetup != nil { + tt.mockSetup(_api, client) + } + + executor, err := mcmston.NewTimelockExecutor(mcmston.TimelockExecutorOpts{ + Client: client, + Wallet: walletOperator, + Amount: tlb.MustFromTON("0.1"), + }) + require.NoError(t, err) + + tx, err := executor.Execute(ctx, tt.bop, tt.timelockAddress, tt.predecessor, tt.salt) + require.Equal(t, tt.wantTxHash, tx.Hash) + if tt.wantErr != nil { + require.EqualError(t, err, tt.wantErr.Error()) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/sdk/ton/timelock_inspector.go b/sdk/ton/timelock_inspector.go new file mode 100644 index 00000000..fc7acdc1 --- /dev/null +++ b/sdk/ton/timelock_inspector.go @@ -0,0 +1,126 @@ +package ton + +import ( + "context" + "fmt" + "math/big" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/ton" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/lib/access/rbac" + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/timelock" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + "github.com/smartcontractkit/mcms/sdk" +) + +var _ sdk.TimelockInspector = (*TimelockInspector)(nil) + +// TimelockInspector is an Inspector implementation for TON, for accessing the MCMS-Timelock contract +type TimelockInspector struct { + client ton.APIClientWrapped +} + +func NewTimelockInspector(client ton.APIClientWrapped) sdk.TimelockInspector { + return &TimelockInspector{client} +} + +func (i TimelockInspector) GetMinDelay(ctx context.Context, _address string) (uint64, error) { + addr, block, err := ParseAddrGetBlock(ctx, i.client, _address) + if err != nil { + return 0, err + } + + return tvm.CallGetter(ctx, i.client, block, addr, timelock.GetMinDelay) +} + +// GetAdmins returns the list of addresses with the admin role +func (i TimelockInspector) GetAdmins(ctx context.Context, addr string) ([]string, error) { + return i.getRoleMembers(ctx, addr, [32]byte(timelock.RoleAdmin.Bytes())) +} + +// GetProposers returns the list of addresses with the proposer role +func (i TimelockInspector) GetProposers(ctx context.Context, addr string) ([]string, error) { + return i.getRoleMembers(ctx, addr, [32]byte(timelock.RoleProposer.Bytes())) +} + +// GetExecutors returns the list of addresses with the executor role +func (i TimelockInspector) GetExecutors(ctx context.Context, addr string) ([]string, error) { + return i.getRoleMembers(ctx, addr, [32]byte(timelock.RoleExecutor.Bytes())) +} + +// GetBypassers returns the list of addresses with the bypasser role +func (i TimelockInspector) GetBypassers(ctx context.Context, addr string) ([]string, error) { + return i.getRoleMembers(ctx, addr, [32]byte(timelock.RoleBypasser.Bytes())) +} + +// GetCancellers returns the list of addresses with the canceller role +func (i TimelockInspector) GetCancellers(ctx context.Context, addr string) ([]string, error) { + return i.getRoleMembers(ctx, addr, [32]byte(timelock.RoleCanceller.Bytes())) +} + +// GetOracles returns the list of addresses with the oracle role +func (i TimelockInspector) GetOracles(ctx context.Context, addr string) ([]string, error) { + return i.getRoleMembers(ctx, addr, [32]byte(timelock.RoleOracle.Bytes())) +} + +// getRoleMembers returns the list of addresses with the given role +func (i TimelockInspector) getRoleMembers(ctx context.Context, _address string, role [32]byte) ([]string, error) { + // Map to Ton Address type (timelock.address) + addr, err := address.ParseAddr(_address) + if err != nil { + return nil, fmt.Errorf("invalid timelock address: %w", err) + } + + _role := new(big.Int).SetBytes(role[:]) + _addresses, err := rbac.GetRoleMembersView(ctx, i.client, addr, _role) + if err != nil { + return nil, fmt.Errorf("error calling GetRoleMembersView: %w", err) + } + + n := len(_addresses) + addresses := make([]string, n) + for j := range n { + addresses[j] = _addresses[j].String() + } + + return addresses, nil +} + +func (i TimelockInspector) IsOperation(ctx context.Context, _address string, opID [32]byte) (bool, error) { + return i.isOperationFor(ctx, _address, opID, timelock.IsOperation) +} + +func (i TimelockInspector) IsOperationPending(ctx context.Context, _address string, opID [32]byte) (bool, error) { + return i.isOperationFor(ctx, _address, opID, timelock.IsOperationPending) +} + +func (i TimelockInspector) IsOperationReady(ctx context.Context, _address string, opID [32]byte) (bool, error) { + return i.isOperationFor(ctx, _address, opID, timelock.IsOperationReady) +} + +func (i TimelockInspector) IsOperationDone(ctx context.Context, _address string, opID [32]byte) (bool, error) { + return i.isOperationFor(ctx, _address, opID, timelock.IsOperationDone) +} + +func (i TimelockInspector) IsOperationError(ctx context.Context, _address string, opID [32]byte) (bool, error) { + return i.isOperationFor(ctx, _address, opID, timelock.IsOperationError) +} + +// isOperationFor is a helper function to check the status of an operation using the provided getter +func (i TimelockInspector) isOperationFor( + ctx context.Context, + _address string, + opID [32]byte, + getter tvm.Getter[*big.Int, bool], +) (bool, error) { + addr, block, err := ParseAddrGetBlock(ctx, i.client, _address) + if err != nil { + return false, err + } + + _opID := new(big.Int).SetBytes(opID[:]) + + return tvm.CallGetter(ctx, i.client, block, addr, getter, _opID) +} diff --git a/sdk/ton/timelock_inspector_test.go b/sdk/ton/timelock_inspector_test.go new file mode 100644 index 00000000..a5a0c6df --- /dev/null +++ b/sdk/ton/timelock_inspector_test.go @@ -0,0 +1,568 @@ +package ton_test + +import ( + "context" + "errors" + "math/big" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/ton/wallet" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" + + "github.com/smartcontractkit/mcms/internal/testutils/chaintest" + + mcmston "github.com/smartcontractkit/mcms/sdk/ton" + ton_mocks "github.com/smartcontractkit/mcms/sdk/ton/mocks" +) + +type roleFetchTest struct { + name string + address string + roleMemberCount *big.Int + roleMembers []*address.Address + proposerRole [32]byte + mockError error + want []string + wantErr error + roleFetchType string // Specify the role type (proposers, executors, etc.) +} + +// Helper to mock contract calls for each role test case +func (tt roleFetchTest) mockRoleContractCalls(t *testing.T, client *ton_mocks.APIClientWrapped) { + t.Helper() + + // Mock CurrentMasterchainInfo + client.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + // Mock response for role member count + encodedRoleMemberCount := tt.roleMemberCount + r := ton.NewExecutionResult([]any{encodedRoleMemberCount}) + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(r, nil).Once() + + // Mock response for each role member + for _, member := range tt.roleMembers { + encodedMember := cell.BeginCell().MustStoreAddr(member).ToSlice() + + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(ton.NewExecutionResult([]any{encodedMember}), nil).Once() + } +} + +func TestTimelockInspector_GetRolesTests(t *testing.T) { + t.Parallel() + + var chainID = chaintest.Chain7TONID + var client *ton.APIClient + var wallets = []*wallet.Wallet{ + must(tvm.NewRandomV5R1TestWallet(client, chainID)), + must(tvm.NewRandomV5R1TestWallet(client, chainID)), + must(tvm.NewRandomV5R1TestWallet(client, chainID)), + must(tvm.NewRandomV5R1TestWallet(client, chainID)), + must(tvm.NewRandomV5R1TestWallet(client, chainID)), + must(tvm.NewRandomV5R1TestWallet(client, chainID)), + must(tvm.NewRandomV5R1TestWallet(client, chainID)), + must(tvm.NewRandomV5R1TestWallet(client, chainID)), + } + + ctx := context.Background() + tests := []roleFetchTest{ + { + name: "GetProposers success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + roleMemberCount: big.NewInt(3), + roleMembers: []*address.Address{ + wallets[0].Address(), + wallets[1].Address(), + wallets[2].Address(), + }, + proposerRole: [32]byte{0x01}, + want: []string{ + wallets[0].Address().String(), + wallets[1].Address().String(), + wallets[2].Address().String(), + }, + roleFetchType: "proposers", + }, + { + name: "GetProposers call contract failure error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockError: errors.New("call to contract failed"), + want: nil, + wantErr: errors.New("error calling GetRoleMembersView: tvm: failed to run get method \"getRoleMemberCount\": call to contract failed"), + roleFetchType: "proposers", + }, + { + name: "GetExecutors success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + roleMemberCount: big.NewInt(3), + roleMembers: []*address.Address{ + wallets[0].Address(), + wallets[1].Address(), + wallets[2].Address(), + }, + proposerRole: [32]byte{0x01}, + want: []string{ + wallets[0].Address().String(), + wallets[1].Address().String(), + wallets[2].Address().String(), + }, + roleFetchType: "executors", + }, + { + name: "GetExecutors call contract failure error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockError: errors.New("call to contract failed"), + want: nil, + wantErr: errors.New("error calling GetRoleMembersView: tvm: failed to run get method \"getRoleMemberCount\": call to contract failed"), + roleFetchType: "executors", + }, + { + name: "GetExecutors success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + roleMemberCount: big.NewInt(3), + roleMembers: []*address.Address{ + wallets[0].Address(), + wallets[1].Address(), + wallets[2].Address(), + }, + proposerRole: [32]byte{0x01}, + want: []string{ + wallets[0].Address().String(), + wallets[1].Address().String(), + wallets[2].Address().String(), + }, + roleFetchType: "executors", + }, + { + name: "GetExecutors call contract failure error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockError: errors.New("call to contract failed"), + want: nil, + wantErr: errors.New("error calling GetRoleMembersView: tvm: failed to run get method \"getRoleMemberCount\": call to contract failed"), + roleFetchType: "executors", + }, + { + name: "GetBypassers success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + roleMemberCount: big.NewInt(3), + roleMembers: []*address.Address{ + wallets[0].Address(), + wallets[1].Address(), + wallets[2].Address(), + }, + proposerRole: [32]byte{0x01}, + want: []string{ + wallets[0].Address().String(), + wallets[1].Address().String(), + wallets[2].Address().String(), + }, + roleFetchType: "bypassers", + }, + { + name: "GetBypassers call contract failure error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockError: errors.New("call to contract failed"), + want: nil, + wantErr: errors.New("error calling GetRoleMembersView: tvm: failed to run get method \"getRoleMemberCount\": call to contract failed"), + roleFetchType: "bypassers", + }, + { + name: "GetCancellers success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + roleMemberCount: big.NewInt(3), + roleMembers: []*address.Address{ + wallets[0].Address(), + wallets[1].Address(), + wallets[2].Address(), + }, + proposerRole: [32]byte{0x01}, + want: []string{ + wallets[0].Address().String(), + wallets[1].Address().String(), + wallets[2].Address().String(), + }, + roleFetchType: "bypassers", + }, + { + name: "GetCancellers call contract failure error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockError: errors.New("call to contract failed"), + want: nil, + wantErr: errors.New("error calling GetRoleMembersView: tvm: failed to run get method \"getRoleMemberCount\": call to contract failed"), + roleFetchType: "cancellers", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create a new mock client and inspector for each test case + client := ton_mocks.NewAPIClientWrapped(t) + inspector := mcmston.NewTimelockInspector(client) + + // Mock the contract calls based on the test case + if tt.mockError == nil { + tt.mockRoleContractCalls(t, client) + } else { + // Mock CurrentMasterchainInfo + client.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + // If there's an error, mock it on the first CallContract call + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, tt.mockError).Once() + } + + // Select and call the appropriate role-fetching function + var got []string + var err error + switch tt.roleFetchType { + case "proposers": + got, err = inspector.GetProposers(ctx, tt.address) + case "executors": + got, err = inspector.GetExecutors(ctx, tt.address) + case "cancellers": + got, err = inspector.GetCancellers(ctx, tt.address) + case "bypassers": + got, err = inspector.GetBypassers(ctx, tt.address) + default: + t.Fatalf("unsupported roleFetchType: %s", tt.roleFetchType) + } + + // Assertions for expected error or successful result + if tt.wantErr != nil { + require.Error(t, err) + require.EqualError(t, err, tt.wantErr.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + + // Verify expectations + client.AssertExpectations(t) + }) + } +} + +func TestTimelockInspector_IsOperation(t *testing.T) { + t.Parallel() + + ctx := context.Background() + tests := []struct { + name string + address string + opID [32]byte + mockError error + want bool + wantErr error + }{ + { + name: "IsOperation success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + opID: [32]byte{0x01}, + want: true, + }, + { + name: "IsOperation call contract failure error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + opID: [32]byte{0x02}, + mockError: errors.New("call to contract failed"), + want: false, + wantErr: errors.New("tvm: failed to run get method \"isOperation\": call to contract failed"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create a new mock client and inspector for each test case + client := ton_mocks.NewAPIClientWrapped(t) + inspector := mcmston.NewTimelockInspector(client) + + // Mock the contract call based on the test case + // Mock CurrentMasterchainInfo + client.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + if tt.mockError == nil { + // Encode the expected `IsOperation` return value for a successful call + wantInt := 0 + if tt.want { + wantInt = 1 + } + r := ton.NewExecutionResult([]any{big.NewInt(int64(wantInt))}) + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(r, nil).Once() + } else { + // If there's an error, mock it on the first CallContract call + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, tt.mockError).Once() + } + + // Call the `IsOperation` method + got, err := inspector.IsOperation(ctx, tt.address, tt.opID) + + // Assertions for expected error or successful result + if tt.wantErr != nil { + require.Error(t, err) + require.EqualError(t, err, tt.wantErr.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + + // Verify expectations + client.AssertExpectations(t) + }) + } +} + +// Helper function to test the various "IsOperation" states +func testIsOperationState( + t *testing.T, + methodName string, + addr string, + opID [32]byte, + want bool, + mockError error, + wantErr error, +) { + t.Helper() + + ctx := context.Background() + + // Create a new mock client and inspector for each test case + client := ton_mocks.NewAPIClientWrapped(t) + inspector := mcmston.NewTimelockInspector(client) + + // Mock the contract call based on the test case + // Mock CurrentMasterchainInfo + client.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + if mockError == nil { + // Encode the expected `IsOperation` return value for a successful call + wantInt := 0 + if want { + wantInt = 1 + } + r := ton.NewExecutionResult([]any{big.NewInt(int64(wantInt))}) + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(r, nil).Once() + } else { + // If there's an error, mock it on the first CallContract call + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, mockError).Once() + } + + // Call the respective method based on methodName + var got bool + var err error + switch methodName { + case "isOperationPending": + got, err = inspector.IsOperationPending(ctx, addr, opID) + case "isOperationReady": + got, err = inspector.IsOperationReady(ctx, addr, opID) + case "isOperationDone": + got, err = inspector.IsOperationDone(ctx, addr, opID) + default: + t.Fatalf("unsupported methodName: %s", methodName) + } + + // Assertions for expected error or successful result + if wantErr != nil { + require.Error(t, err) + require.EqualError(t, err, wantErr.Error()) + } else { + require.NoError(t, err) + require.Equal(t, want, got) + } + + // Verify expectations + client.AssertExpectations(t) +} + +// Individual test functions calling the helper function with specific method names +func TestTimelockInspector_IsOperationPending(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + address string + opID [32]byte + want bool + mockError error + wantErr error + }{ + { + name: "IsOperationPending success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + opID: [32]byte{0x01}, + want: true, + }, + { + name: "IsOperationPending call contract failure error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + opID: [32]byte{0x02}, + mockError: errors.New("call to contract failed"), + want: false, + wantErr: errors.New("tvm: failed to run get method \"isOperationPending\": call to contract failed"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + testIsOperationState(t, "isOperationPending", tt.address, tt.opID, tt.want, tt.mockError, tt.wantErr) + }) + } +} + +func TestTimelockInspector_IsOperationReady(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + address string + opID [32]byte + want bool + mockError error + wantErr error + }{ + { + name: "IsOperationReady success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + opID: [32]byte{0x01}, + want: true, + }, + { + name: "IsOperationReady call contract failure error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + opID: [32]byte{0x02}, + mockError: errors.New("call to contract failed"), + want: false, + wantErr: errors.New("tvm: failed to run get method \"isOperationReady\": call to contract failed"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + testIsOperationState(t, "isOperationReady", tt.address, tt.opID, tt.want, tt.mockError, tt.wantErr) + }) + } +} + +func TestTimelockInspector_IsOperationDone(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + address string + opID [32]byte + want bool + mockError error + wantErr error + }{ + { + name: "IsOperationDone success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + opID: [32]byte{0x01}, + want: true, + }, + { + name: "IsOperationDone call contract failure error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + opID: [32]byte{0x02}, + mockError: errors.New("call to contract failed"), + want: false, + wantErr: errors.New("tvm: failed to run get method \"isOperationDone\": call to contract failed"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + testIsOperationState(t, "isOperationDone", tt.address, tt.opID, tt.want, tt.mockError, tt.wantErr) + }) + } +} + +func TestTimelockInspector_GetMinDelay(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + tests := []struct { + name string + address string + minDelay *big.Int + mockError error + want uint64 + wantErr error + }{ + { + name: "GetMinDelay success", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + minDelay: big.NewInt(300), + want: 300, + }, + { + name: "GetMinDelay call contract failure error", + address: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + mockError: errors.New("call to contract failed"), + want: 0, + wantErr: errors.New("tvm: failed to run get method \"getMinDelay\": call to contract failed"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create a new mock client and inspector for each test case + client := ton_mocks.NewAPIClientWrapped(t) + inspector := mcmston.NewTimelockInspector(client) + + // Mock CurrentMasterchainInfo + client.EXPECT().CurrentMasterchainInfo(mock.Anything). + Return(&ton.BlockIDExt{}, nil) + + if tt.mockError == nil { + // Encode the expected `getMinDelay` return value for a successful call + r := ton.NewExecutionResult([]any{tt.minDelay}) + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(r, nil).Once() + } else { + // Simulate a low-level call failure + client.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, tt.mockError).Once() + } + + // Act + got, err := inspector.GetMinDelay(ctx, tt.address) + + // Assert + if tt.wantErr != nil { + require.Error(t, err) + require.EqualError(t, err, tt.wantErr.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + + client.AssertExpectations(t) + }) + } +} diff --git a/sdk/ton/transaction.go b/sdk/ton/transaction.go new file mode 100644 index 00000000..7b983856 --- /dev/null +++ b/sdk/ton/transaction.go @@ -0,0 +1,68 @@ +package ton + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/mcms/types" +) + +func ValidateAdditionalFields(additionalFields json.RawMessage) error { + fields := AdditionalFields{ + Value: big.NewInt(0), + } + if len(additionalFields) != 0 { + if err := json.Unmarshal(additionalFields, &fields); err != nil { + return fmt.Errorf("failed to unmarshal TON additional fields: %w", err) + } + } + + return fields.Validate() +} + +type AdditionalFields struct { + Value *big.Int `json:"value"` +} + +// Validate ensures the TON-specific fields are correct +func (f AdditionalFields) Validate() error { + if f.Value == nil || f.Value.Sign() < 0 { + return fmt.Errorf("invalid TON value: %v", f.Value) + } + + return nil +} + +// TODO: should use a generic type and an interface to define this (method to create generic types.Transaction from a specific type [S]) +func NewTransaction( + to *address.Address, + body *cell.Slice, + value *big.Int, + contractType string, + tags []string, +) (types.Transaction, error) { + additionalFields, err := json.Marshal(AdditionalFields{Value: value}) + if err != nil { + return types.Transaction{}, fmt.Errorf("failed to marshal additional fields: %w", err) + } + + bodyCell, err := body.ToCell() + if err != nil { + return types.Transaction{}, fmt.Errorf("failed to convert body to cell: %w", err) + } + data := bodyCell.ToBOC() + + return types.Transaction{ + To: to.String(), + Data: data, + AdditionalFields: additionalFields, + OperationMetadata: types.OperationMetadata{ + ContractType: contractType, + Tags: tags, + }, + }, nil +} diff --git a/sdk/ton/transaction_test.go b/sdk/ton/transaction_test.go new file mode 100644 index 00000000..5ec235a1 --- /dev/null +++ b/sdk/ton/transaction_test.go @@ -0,0 +1,107 @@ +package ton_test + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/mcms/sdk/ton" +) + +func TestValidateAdditionalFields(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input json.RawMessage + expectedErr bool + }{ + { + name: "empty json defaults to value 0", + input: json.RawMessage(""), + expectedErr: false, + }, + { + name: "valid json with missing value field", + input: json.RawMessage(`{}`), + expectedErr: false, + }, + { + name: "json with negative value", + input: json.RawMessage(`{"value": "-10"}`), + expectedErr: true, + }, + { + name: "json with null value", + input: json.RawMessage(`{"value": null}`), + expectedErr: true, + }, + { + name: "invalid json", + input: json.RawMessage(`{bad json}`), + expectedErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := ton.ValidateAdditionalFields(tt.input) + if tt.expectedErr { + assert.Error(t, err, "expected an error but got none") + } else { + assert.NoError(t, err, "expected no error but got one") + } + }) + } +} +func TestOperationFieldsValidate(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + value *big.Int + expectedErr bool + }{ + { + name: "valid positive value", + value: big.NewInt(100), + expectedErr: false, + }, + { + name: "valid zero value", + value: big.NewInt(0), + expectedErr: false, + }, + { + name: "nil value", + value: nil, + expectedErr: true, + }, + { + name: "negative value", + value: big.NewInt(-10), + expectedErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + op := ton.AdditionalFields{ + Value: tt.value, + } + + err := op.Validate() + + if tt.expectedErr { + assert.Error(t, err, "expected an error but got none") + } else { + assert.NoError(t, err, "expected no error but got one") + } + }) + } +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 00000000..dedf007f --- /dev/null +++ b/shell.nix @@ -0,0 +1,56 @@ +{ + stdenv, + pkgs, + lib, + # smart contract pkgs to load + pkgsContracts, +}: +pkgs.mkShell { + buildInputs = with pkgs; + [ + # nix tooling + alejandra + + # Go 1.24 + tools + go_1_24 + gopls + delve + golangci-lint + gotools + go-mockery_2 + go-task # taskfile runner + + # Rust + tools + # rustc + # cargo + # solana-cli + + # TS/Node set of tools for changesets + nodejs_24 + (pnpm.override {nodejs = nodejs_24;}) + nodePackages.typescript + nodePackages.typescript-language-server + # Required dependency for @ledgerhq/hw-transport-node-hid -> usb + nodePackages.node-gyp + + # Extra tools + git + jq + yq-go # for manipulating golangci-lint config + go-task + ] + ++ builtins.attrValues pkgsContracts + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + libiconv + + # Required to support go build inside a nix devshell (c compiler dependency on SecTrustCopyCertificateChain/macOS 12+) + # https://github.com/NixOS/nixpkgs/issues/433688#issuecomment-3231551949 + pkgs.apple-sdk_15 + ]; + + PATH_CONTRACTS_TON = "${pkgsContracts.chainlink-ton-contracts}/lib/node_modules/@chainlink/contracts-ton/build/"; + + shellHook = '' + echo "TON contracts located here: $PATH_CONTRACTS_TON" + ''; +} diff --git a/taskfiles/test/Taskfile.yml b/taskfiles/test/Taskfile.yml index 8a0a5049..5289ec2b 100644 --- a/taskfiles/test/Taskfile.yml +++ b/taskfiles/test/Taskfile.yml @@ -17,6 +17,11 @@ tasks: cmds: - go test -v {{.CLI_ARGS}} ./sdk/sui/... + unit:ton: + desc: "Run TON unit tests" + cmds: + - go test -v {{.CLI_ARGS}} ./sdk/ton/... + e2e: desc: "Run e2e tests" env: @@ -45,6 +50,13 @@ tasks: cmds: - CTF_CONFIGS=$CTF_CONFIGS go test -v -tags=e2e -test.run TestSuiSuite {{.CLI_ARGS}} ./e2e/tests... + e2e:ton: + desc: "Run Ton e2e tests" + env: + CTF_CONFIGS: '{{ .CTF_CONFIGS | default "../config.ton.toml" }}' + cmds: + - CTF_CONFIGS=$CTF_CONFIGS go test -v -tags=e2e -test.run TestTONSuite {{.CLI_ARGS}} ./e2e/tests... + coverage: desc: "Run unit test suite with coverage" cmds: diff --git a/types/chain_selector.go b/types/chain_selector.go index f99ab50e..1bf0cdf0 100644 --- a/types/chain_selector.go +++ b/types/chain_selector.go @@ -28,6 +28,7 @@ var supportedFamilies = []string{ cselectors.FamilySolana, cselectors.FamilyAptos, cselectors.FamilySui, + cselectors.FamilyTon, } // GetChainSelectorFamily returns the family of the chain selector. diff --git a/validation.go b/validation.go index e8fe3507..f5355bcd 100644 --- a/validation.go +++ b/validation.go @@ -12,6 +12,7 @@ import ( "github.com/smartcontractkit/mcms/sdk/evm" "github.com/smartcontractkit/mcms/sdk/solana" "github.com/smartcontractkit/mcms/sdk/sui" + "github.com/smartcontractkit/mcms/sdk/ton" ) func validateAdditionalFields(additionalFields json.RawMessage, csel types.ChainSelector) error { @@ -32,6 +33,9 @@ func validateAdditionalFields(additionalFields json.RawMessage, csel types.Chain case cselectors.FamilySui: return sui.ValidateAdditionalFields(additionalFields) + + case cselectors.FamilyTon: + return ton.ValidateAdditionalFields(additionalFields) } return nil @@ -53,6 +57,10 @@ func validateChainMetadata(metadata types.ChainMetadata, csel types.ChainSelecto return nil case cselectors.FamilySui: return sui.ValidateChainMetadata(metadata) + case cselectors.FamilyTon: + // TODO (ton): do we need special chain metadata for TON? + // Yes! We could attach MCMS -> Timelock value here which is now hardcoded default in timelock converter + return nil default: return fmt.Errorf("unsupported chain family: %s", chainFamily) }