import hre, { ethers } from "hardhat";
import { Signer } from "ethers";
import { expect } from "chai";
// The following items come from '@tonappchain/evm-ccl' to help test cross-chain logic locally.
import {
deploy,
TacLocalTestSdk,
JettonInfo,
TokenMintInfo,
TokenUnlockInfo,
} from "@tonappchain/evm-ccl";
// Types for your compiled contracts
import { TestProxy, TestToken } from "../typechain-types";
import { InvokeWithCallbackEvent } from "../typechain-types/contracts/TestProxy";
describe("TestProxy with @tonappchain/evm-ccl", () => {
let admin: Signer;
let testSdk: TacLocalTestSdk;
let proxyContract: TestProxy;
let existedToken: TestToken;
before(async () => {
[admin] = await ethers.getSigners();
// 1. Initialize local test SDK
testSdk = new TacLocalTestSdk();
const crossChainLayerAddress = testSdk.create(ethers.provider);
// 2. Deploy a sample ERC20 token
existedToken = await deploy<TestToken>(
admin,
hre.artifacts.readArtifactSync("TestToken"),
["TestToken", "TTK"],
undefined,
false
);
// 3. Deploy the proxy contract
proxyContract = await deploy<TestProxy>(
admin,
hre.artifacts.readArtifactSync("TestProxy"),
[crossChainLayerAddress],
undefined,
false
);
});
it("Should correctly handle invokeWithCallback", async () => {
// Prepare call parameters
const shardsKey = 1n;
const operationId = ethers.encodeBytes32String("operationId");
const extraData = "0x"; // untrusted data from the executor
const timestamp = BigInt(Math.floor(Date.now() / 1000));
const tvmWalletCaller = "TVMCallerAddress";
// Example bridging: create a Jetton and specify how many tokens to mint
const jettonInfo: JettonInfo = {
tvmAddress: "JettonMinterAddress",
name: "TestJetton",
symbol: "TJT",
decimals: 9n,
};
const tokenMintInfo: TokenMintInfo = {
info: jettonInfo,
amount: 10n ** 9n,
};
// Also handle an existing EVM token to simulate bridging
const tokenUnlockInfo: TokenUnlockInfo = {
evmAddress: await existedToken.getAddress(),
amount: 10n ** 18n,
};
// Lock existedToken in the cross-chain layer to emulate bridging from EVM
await existedToken.mint(
testSdk.getCrossChainLayerAddress(),
tokenUnlockInfo.amount
);
// You can define a native TAC amount to bridge to your proxy,
// but you must first lock this amount on the CrossChainLayer contract
// use the testSdk.lockNativeTacOnCrossChainLayer(nativeTacAmount) function
const tacAmountToBridge = 0n;
// Determine the EVM address of the bridged Jetton (for minted jettons)
const bridgedJettonAddress = testSdk.getEVMJettonAddress(
jettonInfo.tvmAddress
);
// Prepare the method call
const target = await proxyContract.getAddress();
const methodName = "invokeWithCallback(bytes,bytes)";
// Our 'arguments' is an array of TokenAmount: (address, uint256)[]
const receivedTokens = [
[bridgedJettonAddress, tokenMintInfo.amount],
[tokenUnlockInfo.evmAddress, tokenUnlockInfo.amount],
];
const encodedArguments = ethers.AbiCoder.defaultAbiCoder().encode(
["tuple(address,uint256)[]"],
[receivedTokens]
);
// 4. Use testSdk to simulate a cross-chain message
const { receipt, deployedTokens, outMessages } = await testSdk.sendMessage(
shardsKey,
target,
methodName,
encodedArguments,
tvmWalletCaller,
[tokenMintInfo], // which jettons to mint
[tokenUnlockInfo], // which EVM tokens to unlock
tacAmountToBridge,
extraData,
operationId,
timestamp,
0, // gasLimit - if 0 - simulate and fill inside sendMessage
false // force send (if simulation failed)
);
// 5. Assertions
expect(receipt.status).to.equal(1);
// - Check if the Jetton was deployed
expect(deployedTokens.length).to.equal(1);
expect(deployedTokens[0].evmAddress).to.equal(bridgedJettonAddress);
// - Check the outMessages array
expect(outMessages.length).to.equal(1);
const outMessage = outMessages[0];
expect(outMessage.shardsKey).to.equal(shardsKey);
expect(outMessage.operationId).to.equal(operationId);
expect(outMessage.callerAddress).to.equal(await proxyContract.getAddress());
expect(outMessage.targetAddress).to.equal(tvmWalletCaller);
// - The returned tokens should be burned or locked as bridging back to TON
expect(outMessage.tokensBurned.length).to.equal(1);
expect(outMessage.tokensBurned[0].evmAddress).to.equal(
bridgedJettonAddress
);
expect(outMessage.tokensBurned[0].amount).to.equal(tokenMintInfo.amount);
expect(outMessage.tokensLocked.length).to.equal(1);
expect(outMessage.tokensLocked[0].evmAddress).to.equal(
tokenUnlockInfo.evmAddress
);
expect(outMessage.tokensLocked[0].amount).to.equal(tokenUnlockInfo.amount);
// - Confirm the event was emitted
let eventFound = false;
receipt.logs.forEach((log) => {
const parsed = proxyContract.interface.parseLog(log);
if (parsed && parsed.name === "InvokeWithCallback") {
eventFound = true;
const typedEvent =
parsed as unknown as InvokeWithCallbackEvent.LogDescription;
expect(typedEvent.args.shardsKey).to.equal(shardsKey);
expect(typedEvent.args.timestamp).to.equal(timestamp);
expect(typedEvent.args.operationId).to.equal(operationId);
expect(typedEvent.args.tvmCaller).to.equal(tvmWalletCaller);
expect(typedEvent.args.extraData).to.equal(extraData);
expect(typedEvent.args.receivedTokens.length).to.equal(2);
expect(typedEvent.args.receivedTokens[0].evmAddress).to.equal(
bridgedJettonAddress
);
expect(typedEvent.args.receivedTokens[1].evmAddress).to.equal(
tokenUnlockInfo.evmAddress
);
}
});
expect(eventFound).to.be.true;
});
});