Created
April 22, 2025 19:40
-
-
Save thaarok/9155659bf94822116ed2c003810e9020 to your computer and use it in GitHub Desktop.
Sonic Gateway usage sample
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
const { ethers } = require("ethers"); | |
const STATE_ORACLE_ABI = [ | |
"function lastBlockNum() external view returns (uint256)", | |
"function lastState() external view returns (bytes32)" | |
]; | |
const ERC20_ABI = [ | |
"function approve(address spender, uint256 amount) external returns (bool)", | |
"function allowance(address owner, address spender) external view returns (uint256)" | |
]; | |
const TOKEN_PAIRS_ABI = [ | |
"function originalToMinted(address) external view returns (address)", | |
"function mintedToOriginal(address) external view returns (address)" | |
]; | |
const TOKEN_DEPOSIT_ABI = [ | |
"function deposit(uint96 uid, address token, uint256 amount) external", | |
"function claim(uint256 id, address token, uint256 amount, bytes calldata proof) external", | |
"event Deposit(uint256 indexed id, address indexed owner, address token, uint256 amount)" | |
]; | |
const BRIDGE_ABI = [ | |
"function withdraw(uint96 uid, address token, uint256 amount) external", | |
"function claim(uint256 id, address token, uint256 amount, bytes calldata proof) external", | |
"event Withdrawal(uint256 indexed id, address indexed owner, address token, uint256 amount)" | |
]; | |
// Ethereum (L1) | |
const ETH_CONTRACTS = { // testnet deployment | |
TOKEN_DEPOSIT: "0xDF0A2E5361701140111fD70F9628231B63680d27", | |
TOKEN_PAIRS: "0x4aBf40965d008856a1Eac0d2585Dd631259fA424", | |
STATE_ORACLE: "0xAD785C64704c07d4898c5F948ed2CCBA59531E55" | |
}; | |
// Sonic (L2) | |
const SONIC_CONTRACTS = { // testnet deployment | |
BRIDGE: "0xA5495Fa50E94f56024F9C7e442b8ad29B03f459b", | |
TOKEN_PAIRS: "0xA7cD9611Aa6214d1b6f2240e54A7f3B80aD60453", | |
STATE_ORACLE: "0x3F5Ef194f6DBf260AB5e131780C1Cd48cE239E40" | |
}; | |
// Network RPC endpoints | |
const ETHEREUM_RPC = "https://ethereum-sepolia-rpc.publicnode.com"; | |
const SONIC_RPC = "https://rpc.blaze.soniclabs.com/"; | |
// Initialize providers | |
const ethProvider = new ethers.providers.JsonRpcProvider(ETHEREUM_RPC); | |
const sonicProvider = new ethers.providers.JsonRpcProvider(SONIC_RPC); | |
// Initialize signer with your private key | |
const PRIVATE_KEY = "..."; | |
const ethSigner = new ethers.Wallet(PRIVATE_KEY, ethProvider); | |
const sonicSigner = new ethers.Wallet(PRIVATE_KEY, sonicProvider); | |
async function bridgeToSonic(tokenAddress, amount) { | |
// 1. Check if token is supported | |
const tokenPairs = new ethers.Contract(ETH_CONTRACTS.TOKEN_PAIRS, TOKEN_PAIRS_ABI, ethProvider); | |
const mintedToken = await tokenPairs.originalToMinted(tokenAddress); | |
if (mintedToken === ethers.constants.AddressZero) { | |
throw new Error("Token not supported"); | |
} | |
// 2. Approve token spending | |
const token = new ethers.Contract(tokenAddress, ERC20_ABI, ethSigner); | |
const approveTx = await token.approve(ETH_CONTRACTS.TOKEN_DEPOSIT, amount); | |
await approveTx.wait(); | |
// 3. Deposit tokens | |
const deposit = new ethers.Contract(ETH_CONTRACTS.TOKEN_DEPOSIT, TOKEN_DEPOSIT_ABI, ethSigner); | |
const tx = await deposit.deposit(Date.now(), tokenAddress, amount); | |
const receipt = await tx.wait(); | |
return { | |
transactionHash: receipt.transactionHash, | |
mintedToken, | |
blockNumber: receipt.blockNumber, | |
depositId: receipt.events.find(e => e.event === 'Deposit').args.id | |
}; | |
} | |
async function waitForStateUpdate(depositBlockNumber) { | |
const stateOracle = new ethers.Contract(SONIC_CONTRACTS.STATE_ORACLE, STATE_ORACLE_ABI, sonicProvider); | |
while (true) { | |
const currentBlockNum = await stateOracle.lastBlockNum(); | |
if (currentBlockNum >= depositBlockNumber) { | |
return currentBlockNum; | |
} | |
await new Promise(resolve => setTimeout(resolve, 30000)); // Check every 30 seconds | |
} | |
} | |
async function generateProof(depositId, blockNum) { | |
// Generate storage slot for deposit | |
const storageSlot = ethers.utils.keccak256( | |
ethers.utils.defaultAbiCoder.encode(['uint256', 'uint8'], [depositId, 7]) | |
); | |
// Get proof from Ethereum node | |
const proof = await ethProvider.send("eth_getProof", [ | |
ETH_CONTRACTS.TOKEN_DEPOSIT, | |
[storageSlot], | |
ethers.utils.hexValue(blockNum) // important to use current stateOracle.lastBlockNum | |
]); | |
// Encode proof in required format | |
return ethers.utils.RLP.encode([ | |
ethers.utils.RLP.encode(proof.accountProof), | |
ethers.utils.RLP.encode(proof.storageProof[0].proof) | |
]); | |
} | |
async function claimOnSonic(depositBlockNumber, depositId, tokenAddress, amount) { | |
// 1. Wait for state oracle update | |
console.log("Waiting for state oracle update to block ", depositBlockNumber); | |
const blockNum = await waitForStateUpdate(depositBlockNumber); | |
// 2. Generate proof | |
console.log("Generating proof..."); | |
const proof = await generateProof(depositId, blockNum); | |
// 3. Claim tokens with proof | |
console.log("Claiming tokens..."); | |
const bridge = new ethers.Contract(SONIC_CONTRACTS.BRIDGE, BRIDGE_ABI, sonicSigner); | |
const tx = await bridge.claim(depositId, tokenAddress, amount, proof); | |
const receipt = await tx.wait(); | |
return receipt.transactionHash; | |
} | |
async function bridgeToEthereum(tokenAddress, amount) { | |
// 1. Check if token is supported | |
const tokenPairs = new ethers.Contract(SONIC_CONTRACTS.TOKEN_PAIRS, TOKEN_PAIRS_ABI, sonicProvider); | |
const originalToken = await tokenPairs.mintedToOriginal(tokenAddress); | |
if (originalToken === ethers.constants.AddressZero) { | |
throw new Error("Token not supported"); | |
} | |
// 2. Approve token spending | |
const token = new ethers.Contract(tokenAddress, ERC20_ABI, sonicSigner); | |
const approveTx = await token.approve(SONIC_CONTRACTS.BRIDGE, amount); | |
console.log("waiting... ", approveTx.hash); | |
await approveTx.wait(); | |
// 3. Initiate withdrawal | |
const bridge = new ethers.Contract(SONIC_CONTRACTS.BRIDGE, BRIDGE_ABI, sonicSigner); | |
const tx = await bridge.withdraw(Date.now(), originalToken, amount); | |
const receipt = await tx.wait(); | |
return { | |
transactionHash: receipt.transactionHash, | |
originalToken, | |
blockNumber: receipt.blockNumber, | |
withdrawalId: receipt.events.find(e => e.event === 'Withdrawal').args.id | |
}; | |
} | |
async function waitForEthStateUpdate(withdrawalBlockNumber) { | |
const stateOracle = new ethers.Contract(ETH_CONTRACTS.STATE_ORACLE, STATE_ORACLE_ABI, ethProvider); | |
while (true) { | |
const currentBlockNum = await stateOracle.lastBlockNum(); | |
if (currentBlockNum >= withdrawalBlockNumber) { | |
return currentBlockNum; | |
} | |
await new Promise(resolve => setTimeout(resolve, 30000)); // Check every 30 seconds | |
} | |
} | |
async function generateWithdrawalProof(withdrawalId, blockNum) { | |
// Generate storage slot for withdrawal | |
const storageSlot = ethers.utils.keccak256( | |
ethers.utils.defaultAbiCoder.encode(['uint256', 'uint8'], [withdrawalId, 1]) | |
); | |
// Get proof from Sonic node | |
const proof = await sonicProvider.send("eth_getProof", [ | |
SONIC_CONTRACTS.BRIDGE, | |
[storageSlot], | |
ethers.utils.hexValue(blockNum) // important to use current stateOracle.lastBlockNum | |
]); | |
// Encode proof in required format | |
return ethers.utils.RLP.encode([ | |
ethers.utils.RLP.encode(proof.accountProof), | |
ethers.utils.RLP.encode(proof.storageProof[0].proof) | |
]); | |
} | |
async function claimOnEthereum(withdrawalBlockNumber, withdrawalId, tokenAddress, amount) { | |
// 1. Wait for state oracle update | |
console.log("Waiting for state oracle update to block ", withdrawalBlockNumber); | |
const blockNum = await waitForEthStateUpdate(withdrawalBlockNumber); | |
// 2. Generate proof | |
console.log("Generating proof..."); | |
const proof = await generateWithdrawalProof(withdrawalId, blockNum); | |
// 3. Claim tokens with proof | |
console.log("Claim tokens with proof..."); | |
const deposit = new ethers.Contract(ETH_CONTRACTS.TOKEN_DEPOSIT, TOKEN_DEPOSIT_ABI, ethSigner); | |
const tx = await deposit.claim(withdrawalId, tokenAddress, amount, proof); | |
const receipt = await tx.wait(); | |
return receipt.transactionHash; | |
} | |
async function bridgeUSDC() { | |
// USDC details | |
const USDC_ADDRESS = "0xd98b590ebe0a3ed8c144170ba4122d402182976f"; // testnet deployment | |
let amount = 1000; | |
// 1. Bridge USDC to Sonic | |
console.log("Initiating bridge to Sonic..."); | |
const deposit = await bridgeToSonic(USDC_ADDRESS, amount); | |
console.log(`Deposit successful: ${deposit.transactionHash}`); | |
// 2. Claim USDC on Sonic | |
console.log("Waiting for state update and claiming on Sonic..."); | |
const claimTx = await claimOnSonic(deposit.blockNumber, deposit.depositId, USDC_ADDRESS, amount); | |
console.log(`Claim successful: ${claimTx}`); | |
//const deposit = { mintedToken: "0x326599186C997717cC07e7A1305a7fddFC21647A" }; | |
amount = 100; | |
// Later: Bridge back to Ethereum | |
console.log("Initiating bridge back to Ethereum..."); | |
const withdrawal = await bridgeToEthereum(deposit.mintedToken, amount); | |
console.log(`Withdrawal initiated: ${withdrawal.transactionHash}`); | |
// Claim on Ethereum | |
console.log("Waiting for state update and claiming on Ethereum..."); | |
const finalClaim = await claimOnEthereum( | |
withdrawal.blockNumber, | |
withdrawal.withdrawalId, | |
withdrawal.originalToken, | |
amount | |
); | |
console.log(`Final claim successful: ${finalClaim}`); | |
} | |
bridgeUSDC(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment