Testing the frontend
The frontend has most of what we need to run it, but is just missing a few details. Let's add them!
Constants
Move into the frontend folder and create a new file in the src/config folder called constants.ts:
cd frontend
touch src/config/constants.ts
Copy and paste the code below:
import { InteropClient } from 'zksync-ethers';
export const GATEWAY_RPC = 'http://localhost:3150/';
export const GW_CHAIN_ID = BigInt('506');
export const TOKEN_CONTRACT_ADDRESS = '0x...';
export const STAKING_CHAIN_1_CONTRACT_ADDRESS = '0x...';
export const STAKING_CHAIN_2_CONTRACT_ADDRESS = '0x...';
export const interopClient = new InteropClient({
gateway: {
env: 'local',
gwRpcUrl: GATEWAY_RPC,
gwChainId: GW_CHAIN_ID,
},
});
Update the contract addresses with your deployed contract addresses.
Wagmi config
In the src/config/wagmi.ts file, double check that the RPC endpoints and chain IDs for match your local chains.
If you forgot these, you can find them in your interop_ecosystem folder.
The chain ID can be found in the chains/<CHAIN_NAME>/ZkStack.yaml file,
and the endpint in the chains/<CHAIN_NAME>/configs/general.yaml file under api.web3_json_rpc.http_url.
Interop utils
Create a new folder called utils in the src folder.
mkdir src/utils
Then create a file called prove.ts in that folder:
touch src/utils/prove.ts
This is where we will implement the logic for fetching the input arguments for the mint function,
and making sure an interop message is ready to be verified.
Once the user deposits some ETH to a staking contract, they will be able to submit their transaction hash to a "Mint" form on the rewards chain, and we can use the transaction hash to fetch the required input arguments for onchain message verification.
Before the message can be verified, though, the transaction must be finalized, and the interop roots on Gateway and the rewards chain must be updated. We will add some functions below to check these statuses, and fetch the input arguments for minting.
Copy and paste the functions below:
import { Provider, utils, Contract, Wallet, getGwBlockForBatch } from 'zksync-ethers';
import { ethers } from 'ethers';
import { rewardsChain } from '../config/wagmi';
import { GATEWAY_RPC, GW_CHAIN_ID, interopClient } from '../config/constants';
const rewardsProvider = new Provider(rewardsChain.rpcUrls.default.http[0]);
export async function checkIfTxIsFinalized(txHash: string, provider: Provider, timeoutMs = 220_000) {
let status:
| 'QUEUED'
| 'SENDING'
| 'PROVING'
| 'EXECUTED'
| 'FAILED'
| 'REJECTED'
| 'UNKNOWN'
| {
phase: string;
message: 'No details available';
} = 'QUEUED';
const deadline = Date.now() + timeoutMs;
let firstCheck = false;
while (status !== 'EXECUTED' && Date.now() < deadline) {
status = await interopClient.getMessageStatus(provider, txHash as `0x${string}`);
if (!firstCheck) {
firstCheck = true;
} else {
await utils.sleep(20000);
}
}
return status;
}
// for local testing only
// forces interop root to update on local leaderboard chain by sending txns
// for testnet or mainnet, use `waitForGatewayInteropRoot` method from `zksync-ethers`
export async function waitForChainInteropRoot(
txHash: string,
srcProvider: Provider,
timeoutMs = 120_000
): Promise<string> {
const PRIVATE_KEY = '0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110';
const wallet = new Wallet(PRIVATE_KEY, rewardsProvider);
const receipt = await (await srcProvider.getTransaction(txHash)).waitFinalize();
const gw = new ethers.JsonRpcProvider(GATEWAY_RPC);
const gwBlock = await getGwBlockForBatch(BigInt(receipt.l1BatchNumber!), srcProvider, gw);
// fetch the interop root from target chain
const InteropRootStorage = new Contract(
utils.L2_INTEROP_ROOT_STORAGE_ADDRESS,
utils.L2_INTEROP_ROOT_STORAGE_ABI,
wallet
);
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const root: string = await InteropRootStorage.interopRoots(GW_CHAIN_ID, gwBlock);
if (root && root !== '0x' + '0'.repeat(64)) return root;
// send tx just to get chain2 to seal batch
const t = await wallet.sendTransaction({
to: wallet.address,
value: BigInt(1),
});
await (await wallet.provider.getTransaction(t.hash)).waitFinalize();
}
throw new Error(`Chain2 did not import interop root for (${GW_CHAIN_ID}, ${gwBlock}) in time`);
}
export async function getProveScoreArgs(txHash: string, srcProvider: Provider) {
const args = await interopClient.getVerificationArgs({
txHash: txHash as `0x${string}`,
srcProvider: srcProvider, // source chain provider (to fetch proof + batch details)
targetChain: rewardsProvider, // target chain provider (to read interop root + verify)
});
return args;
}
There are three functions we've just added:
checkIfTxIsFinalized: This function checks to see if the deposit transaction has finalized. This will take just a few minutes.waitForChainInteropRoot: This function does a couple things. First, it uses the local pre-configured wallet we used for deploying contracts to send transactions on the rewards chain in order to seal the next batch of blocks. This is needed only on a local ecosystem, as there would otherwise not be any other transactions to create new blocks. Second, it fetches the interop root on gateway to check if it has been updated based on the deposit transaction yet.getProveScoreArgs: This function takes the transaction hash input by the user in the "Mint" form and fetches the needed arguments to verify the message sent in thedepositfunction.
Handling Verification
The last change we need to make is updating the handleSubmit function
in the MintForm component.
The full flow for minting the token looks like this:
- The user inputs the transaction hash from their deposit transaction, and selects which staking chain they used.
- The appropriate staking contract address is fetched from our constants file.
- The
handleSubmitfunction waits until the transaction is finalized. - The
handleSubmitfunction waits until the interop root on gateway is updated. - The
handleSubmitfunction fetches the input arguments for themintfunction. - The user is prompted to approve calling the
mintfunction with their wallet. - The transaction gets approved, and the leaderboard table gets updated with the latest number of mints per chain.
In components/MintForm.tsx, update the handleSubmit function with the completed function below:
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!txHash || !chainId) {
alert('missing tx hash');
return;
}
const chain = getChainInfo(chainId);
const stakingContractAddress = getContractAddress(chainId);
if (!chain || !stakingContractAddress) {
alert('Staking chain not supported');
return;
}
setIsSubmitPending(true);
const provider = new Provider(chain.rpcUrls.default.http[0]);
const status = await checkIfTxIsFinalized(txHash, provider);
if (status !== 'EXECUTED') {
alert('Deposit txn is not yet finalized.');
setIsSubmitPending(false);
return;
}
setIsFinalized(true);
await waitForChainInteropRoot(txHash, provider);
setIsRootUpdated(true);
const args = await getProveScoreArgs(txHash, provider);
writeContract({
address: TOKEN_CONTRACT_ADDRESS,
abi: TOKEN_JSON.abi as Abi,
functionName: 'mint',
args: [args.srcChainId, args.l1BatchNumber, args.l2MessageIndex, args.msgData, args.gatewayProof],
});
};
Run the app
Use the command below to run the frontend.
npm run dev
You can now open the frontend at http://localhost:5173/.
On the frontend, you should be able to add each network to your wallet by clicking on them from the dropdown menu.
You can send funds to your wallet using the zkstack dev rich-account command:
zkstack dev rich-account --chain zk_chain_1 0x<YOUR_WALLET_ADDRESS>
zkstack dev rich-account --chain zk_chain_2 0x<YOUR_WALLET_ADDRESS>
zkstack dev rich-account --chain zk_chain_3 0x<YOUR_WALLET_ADDRESS>
Now you can test the staking and tokens contracts with the frontend!
On one of the staking chains, deposit any amount of ETH and then copy your transaction hash. On the rewards chain, input the transaction hash and select the staking chain you used.
Then click the mint button to mint a reward token. This process will take a few minutes on a local ecosystem. Once minted, you should see the leaderboard table update.
Share on X