import * as fs from 'fs'; import { attestFromEth, createWrappedOnEth, getEmitterAddressEth, getEmitterAddressSolana, getForeignAssetEth, parseSequenceFromLogEth, redeemOnEth, setDefaultWasm, transferFromEthNative, tryNativeToUint8Array, } from '@certusone/wormhole-sdk'; import * as ethers from 'ethers'; import fetch from 'node-fetch'; import { promisify } from 'util'; import * as solana from './solana'; const exec = promisify(require('child_process').exec); const config = JSON.parse(fs.readFileSync('./xdapp.config.json').toString()); let ABI; try { ABI = JSON.parse(fs.readFileSync("./chains/evm/out/Xmint.sol/Xmint.json").toString()).abi } catch (e) { // fail silenty // The only time this fails is when deploy hasn't been called, in which case, this isn't needed } /** * 1. Deploy on chain contract "XMint" * @param src The network to deploy */ export async function deploy(src: string){ const rpc = config.networks[src]['rpc']; const core = config.networks[src]['bridgeAddress']; const token = config.networks[src]['tokenBridgeAddress']; const key = fs.readFileSync(`keypairs/${src}.key`).toString(); const { stdout , stderr } = await exec( `cd chains/evm && forge build && forge create --legacy --rpc-url ${rpc} --private-key ${key} src/Xmint.sol:Xmint --constructor-args "${src.toUpperCase()}-TOKEN" "${src.toUpperCase()}T" ${core} ${token} && exit` ) if (stderr) { throw new Error(stderr.message); } let deploymentAddress:string; if (stdout) { console.log(stdout); deploymentAddress = stdout .split("Deployed to: ")[1] .split("\n")[0] .trim(); const emittedVAAs = []; //Resets the emittedVAAs fs.writeFileSync( `./deployinfo/${src}.deploy.json`, JSON.stringify({ address: deploymentAddress, vaas: emittedVAAs }, null, 4) ); } } /** * Registers the cross chain mint contracts with one another across chains * @param src The network you want to register the foreign network on. * @param target The foreign network */ export async function registerApp(src:string, target:string){ const key = fs.readFileSync(`keypairs/${src}.key`).toString(); const srcNetwork = config.networks[src]; const targetNetwork = config.networks[target]; let srcDeploymentInfo; let targetDeploymentInfo; let targetEmitter; try{ srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString()); } catch (e){ throw new Error(`${src} is not deployed yet`); } try{ targetDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString()); } catch (e){ throw new Error(`${target} is not deployed yet`); } switch (targetNetwork['type']){ case 'evm': targetEmitter = getEmitterAddressEth(targetDeploymentInfo['address']); break; case 'solana': setDefaultWasm("node"); // *sigh* targetEmitter = await getEmitterAddressSolana(targetDeploymentInfo['address']); break; } const emitterBuffer = Buffer.from(targetEmitter, 'hex'); const signer = new ethers.Wallet(key).connect( new ethers.providers.JsonRpcProvider(srcNetwork.rpc) ); const messenger = new ethers.Contract( srcDeploymentInfo.address, ABI, signer ); const tx = await messenger.registerApplicationContracts( targetNetwork.wormholeChainId, emitterBuffer ); console.log(`Registered ${target} application on ${src}`); // Alongside registering the App, go ahead register the tokens with one another // Register target token with src chain console.log(`Registering ${target} token on ${src}`); switch(targetNetwork.type){ case 'evm': await attest(target, src); break; case 'solana': await solana.attest(target, src); break; } console.log(`Attested ${target} token on ${src}`); } /** * Attest token from src and create wrapped on target * @param src * @param target * @param address */ export async function attest(src: string, target: string, address:string = null){ //Check TARGET type == EVM, else throw error const srcNetwork = config.networks[src]; const targetNetwork = config.networks[target]; const srcDeployInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString()); const targetDeployInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString()); const srcKey = fs.readFileSync(`keypairs/${src}.key`).toString(); const srcSigner = new ethers.Wallet(srcKey).connect( new ethers.providers.JsonRpcProvider(srcNetwork.rpc) ); console.log(`Attesting ${src} Network Token on ${target} Network`) if(!address){ address = srcDeployInfo.address; } console.log(`Attesting ${address} from ${target} network onto ${src}`); const tx = await attestFromEth( srcNetwork.tokenBridgeAddress, srcSigner, address, { gasLimit: 1500000 } ); // in this context the target is network we're attesting *from* so it's the network the vaa comes from (hence being placed as the 'source') // The emitter for this is PORTAL, not our contract, so we set portal=true in fetchVaa const attestVaa = await fetchVaa(src, tx, true); switch(targetNetwork.type){ case "evm": await createWrapped(target, src, attestVaa) break; case "solana": await solana.createWrapped(target, src, attestVaa) break; } } export async function createWrapped(src:string, target: string, vaa:string){ const srcNetwork = config.networks[src]; const targetNetwork = config.networks[target]; const targetDeployInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString()); const key = fs.readFileSync(`keypairs/${src}.key`).toString(); const signer = new ethers.Wallet(key).connect( new ethers.providers.JsonRpcProvider(srcNetwork.rpc) ); const tx = await createWrappedOnEth( srcNetwork.tokenBridgeAddress, signer, Buffer.from(vaa, 'base64'), { gasLimit: 1000000 } ); await new Promise((r) => setTimeout(r, 5000)); // wait for blocks to advance before fetching new foreign address const foreignAddress = await getForeignAssetEth( srcNetwork.tokenBridgeAddress, signer, targetNetwork.wormholeChainId, tryNativeToUint8Array(targetDeployInfo.address, targetNetwork.wormholeChainId) ); console.log(`${src} Network has new PortalWrappedToken for ${target} network at ${foreignAddress}`); } async function fetchVaa(src:string, tx:ethers.ethers.ContractReceipt, portal:boolean = false){ const srcNetwork = config.networks[src]; const srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString()); const seq = parseSequenceFromLogEth(tx, srcNetwork['bridgeAddress']); let emitterAddr = ""; if(portal){ emitterAddr = getEmitterAddressEth(srcNetwork['tokenBridgeAddress']); } else { emitterAddr = getEmitterAddressEth(srcDeploymentInfo['address']); } await new Promise((r) => setTimeout(r, 5000)); //wait for Guardian to pick up message console.log( "Searching for: ", `${config.wormhole.restAddress}/v1/signed_vaa/${srcNetwork.wormholeChainId}/${emitterAddr}/${seq}` ); const vaaBytes = await ( await fetch( `${config.wormhole.restAddress}/v1/signed_vaa/${srcNetwork.wormholeChainId}/${emitterAddr}/${seq}` ) ).json(); if(!vaaBytes['vaaBytes']){ throw new Error("VAA not found!"); } console.log("VAA Found: ", vaaBytes.vaaBytes); return vaaBytes.vaaBytes; } function writeVaa(src:string, vaa:string){ const srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString()); if(!srcDeploymentInfo['vaas']){ srcDeploymentInfo['vaas'] = [vaa] } else { srcDeploymentInfo['vaas'].push(vaa) } fs.writeFileSync( `./deployinfo/${src}.deploy.json`, JSON.stringify(srcDeploymentInfo, null, 4) ); } /** * Submits target Purchase VAA onto src network * @param src The EVM type of network that the VAA is being submitted to * @param target The target network which initiated the purchase * @param vaa The b64 encoded VAA */ export async function submitForeignPurchase(src:string, target:string, vaa:string) : Promise { const srcNetwork = config.networks[src]; const key = fs.readFileSync(`keypairs/${src}.key`).toString(); let srcDeploymentInfo; let targetDeploymentInfo; try{ srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString()); } catch (e){ throw new Error(`${src} is not deployed yet`); } try{ targetDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString()); } catch (e){ throw new Error(`${target} is not deployed yet`); } const signer = new ethers.Wallet(key).connect( new ethers.providers.JsonRpcProvider(srcNetwork.rpc) ); const messenger = new ethers.Contract( srcDeploymentInfo.address, ABI, signer ); //This will mint tokens and create a VAA to transfer them back over to the src chain const tx = await messenger.submitForeignPurchase(Buffer.from(vaa, "base64"), {gasLimit: 1000000}); console.log("Submit foreign purchase succeeded"); //Even though we call our contract, portal=true here because our contract calls Portal's transfer() function, making the emitter Portal const claimTokensVaa = await fetchVaa(src, await tx.wait(), true); return claimTokensVaa; } /** * Claims the tokens generated on a foreign network onto the key in the source network * @param src The chain you want to claim the vaa on * @param vaa The vaa you want to claim */ export async function claimTokens(src:string, vaa:string){ const srcNetwork = config.networks[src]; const key = fs.readFileSync(`keypairs/${src}.key`).toString(); let srcDeploymentInfo; try{ srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString()); } catch (e){ throw new Error(`${src} is not deployed yet`); } const signer = new ethers.Wallet(key).connect( new ethers.providers.JsonRpcProvider(srcNetwork.rpc) ); console.log(`Redeeming on ${src} network`); await redeemOnEth( srcNetwork.tokenBridgeAddress, signer, Buffer.from(vaa, "base64"), { gasLimit: 1000000 } ) } export async function submitForeignSale(src:string, target:string, vaa:string){ } /** * Creates a P3 VAA that can only be redeemed by target contract with src key as recipient address * @param src * @param target * @param amount * @returns */ export async function buyToken(src:string, target: string, amount: number): Promise { //Buy Token on Target Chain with SRC Native Currency // Create P3 VAA that pays X native and has the Receipient Address set to XMINT on Target Chain & payload is src key const srcNetwork = config.networks[src]; const targetNetwork = config.networks[target]; const key = fs.readFileSync(`keypairs/${src}.key`).toString(); let srcDeploymentInfo; let targetDeploymentInfo; try{ srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString()); } catch (e){ throw new Error(`${src} is not deployed yet`); } try{ targetDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString()); } catch (e){ throw new Error(`${target} is not deployed yet`); } const signer = new ethers.Wallet(key).connect( new ethers.providers.JsonRpcProvider(srcNetwork.rpc) ); //For this project, 1 Native Token will always equal 100 Chain Tokens, no matter the source or target chains const ethToTransferAmt = ethers.utils.parseUnits((amount/100).toString(), "18"); //how much native you want to transfer to buy AMT worth of Tokens on target chain const targetChainAddress = tryNativeToUint8Array(targetDeploymentInfo.address, targetNetwork.wormholeChainId); //The payload is just the purchaser's public key // This is used to send a Payload 1 Transfer of Tokens back // If it errors, will send a Refund VAA back const purchaseOrderPayload = tryNativeToUint8Array(await signer.getAddress(), srcNetwork.wormholeChainId); //Gotta approve the WETH transfer before we actually transfer using Token Bridge const WETH = new ethers.Contract( srcNetwork.wrappedNativeAddress, JSON.parse(fs.readFileSync("./chains/evm/out/PortalWrappedToken.sol/PortalWrappedToken.json").toString()).abi, signer ); await WETH.approve(srcNetwork.tokenBridgeAddress, ethToTransferAmt); console.log("WETH Approved"); // Now call token bridge to do the transfer const tx = await transferFromEthNative( srcNetwork.tokenBridgeAddress, signer, ethToTransferAmt, targetNetwork.wormholeChainId, targetChainAddress, ethers.BigNumber.from(0), { gasPrice: 2000000000 }, purchaseOrderPayload ); console.log("ETH Native Transferred"); // The buy order will be written to the SRC chain's vaa list // Needs to be submitted to target chain with `submitForeignPurchase` const vaa = await fetchVaa(src, tx, true); writeVaa(src, vaa); return vaa; } /** * Fetches the balance of the SRC KEY on the SRC NETWORK for either SRC NATIVE CURRENCY or Target Tokens * @param src * @param target * @returns */ export async function balance(src:string, target: string) : Promise { const srcNetwork = config.networks[src]; const targetNetwork = config.networks[target]; const key = fs.readFileSync(`keypairs/${src}.key`).toString(); let srcDeploymentInfo; let targetDeploymentInfo; try{ srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString()); } catch (e){ throw new Error(`${src} is not deployed yet`); } try{ targetDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString()); } catch (e){ throw new Error(`${target} is not deployed yet`); } const signer = new ethers.Wallet(key).connect( new ethers.providers.JsonRpcProvider(srcNetwork.rpc) ); if(src == target){ //Get native currency balance return (await signer.getBalance()).toString() } // Else get the Token Balance of the Foreign Network's token on Src Network const foreignAddress = await getForeignAssetEth( srcNetwork.tokenBridgeAddress, signer, targetNetwork.wormholeChainId, tryNativeToUint8Array(targetDeploymentInfo.address, targetNetwork.wormholeChainId) ); const TKN = new ethers.Contract( foreignAddress, JSON.parse(fs.readFileSync("./chains/evm/out/PortalWrappedToken.sol/PortalWrappedToken.json").toString()).abi, signer ); return ((await TKN.balanceOf(await signer.getAddress()))).div(1e8).toString() }