diff --git a/web/packages/api/package.json b/web/packages/api/package.json index 9709aac504..764db5d5cf 100644 --- a/web/packages/api/package.json +++ b/web/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@snowbridge/api", - "version": "0.1.28", + "version": "0.1.29", "description": "Snowbridge API client", "license": "Apache-2.0", "repository": { diff --git a/web/packages/api/src/toEthereum.ts b/web/packages/api/src/toEthereum.ts index a4ff051030..7a5db7acda 100644 --- a/web/packages/api/src/toEthereum.ts +++ b/web/packages/api/src/toEthereum.ts @@ -1,5 +1,7 @@ +import { ApiPromise } from "@polkadot/api" +import { SubmittableExtrinsic, SubmittableExtrinsicFunction } from "@polkadot/api/types" import { EventRecord } from "@polkadot/types/interfaces" -import { Codec, IKeyringPair, Signer } from "@polkadot/types/types" +import { AnyTuple, Codec, IKeyringPair, ISubmittableResult, Signer } from "@polkadot/types/types" import { BN, u8aToHex } from "@polkadot/util" import { decodeAddress, xxhashAsHex } from "@polkadot/util-crypto" import { assetStatusInfo, palletAssetsBalance } from "./assets" @@ -100,6 +102,91 @@ export const getSendFee = async ( return leFee.eqn(0) ? options.defaultFee : BigInt(leFee.toString()) } +export type SendTokenTx = { + input: { + ethereumChainId: bigint; + sourceAddress: string; + beneficiaryAddress: any; + tokenAddress: string; + amount: bigint; + }, + computed: { + assetLocation: any; + sourceAddressHex: `0x${string}`; + destination: any; + beneficiary: any; + assets: any; + fee_asset: number; + weight: string; + extrinsic: SubmittableExtrinsicFunction<"promise", AnyTuple>; + }, + tx: SubmittableExtrinsic<"promise", ISubmittableResult> +} + +export async function createTx( + sourceParachain: ApiPromise, + ethereumChainId: bigint, + sourceAddress: string, + beneficiaryAddress: string, + tokenAddress: string, + amount: bigint, +): Promise { + const assetLocation = { + parents: 2, + interior: { + X2: [ + { GlobalConsensus: { Ethereum: { chain_id: ethereumChainId } } }, + { AccountKey20: { key: tokenAddress } }, + ], + }, + } + const sourceAddressHex = u8aToHex(decodeAddress(sourceAddress)) + const fee_asset = 0 + const weight = "Unlimited" + const versionKey = "V3" + const assets: { [key: string]: any } = {} + const transferAsset = { + id: { Concrete: assetLocation }, + fun: { Fungible: amount }, + } + assets[versionKey] = [transferAsset] + const destination: { [key: string]: any } = {} + destination[versionKey] = { + parents: 2, + interior: { + X1: { GlobalConsensus: { Ethereum: { chain_id: ethereumChainId } } }, + }, + } + const beneficiaryLocation: { [key: string]: any } = {} + beneficiaryLocation[versionKey] = { + parents: 0, + interior: { X1: { AccountKey20: { key: beneficiaryAddress } } }, + } + const extrinsic = sourceParachain.tx.polkadotXcm.transferAssets + const tx = extrinsic(destination, beneficiaryLocation, assets, fee_asset, weight) + + return { + input: { + ethereumChainId, + sourceAddress, + beneficiaryAddress, + amount, + tokenAddress, + }, + computed: { + assetLocation, + sourceAddressHex, + destination, + beneficiary: beneficiaryLocation, + assets, + fee_asset, + weight, + extrinsic + }, + tx + } +} + export const validateSend = async ( context: Context, signer: WalletOrKeypair, @@ -215,7 +302,7 @@ export const validateSend = async ( }) const bridgeStatus = await bridgeStatusInfo(context); - + const bridgeOperational = bridgeStatus.toEthereum.operatingMode.outbound === "Normal" const lightClientLatencyIsAcceptable = bridgeStatus.toEthereum.latencySeconds < options.acceptableLatencyInSeconds @@ -418,7 +505,7 @@ export const send = async ( const parachainSignedTx = await parachainApi.tx.polkadotXcm .transferAssets(pDestination, pBeneficiary, pAssets, fee_asset, weight) - .signAsync(addressOrPair, { signer: walletSigner }) + .signAsync(addressOrPair, { signer: walletSigner, withSignedTransaction: true }) pResult = await new Promise<{ blockNumber: number @@ -523,9 +610,17 @@ export const send = async ( interior: { X1: { AccountKey20: { key: plan.success.beneficiary } } }, } - const assetHubSignedTx = await assetHub.tx.polkadotXcm - .transferAssets(destination, beneficiary, assets, fee_asset, weight) - .signAsync(addressOrPair, { signer: walletSigner }) + const assetHubUnsigned = await createTx( + assetHub, + plan.success.ethereumChainId, + plan.success.sourceAddress, + plan.success.beneficiary, + plan.success.tokenAddress, + plan.success.amount + ); + + const assetHubSignedTx = await assetHubUnsigned.tx + .signAsync(addressOrPair, { signer: walletSigner, withSignedTransaction: true }) let result = await new Promise<{ blockNumber: number @@ -762,7 +857,7 @@ export const trackSendProgressPolling = async ( messageID.toLowerCase() === success.messageId?.toLowerCase() && nonce === success.bridgeHub.nonce && channelID.toLowerCase() == - paraIdToChannelId(success.plan.success?.assetHub.paraId ?? 1000).toLowerCase() + paraIdToChannelId(success.plan.success?.assetHub.paraId ?? 1000).toLowerCase() ) { success.ethereum.transferBlockNumber = blockNumber success.ethereum.transferBlockHash = blockHash @@ -873,9 +968,9 @@ export async function* trackSendProgress( messageId.toLowerCase() === success.messageId?.toLowerCase() && nonce === success.bridgeHub.nonce && channelId.toLowerCase() == - paraIdToChannelId( - success.plan.success?.assetHub.paraId ?? 1000 - ).toLowerCase() + paraIdToChannelId( + success.plan.success?.assetHub.paraId ?? 1000 + ).toLowerCase() ) { resolve(dispatchSuccess) gateway.removeListener(InboundMessageDispatched, listener) diff --git a/web/packages/api/src/toPolkadot.ts b/web/packages/api/src/toPolkadot.ts index 5c628b03ae..3da71c8196 100644 --- a/web/packages/api/src/toPolkadot.ts +++ b/web/packages/api/src/toPolkadot.ts @@ -3,7 +3,7 @@ import { Codec } from "@polkadot/types/types" import { u8aToHex } from "@polkadot/util" import { IERC20__factory, IGateway__factory, WETH9__factory } from "@snowbridge/contract-types" import { MultiAddressStruct } from "@snowbridge/contract-types/src/IGateway" -import { LogDescription, Signer, TransactionReceipt, ethers } from "ethers" +import { Contract, ContractTransaction, LogDescription, Signer, TransactionReceipt, ethers } from "ethers" import { concatMap, filter, firstValueFrom, lastValueFrom, take, takeWhile, tap } from "rxjs" import { assetStatusInfo } from "./assets" import { Context } from "./index" @@ -128,6 +128,72 @@ export const getSubstrateAccount = async (parachain: ApiPromise, beneficiaryHex: return { balance: account.data.free, consumers: account.consumers } } + +export type SendTokenTx = { + input: { + gatewayAddress: string, + sourceAddress: string; + beneficiaryAddress: string; + tokenAddress: string; + destinationParaId: number; + amount: bigint; + totalFeeInWei: bigint; + destinationFeeInDOT: bigint; + }, + computed: { + beneficiaryAddressHex: string; + beneficiaryMultiAddress: MultiAddressStruct; + totalValue: bigint; + }, + tx: ContractTransaction +} + +export async function createTx( + gatewayAddress: string, + sourceAddress: string, + beneficiaryAddress: string, + tokenAddress: string, + destinationParaId: number, + amount: bigint, + totalFeeInWei: bigint, + destinationFeeInDOT: bigint, +): Promise { + let { address: beneficiary, hexAddress: beneficiaryAddressHex } = beneficiaryMultiAddress(beneficiaryAddress) + const value = totalFeeInWei + const ifce = IGateway__factory.createInterface() + const con = new Contract(gatewayAddress, ifce); + const tx = await con.getFunction("sendToken").populateTransaction( + tokenAddress, + destinationParaId, + beneficiary, + destinationFeeInDOT, + amount, + { + value, + from: sourceAddress + } + ) + + return { + input: { + gatewayAddress, + sourceAddress, + beneficiaryAddress, + tokenAddress, + destinationParaId, + amount, + totalFeeInWei, + destinationFeeInDOT, + }, computed: { + beneficiaryAddressHex, + beneficiaryMultiAddress: beneficiary, + totalValue: value, + }, + tx, + } +} + + export const validateSend = async ( context: Context, source: ethers.Addressable, @@ -416,12 +482,16 @@ export const send = async ( polkadot: { api: { assetHub, bridgeHub }, }, + ethereum: { + contracts: { gateway } + } } = context - const { success } = plan - if (plan.failure || !success) { + if (plan.failure || !plan.success) { throw new Error("Plan failed") } + + const { success } = plan if (success.sourceAddress !== (await signer.getAddress())) { throw new Error("Invalid signer") } @@ -432,44 +502,19 @@ export const send = async ( bridgeHub.rpc.chain.getFinalizedHead(), ]) - const contract = IGateway__factory.connect(context.config.appContracts.gateway, signer) - - const response = await contract.sendToken( - success.token, - success.destinationParaId, - success.beneficiaryMultiAddress, - success.destinationFee, - success.amount, - { - value: success.fee, - } + let { tx } = await createTx( + await gateway.getAddress(), + plan.success.sourceAddress, + plan.success.beneficiaryAddress, + plan.success.token, + plan.success.destinationParaId, + plan.success.amount, + plan.success.fee, + plan.success.destinationFee ) + const response = await signer.sendTransaction(tx) let receipt = await response.wait(confirmations) - /// Was a nice idea to sign and send in two steps but metamask does not support this. - /// https://github.com/MetaMask/metamask-extension/issues/2506 - - //const response = await contract.sendToken( - // success.token, - // success.destinationParaId, - // success.beneficiaryMultiAddress, - // success.destinationFee, - // success.amount, - // { - // value: success.fee - // } - //) - //let receipt = await response.wait(confirmations) - //const signedTx = await signer.signTransaction(tx) - //const txHash = keccak256(signedTx) - //const response = await context.ethereum.api.provider.broadcastTransaction(signedTx) - // TODO: await context.ethereum.api.getTransaction(txHash) // Use this to check if the server knows about transaction. - // TODO: await context.ethereum.api.getTransactionReceipt(txHash) // Use this to check if the server has mined the transaction. - // TODO: remove this wait and move everything below this line to trackProgress/Polling methods. - //if(txHash !== receipt.hash) { - // throw new Error('tx Hash mismtach') - //} - if (receipt === null) { throw new Error("Error waiting for transaction completion") } @@ -483,7 +528,7 @@ export const send = async ( } const events: LogDescription[] = [] receipt.logs.forEach((log) => { - let event = contract.interface.parseLog({ + let event = gateway.interface.parseLog({ topics: [...log.topics], data: log.data, }) @@ -608,8 +653,7 @@ export const trackSendProgressPolling = async ( } console.log( - `Bridge Hub block ${blockHash.toHex()}: Beacon client ${ - ethereumBlockNumber - Number(ethBlockNumber) + `Bridge Hub block ${blockHash.toHex()}: Beacon client ${ethereumBlockNumber - Number(ethBlockNumber) } blocks behind.` ) } @@ -696,12 +740,12 @@ export const trackSendProgressPolling = async ( assetHub.events.foreignAssets.Issued.is(event.event) && eventData[2].toString() === success?.plan.success?.amount.toString() && u8aToHex(decodeAddress(eventData[1])).toLowerCase() === - issuedTo.toLowerCase() && + issuedTo.toLowerCase() && eventData[0]?.parents === 2 && eventData[0]?.interior?.x2[0]?.globalConsensus?.ethereum?.chainId.toString() === - success?.plan.success?.ethereumChainId.toString() && + success?.plan.success?.ethereumChainId.toString() && eventData[0]?.interior?.x2[1]?.accountKey20?.key.toLowerCase() === - success?.plan.success?.token.toLowerCase() + success?.plan.success?.token.toLowerCase() ) { transferBlockHash = blockHash.toHex() } @@ -822,8 +866,7 @@ export async function* trackSendProgress( takeWhile(({ blockNumber }) => ethereumBlockNumber > Number(blockNumber)), tap(({ createdAtHash, blockNumber }) => console.log( - `Bridge Hub block ${createdAtHash}: Beacon client ${ - ethereumBlockNumber - Number(blockNumber) + `Bridge Hub block ${createdAtHash}: Beacon client ${ethereumBlockNumber - Number(blockNumber) } blocks behind.` ) ) @@ -861,7 +904,7 @@ export async function* trackSendProgress( bridgeHub.events.ethereumInboundQueue.MessageReceived.is(event.event) && eventData.nonce === success?.nonce.toString() && eventData.messageId.toLowerCase() === - success?.messageId.toLowerCase() && + success?.messageId.toLowerCase() && eventData.channelId.toLowerCase() === success?.channelId.toLowerCase() ) { messageReceivedFound = true @@ -912,12 +955,12 @@ export async function* trackSendProgress( assetHub.events.foreignAssets.Issued.is(event.event) && eventData[2].toString() === success?.plan.success?.amount.toString() && u8aToHex(decodeAddress(eventData[1])).toLowerCase() === - issuedTo.toLowerCase() && + issuedTo.toLowerCase() && eventData[0]?.parents === 2 && eventData[0]?.interior?.x2[0]?.globalConsensus?.ethereum?.chainId.toString() === - success?.plan.success?.ethereumChainId.toString() && + success?.plan.success?.ethereumChainId.toString() && eventData[0]?.interior?.x2[1]?.accountKey20?.key.toLowerCase() === - success?.plan.success?.token.toLowerCase() + success?.plan.success?.token.toLowerCase() ) }, { @@ -955,12 +998,12 @@ export async function* trackSendProgress( destParaApi.events.foreignAssets.Issued.is(event.event) && eventData[2].toString() === success?.plan.success?.amount.toString() && u8aToHex(decodeAddress(eventData[1])).toLowerCase() === - issuedTo.toLowerCase() && + issuedTo.toLowerCase() && eventData[0]?.parents === 2 && eventData[0]?.interior?.x2[0]?.globalConsensus?.ethereum?.chainId.toString() === - success?.plan.success?.ethereumChainId.toString() && + success?.plan.success?.ethereumChainId.toString() && eventData[0]?.interior?.x2[1]?.accountKey20?.key.toLowerCase() === - success?.plan.success?.token.toLowerCase() + success?.plan.success?.token.toLowerCase() ) }, { @@ -975,9 +1018,8 @@ export async function* trackSendProgress( ) } } - yield `Message delivered to parachain ${ - success.plan.success.destinationParaId - } at block ${success.destinationParachain?.events?.createdAtHash?.toHex()}.` + yield `Message delivered to parachain ${success.plan.success.destinationParaId + } at block ${success.destinationParachain?.events?.createdAtHash?.toHex()}.` } yield "Transfer complete." diff --git a/web/packages/contract-types/package.json b/web/packages/contract-types/package.json index 78ed2e7f8d..5b1e8c242e 100644 --- a/web/packages/contract-types/package.json +++ b/web/packages/contract-types/package.json @@ -1,6 +1,6 @@ { "name": "@snowbridge/contract-types", - "version": "0.1.28", + "version": "0.1.29", "description": "Snowbridge contract type bindings", "license": "Apache-2.0", "repository": { diff --git a/web/packages/contracts/package.json b/web/packages/contracts/package.json index 4b3e846c51..6a9154f441 100644 --- a/web/packages/contracts/package.json +++ b/web/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@snowbridge/contracts", - "version": "0.1.28", + "version": "0.1.29", "description": "Snowbridge contract source and abi.", "license": "Apache-2.0", "repository": { diff --git a/web/packages/operations/src/transfer_token.ts b/web/packages/operations/src/transfer_token.ts index 39abcd878b..e35463a93b 100644 --- a/web/packages/operations/src/transfer_token.ts +++ b/web/packages/operations/src/transfer_token.ts @@ -1,4 +1,5 @@ import { Keyring } from "@polkadot/keyring" +import { Signer } from "@polkadot/types/types" import { contextFactory, destroyContext, @@ -18,6 +19,7 @@ const monitor = async () => { if (snwobridgeEnv === undefined) { throw Error(`Unknown environment '${env}'`) } + console.log(`Using environment '${env}'`) const { config } = snwobridgeEnv @@ -62,7 +64,7 @@ const monitor = async () => { const depositResult = await weth9.deposit({ value: amount }) const depositReceipt = await depositResult.wait() - const approveResult = await weth9.approve(config.GATEWAY_CONTRACT, amount) + const approveResult = await weth9.approve(config.GATEWAY_CONTRACT, amount * 2n) const approveReceipt = await approveResult.wait() console.log('deposit tx', depositReceipt?.hash, 'approve tx', approveReceipt?.hash) @@ -70,16 +72,34 @@ const monitor = async () => { console.log("# Ethereum to Asset Hub") { + const destinationChainId = 1000 + const destinationFeeInDOT = 0n + const totalFee = await toPolkadot.getSendFee(context, WETH_CONTRACT, destinationChainId, destinationFeeInDOT) + const { tx } = await toPolkadot.createTx( + context.config.appContracts.gateway, + ETHEREUM_ACCOUNT_PUBLIC, + POLKADOT_ACCOUNT_PUBLIC, + WETH_CONTRACT, + destinationChainId, + amount, + totalFee, + destinationFeeInDOT, + ); + console.log('Plan tx:', tx) + console.log('Plan gas:', await context.ethereum.api.estimateGas(tx)) + console.log('Plan dry run:', await context.ethereum.api.call(tx)) + const plan = await toPolkadot.validateSend( context, ETHEREUM_ACCOUNT, POLKADOT_ACCOUNT_PUBLIC, WETH_CONTRACT, - 1000, + destinationChainId, amount, - BigInt(0) + destinationFeeInDOT ) console.log("Plan:", plan, plan.failure?.errors) + let result = await toPolkadot.send(context, ETHEREUM_ACCOUNT, plan) console.log("Execute:", result) while (true) { @@ -94,6 +114,25 @@ const monitor = async () => { console.log("# Asset Hub to Ethereum") { + const assetHubUnsigned = await toEthereum.createTx( + context.polkadot.api.assetHub, + (await context.ethereum.api.getNetwork()).chainId, + POLKADOT_ACCOUNT_PUBLIC, + ETHEREUM_ACCOUNT_PUBLIC, + WETH_CONTRACT, + amount + ); + console.log('call: ', assetHubUnsigned.tx.inner.toHex()) + console.log('utx: ', assetHubUnsigned.tx.toHex()) + console.log('payment: ', (await assetHubUnsigned.tx.paymentInfo(POLKADOT_ACCOUNT)).toHuman()) + console.log('dryRun: ', ( + await assetHubUnsigned.tx.dryRun( + POLKADOT_ACCOUNT, + { withSignedTransaction: true } + ) + ).toHuman() + ) + const plan = await toEthereum.validateSend( context, POLKADOT_ACCOUNT, @@ -103,6 +142,7 @@ const monitor = async () => { amount ) console.log("Plan:", plan, plan.failure?.errors) + const result = await toEthereum.send(context, POLKADOT_ACCOUNT, plan) console.log("Execute:", result) while (true) { @@ -117,16 +157,34 @@ const monitor = async () => { console.log("# Ethereum to Penpal") { + const destinationChainId = 2000 + const destinationFeeInDOT = 4_000_000_000n + const totalFee = await toPolkadot.getSendFee(context, WETH_CONTRACT, destinationChainId, destinationFeeInDOT) + const { tx } = await toPolkadot.createTx( + context.config.appContracts.gateway, + ETHEREUM_ACCOUNT_PUBLIC, + POLKADOT_ACCOUNT_PUBLIC, + WETH_CONTRACT, + destinationChainId, + amount, + totalFee, + destinationFeeInDOT, + ); + console.log('Plan tx:', tx) + console.log('Plan gas:', await context.ethereum.api.estimateGas(tx)) + console.log('Plan Dry run:', await context.ethereum.api.call(tx)) + const plan = await toPolkadot.validateSend( context, ETHEREUM_ACCOUNT, POLKADOT_ACCOUNT_PUBLIC, WETH_CONTRACT, - 2000, + destinationChainId, amount, - BigInt(4_000_000_000) + destinationFeeInDOT ) console.log("Plan:", plan, plan.failure?.errors) + let result = await toPolkadot.send(context, ETHEREUM_ACCOUNT, plan) console.log("Execute:", result) while (true) { @@ -141,6 +199,25 @@ const monitor = async () => { console.log("# Penpal to Ethereum") { + const assetHubUnsigned = await toEthereum.createTx( + context.polkadot.api.assetHub, + (await context.ethereum.api.getNetwork()).chainId, + POLKADOT_ACCOUNT_PUBLIC, + ETHEREUM_ACCOUNT_PUBLIC, + WETH_CONTRACT, + amount + ); + console.log('call: ', assetHubUnsigned.tx.inner.toHex()) + console.log('utx: ', assetHubUnsigned.tx.toHex()) + console.log('payment: ', (await assetHubUnsigned.tx.paymentInfo(POLKADOT_ACCOUNT)).toHuman()) + console.log('dryRun: ', ( + await assetHubUnsigned.tx.dryRun( + POLKADOT_ACCOUNT, + { withSignedTransaction: true } + ) + ).toHuman() + ) + const plan = await toEthereum.validateSend( context, POLKADOT_ACCOUNT, @@ -150,6 +227,7 @@ const monitor = async () => { amount ) console.log("Plan:", plan, plan.failure?.errors) + const result = await toEthereum.send(context, POLKADOT_ACCOUNT, plan) console.log("Execute:", result) while (true) { diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index de34a2bfb7..5d7780477c 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -104,6 +104,8 @@ importers: specifier: ^5.4.5 version: 5.5.4 + packages/contracts: {} + packages/operations: dependencies: '@aws-sdk/client-cloudwatch':