diff --git a/web/packages/api/package.json b/web/packages/api/package.json index 6f3ccb5130..178387d7a3 100644 --- a/web/packages/api/package.json +++ b/web/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@snowbridge/api", - "version": "0.1.3", + "version": "0.1.4", "description": "Snowbridge API client", "license": "Apache-2.0", "repository": { diff --git a/web/packages/api/src/assets.ts b/web/packages/api/src/assets.ts new file mode 100644 index 0000000000..9a21097dba --- /dev/null +++ b/web/packages/api/src/assets.ts @@ -0,0 +1,101 @@ +import { ApiPromise } from "@polkadot/api" +import { Codec, Registry } from "@polkadot/types/types" +import { IERC20Metadata__factory, IERC20__factory } from "@snowbridge/contract-types" +import { Context } from './index' + +export const parachainNativeToken = async (api: ApiPromise): Promise<{ + tokenSymbol: string + tokenDecimal: number + ss58Format: number | null +}> => { + const properties = await api.rpc.system.properties() + const tokenSymbols = properties.tokenSymbol.unwrapOrDefault() + const tokenDecimals = properties.tokenDecimals.unwrapOrDefault() + + return { + tokenSymbol: tokenSymbols.at(0)?.toString() ?? "DOT", + tokenDecimal: tokenDecimals.at(0)?.toNumber() ?? 10, + ss58Format: properties.ss58Format.unwrapOr(null)?.toNumber() ?? null, + } +} + +export const erc20TokenToAssetLocation = (registry: Registry, ethChainId: bigint, token: string) => { + return registry.createType('StagingXcmV3MultiLocation', { + parents: 2, + interior: { + X2: [ + { GlobalConsensus: { Ethereum: { chain_id: ethChainId } } }, + { AccountKey20: { key: token } }, + ] + } + }) +} + +export const assetStatusInfo = async (context: Context, tokenAddress: string, ownerAddress?: string) => { + let [ethereumNetwork, isTokenRegistered] = await Promise.all([ + context.ethereum.api.getNetwork(), + context.ethereum.contracts.gateway.isTokenRegistered(tokenAddress) + ]) + + const ethereumChainId = ethereumNetwork.chainId + const multiLocation = erc20TokenToAssetLocation(context.polkadot.api.assetHub.registry, ethereumChainId, tokenAddress) + const foreignAsset = (await context.polkadot.api.assetHub.query.foreignAssets.asset(multiLocation)).toPrimitive() as { status: 'Live' } + + const tokenContract = IERC20__factory.connect(tokenAddress, context.ethereum.api) + let ownerBalance = BigInt(0) + let tokenGatewayAllowance = BigInt(0) + let isValidERC20 = true + try { + const erc20balance = await assetErc20Balance(context, tokenAddress, ownerAddress ?? '0x0000000000000000000000000000000000000000') + ownerBalance = erc20balance.balance + tokenGatewayAllowance = erc20balance.gatewayAllowance + + } catch { + isValidERC20 = false + } + + return { + ethereumChainId, + multiLocation, + isValidERC20, + tokenContract, + isTokenRegistered, + tokenGatewayAllowance, + ownerBalance, + foreignAssetExists: foreignAsset !== null, + foreignAsset, + } +} + +export const assetErc20Balance = async (context: Context, token: string, owner: string): Promise<{ + balance: bigint, + gatewayAllowance: bigint, +}> => { + const tokenContract = IERC20__factory.connect(token, context.ethereum.api) + const gateway = await context.ethereum.contracts.gateway.getAddress() + const [balance, gatewayAllowance] = await Promise.all([ + tokenContract.balanceOf(owner), + tokenContract.allowance(owner, gateway), + ]) + return { + balance, gatewayAllowance + } +} + +export const assetErc20Metadata = async (context: Context, tokenAddress: string) => { + const tokenMetadata = IERC20Metadata__factory.connect(tokenAddress, context.ethereum.api) + const [name, symbol, decimals] = await Promise.all([ + tokenMetadata.name(), + tokenMetadata.symbol(), + tokenMetadata.decimals(), + ]) + return { name, symbol, decimals } +} + +export const palletAssetsBalance = async (api: ApiPromise, location: Codec, address: string, instance = "assets"): Promise => { + let account = (await api.query[instance].account(location, address)).toPrimitive() as any + if (account !== null) { + return BigInt(account.balance) + } + return null +} diff --git a/web/packages/api/src/environment.ts b/web/packages/api/src/environment.ts index c49bd31064..4661fc4865 100644 --- a/web/packages/api/src/environment.ts +++ b/web/packages/api/src/environment.ts @@ -17,7 +17,7 @@ export type Config = { export type SourceType = 'substrate' | 'ethereum' export type Relayer = { name: string, account: string, type: SourceType } -export type ParachainInfo = { paraId: number, destinationFeeDOT: bigint, has20ByteAccounts: boolean, decimals: number } +export type ParachainInfo = { paraId: number, destinationFeeDOT: bigint, has20ByteAccounts: boolean, decimals: number, ss58Format?: number } export type TransferLocation = { id: string name: string @@ -77,7 +77,7 @@ export const SNOWBRIDGE_ENV: { [id: string]: SnowbridgeEnvironment } = { }], config: { BEACON_HTTP_API: 'http://127.0.0.1:9596', - ETHEREUM_WS_API: (_) => 'ws://127.0.0.1:8546', + ETHEREUM_WS_API: () => 'ws://127.0.0.1:8546', RELAY_CHAIN_WS_URL: 'ws://127.0.0.1:9944', ASSET_HUB_WS_URL: 'ws://127.0.0.1:12144', BRIDGE_HUB_WS_URL: 'ws://127.0.0.1:11144', @@ -139,7 +139,7 @@ export const SNOWBRIDGE_ENV: { [id: string]: SnowbridgeEnvironment } = { destinationIds: [], paraInfo: { paraId: 3369, - destinationFeeDOT: 4_000_000_000n, + destinationFeeDOT: 4_000_000_000_000n, has20ByteAccounts: true, decimals: 12, }, diff --git a/web/packages/api/src/index.ts b/web/packages/api/src/index.ts index 9dc2b990eb..01f58b6600 100644 --- a/web/packages/api/src/index.ts +++ b/web/packages/api/src/index.ts @@ -135,4 +135,5 @@ export * as toPolkadot from './toPolkadot' export * as toEthereum from './toEthereum' export * as utils from './utils' export * as status from './status' +export * as assets from './assets' export * as environment from './environment' diff --git a/web/packages/api/src/query.ts b/web/packages/api/src/query.ts index 772dd8a81c..395595d331 100644 --- a/web/packages/api/src/query.ts +++ b/web/packages/api/src/query.ts @@ -32,7 +32,7 @@ export const scanSubstrateEvents = async ( export const waitForMessageQueuePallet = async ( parachain: ApiPromise, - messageId: string | undefined, + messageId: string, siblingParachain: number, eventFilter: (event: Codec) => boolean, options = { @@ -50,8 +50,7 @@ export const waitForMessageQueuePallet = async ( for (const event of (events as any)) { let eventData = event.event.toPrimitive().data if (parachain.events.messageQueue.Processed.is(event.event) - // TODO: Use SetTopic to forward the message id to the destination chain and then remove undefined check. - && (messageId === undefined || eventData[0].toLowerCase() === messageId.toLowerCase()) + && eventData[0].toLowerCase() === messageId.toLowerCase() && eventData[1]?.sibling === siblingParachain) { foundMessageQueue = true diff --git a/web/packages/api/src/status.ts b/web/packages/api/src/status.ts index 7927675e15..8acee5dc60 100644 --- a/web/packages/api/src/status.ts +++ b/web/packages/api/src/status.ts @@ -1,4 +1,3 @@ -import { IERC20Metadata__factory, IERC20__factory } from '@snowbridge/contract-types' import { Context } from './index' import { fetchBeaconSlot } from './utils' @@ -109,61 +108,3 @@ export const channelStatusInfo = async (context: Context, channelId: string): Pr }, } } - -export const assetStatusInfo = async (context: Context, tokenAddress: string, ownerAddress?: string) => { - let [ethereumNetwork, gatewayAddress, isTokenRegistered] = await Promise.all([ - context.ethereum.api.getNetwork(), - context.ethereum.contracts.gateway.getAddress(), - context.ethereum.contracts.gateway.isTokenRegistered(tokenAddress) - ]) - - const ethereumChainId = ethereumNetwork.chainId - const multiLocation = context.polkadot.api.assetHub.createType('StagingXcmV3MultiLocation', { - parents: 2, - interior: { - X2: [ - { GlobalConsensus: { Ethereum: { chain_id: ethereumChainId } } }, - { AccountKey20: { key: tokenAddress } }, - ] - } - }) - const foreignAsset = (await context.polkadot.api.assetHub.query.foreignAssets.asset(multiLocation)).toPrimitive() as { status: 'Live' } - - const tokenContract = IERC20__factory.connect(tokenAddress, context.ethereum.api) - let ownerBalance = BigInt(0) - let tokenGatewayAllowance = BigInt(0) - let isValidERC20 = true - try { - const owner = ownerAddress || "0x0000000000000000000000000000000000000000" - const [tokenBalance_, tokenGatewayAllowance_] = await Promise.all([ - tokenContract.balanceOf(owner), - tokenContract.allowance(owner, gatewayAddress), - ]) - ownerBalance = tokenBalance_ - tokenGatewayAllowance = tokenGatewayAllowance_ - } catch { - isValidERC20 = false - } - - return { - ethereumChainId, - multiLocation, - isValidERC20, - tokenContract, - isTokenRegistered, - tokenGatewayAllowance, - ownerBalance, - foreignAssetExists: foreignAsset !== null, - foreignAsset, - } -} - -export const assetMetadata = async (context: Context, tokenAddress: string) => { - const tokenMetadata = IERC20Metadata__factory.connect(tokenAddress, context.ethereum.api) - const [name, symbol, decimal] = await Promise.all([ - tokenMetadata.name(), - tokenMetadata.symbol(), - tokenMetadata.decimals(), - ]) - return { name, symbol, decimal } -} diff --git a/web/packages/api/src/toEthereum.ts b/web/packages/api/src/toEthereum.ts index b423e0c70c..b99e520936 100644 --- a/web/packages/api/src/toEthereum.ts +++ b/web/packages/api/src/toEthereum.ts @@ -2,9 +2,10 @@ import { EventRecord } from "@polkadot/types/interfaces" import { Codec, IKeyringPair, Signer } from "@polkadot/types/types" import { BN, u8aToHex } from "@polkadot/util" import { decodeAddress, xxhashAsHex } from "@polkadot/util-crypto" -import { Context } from "./index" +import { assetStatusInfo, palletAssetsBalance } from "./assets" +import { Context, utils } from "./index" import { scanSubstrateEvents, waitForMessageQueuePallet } from "./query" -import { assetStatusInfo, bridgeStatusInfo } from "./status" +import { bridgeStatusInfo } from "./status" import { paraIdToChannelId } from "./utils" export interface WalletSigner { @@ -103,9 +104,9 @@ export const validateSend = async (context: Context, signer: WalletOrKeypair, so const assetInfo = await assetStatusInfo(context, tokenAddress) const foreignAssetExists = assetInfo.foreignAsset !== null && assetInfo.foreignAsset.status === 'Live' - if (!foreignAssetExists) errors.push({ code: SendValidationCode.ForeignAssetMissing, message: "Foreign asset is not registered on Asset Hub."}) - if (!assetInfo.isTokenRegistered) errors.push({ code: SendValidationCode.ERC20NotRegistered, message: "ERC20 token is not registered with the Snowbridge Gateway."}) - if (!assetInfo.isValidERC20) errors.push({ code: SendValidationCode.ERC20InvalidToken, message: "Token address is not a valid ERC20 token."}) + if (!foreignAssetExists) errors.push({ code: SendValidationCode.ForeignAssetMissing, message: "Foreign asset is not registered on Asset Hub." }) + if (!assetInfo.isTokenRegistered) errors.push({ code: SendValidationCode.ERC20NotRegistered, message: "ERC20 token is not registered with the Snowbridge Gateway." }) + if (!assetInfo.isValidERC20) errors.push({ code: SendValidationCode.ERC20InvalidToken, message: "Token address is not a valid ERC20 token." }) let parachainHasPalletXcm = true let hrmpChannelSetup = true @@ -126,26 +127,20 @@ export const validateSend = async (context: Context, signer: WalletOrKeypair, so validatedAtHash: u8aToHex(sourceParachainHead), } if (foreignAssetExists) { - let account = (await parachains[sourceParachainId].query.foreignAssets.account(assetInfo.multiLocation, signer.address)).toPrimitive() as any - if (account !== null) { - assetBalance = BigInt(account.balance) - } - hasAsset = assetBalance >= amount + assetBalance = (await palletAssetsBalance(parachains[sourceParachainId], assetInfo.multiLocation, signer.address, "foreignAssets")) ?? 0n + hasAsset = (assetBalance) >= amount } } else { if (foreignAssetExists) { - let account = (await assetHub.query.foreignAssets.account(assetInfo.multiLocation, signer.address)).toPrimitive() as any - if (account !== null) { - assetBalance = BigInt(account.balance) - } + assetBalance = (await palletAssetsBalance(assetHub, assetInfo.multiLocation, signer.address, "foreignAssets")) ?? 0n hasAsset = assetBalance >= amount } } - if(!parachainKnownToContext) errors.push({ code: SendValidationCode.ParachainContextMissing, message: "The source parachain is missing from context configuration."}) - if(!parachainHasPalletXcm) errors.push({ code: SendValidationCode.PalletXcmMissing, message: "The source parachain does not have pallet-xcm."}) - if(!hrmpChannelSetup) errors.push({ code: SendValidationCode.NoHRMPChannelToAssetHub, message: "The source parachain does have an open HRMP channel to Asset Hub."}) - if (!hasAsset) errors.push({ code: SendValidationCode.InsufficientAsset, message: "Asset balance insufficient for transfer."}) + if (!parachainKnownToContext) errors.push({ code: SendValidationCode.ParachainContextMissing, message: "The source parachain is missing from context configuration." }) + if (!parachainHasPalletXcm) errors.push({ code: SendValidationCode.PalletXcmMissing, message: "The source parachain does not have pallet-xcm." }) + if (!hrmpChannelSetup) errors.push({ code: SendValidationCode.NoHRMPChannelToAssetHub, message: "The source parachain does have an open HRMP channel to Asset Hub." }) + if (!hasAsset) errors.push({ code: SendValidationCode.InsufficientAsset, message: "Asset balance insufficient for transfer." }) const [bridgeStatus] = await Promise.all([ bridgeStatusInfo(context), @@ -155,8 +150,8 @@ export const validateSend = async (context: Context, signer: WalletOrKeypair, so const bridgeOperational = bridgeStatus.toEthereum.operatingMode.outbound === 'Normal' const lightClientLatencyIsAcceptable = bridgeStatus.toEthereum.latencySeconds < options.acceptableLatencyInSeconds - if (!bridgeOperational) errors.push({ code: SendValidationCode.BridgeNotOperational, message: "Bridge status is not operational."}) - if (!lightClientLatencyIsAcceptable) errors.push({ code: SendValidationCode.LightClientLatencyTooHigh, message: "Light client is too far behind."}) + if (!bridgeOperational) errors.push({ code: SendValidationCode.BridgeNotOperational, message: "Bridge status is not operational." }) + if (!lightClientLatencyIsAcceptable) errors.push({ code: SendValidationCode.LightClientLatencyTooHigh, message: "Light client is too far behind." }) const [account, fee] = await Promise.all([ assetHub.query.system.account(signer.address), @@ -164,7 +159,7 @@ export const validateSend = async (context: Context, signer: WalletOrKeypair, so ]) const dotBalance = BigInt((account.toPrimitive() as any).data.free) const canPayFee = fee < dotBalance - if (!canPayFee) errors.push({ code: SendValidationCode.InsufficientFee, message: "Insufficient DOT balance to pay fees."}) + if (!canPayFee) errors.push({ code: SendValidationCode.InsufficientFee, message: "Insufficient DOT balance to pay fees." }) if (errors.length === 0) { return { @@ -328,6 +323,15 @@ export const send = async (context: Context, signer: WalletOrKeypair, plan: Send parents: 0, interior: { X1: { AccountId32: { id: plan.success.sourceAddressRaw } } } } + + const parachainSignedTx = await parachainApi.tx.polkadotXcm.transferAssets( + pDestination, + pBeneficiary, + pAssets, + fee_asset, + weight + ).signAsync(addressOrPair, { signer: walletSigner }) + pResult = await new Promise<{ blockNumber: number blockHash: string @@ -339,15 +343,10 @@ export const send = async (context: Context, signer: WalletOrKeypair, plan: Send messageId?: string }>((resolve, reject) => { try { - parachainApi.tx.polkadotXcm.transferAssets( - pDestination, - pBeneficiary, - pAssets, - fee_asset, - weight - ).signAndSend(addressOrPair, { signer: walletSigner }, (c) => { + parachainSignedTx.send(c => { if (c.isError) { - reject(c.internalError || c.dispatchError) + console.error(c) + reject(c.internalError || c.dispatchError || c) } if (c.isFinalized) { const result = { @@ -382,12 +381,13 @@ export const send = async (context: Context, signer: WalletOrKeypair, plan: Send }) } catch (e) { + console.error(e) reject(e) } }) pResult.blockHash = u8aToHex(await parachainApi.rpc.chain.getBlockHash(pResult.blockNumber)) - if (!pResult.success) { + if (!pResult.success || pResult.messageId === undefined) { return { failure: { sourceParachain: pResult, @@ -397,7 +397,7 @@ export const send = async (context: Context, signer: WalletOrKeypair, plan: Send } const { extrinsicSuccess, allEvents, foundEvent } = await waitForMessageQueuePallet(assetHub, pResult.messageId, plan.success.sourceParachain.paraId, - _ => true, + () => true, options, ) if (!extrinsicSuccess) { @@ -430,6 +430,14 @@ export const send = async (context: Context, signer: WalletOrKeypair, plan: Send interior: { X1: { AccountKey20: { key: plan.success.beneficiary } } } } + const assetHubSignedTx = await assetHub.tx.polkadotXcm.transferAssets( + destination, + beneficiary, + assets, + fee_asset, + weight + ).signAsync(addressOrPair, { signer: walletSigner }); + let result = await new Promise<{ blockNumber: number txIndex: number @@ -440,16 +448,12 @@ export const send = async (context: Context, signer: WalletOrKeypair, plan: Send messageId?: string }>((resolve, reject) => { try { - assetHub.tx.polkadotXcm.transferAssets( - destination, - beneficiary, - assets, - fee_asset, - weight - ).signAndSend(addressOrPair, { signer: walletSigner }, (c) => { - if (c.isError) { - reject(c.internalError || c.dispatchError) - } + assetHubSignedTx.send(c => { + if (c.status) + if (c.isError) { + console.error(c) + reject(c.internalError || c.dispatchError || c) + } if (c.isFinalized) { const result = { txHash: u8aToHex(c.txHash), @@ -483,6 +487,7 @@ export const send = async (context: Context, signer: WalletOrKeypair, plan: Send } catch (e) { + console.error(e) reject(e) } }) @@ -534,12 +539,11 @@ export const trackSendProgressPolling = async (context: Context, result: SendRes } if (success.polling === undefined) { - let a = - success.polling = { - bridgeHubMessageQueueProcessed: (await bridgeHub.rpc.chain.getHeader(success.bridgeHub.submittedAtHash)).number.toBigInt() + 1n, - ethereumBeefyClient: BigInt((await ethereum.api.getBlock(success.ethereum.submittedAtHash))?.number ?? 0n) + 1n, - ethereumMessageDispatched: BigInt((await ethereum.api.getBlock(success.ethereum.submittedAtHash))?.number ?? 0n) + 1n, - } + success.polling = { + bridgeHubMessageQueueProcessed: (await bridgeHub.rpc.chain.getHeader(success.bridgeHub.submittedAtHash)).number.toBigInt() + 1n, + ethereumBeefyClient: BigInt((await ethereum.api.getBlock(success.ethereum.submittedAtHash))?.number ?? 0n) + 1n, + ethereumMessageDispatched: BigInt((await ethereum.api.getBlock(success.ethereum.submittedAtHash))?.number ?? 0n) + 1n, + } } if (success.bridgeHub.events === undefined) { @@ -628,7 +632,10 @@ export async function* trackSendProgress(context: Context, result: SendResult, o const { success } = result if (result.failure || !success || !success.plan.success) { - throw new Error('Send failed') + throw Error('Send failed') + } + if (success.messageId === undefined) { + throw Error('No message Id') } if (success.bridgeHub.events === undefined) { @@ -636,7 +643,7 @@ export async function* trackSendProgress(context: Context, result: SendResult, o let nonce: bigint | undefined = undefined let { extrinsicSuccess, allEvents: receivedEvents } = await waitForMessageQueuePallet( bridgeHub, - undefined, + utils.forwardedTopicId(success.messageId), success.plan.success.assetHub.paraId, eventRow => { let event = eventRow as any diff --git a/web/packages/api/src/toPolkadot.ts b/web/packages/api/src/toPolkadot.ts index a6acaa51ce..b62e177b6d 100644 --- a/web/packages/api/src/toPolkadot.ts +++ b/web/packages/api/src/toPolkadot.ts @@ -3,11 +3,12 @@ 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 { ContractTransactionReceipt, LogDescription, Signer, ethers } from 'ethers' +import { LogDescription, Signer, TransactionReceipt, ethers, keccak256 } from 'ethers' import { concatMap, filter, firstValueFrom, lastValueFrom, take, takeWhile, tap } from 'rxjs' +import { assetStatusInfo } from './assets' import { Context } from './index' import { scanSubstrateEvents, waitForMessageQueuePallet } from './query' -import { assetStatusInfo, bridgeStatusInfo, channelStatusInfo } from './status' +import { bridgeStatusInfo, channelStatusInfo } from './status' import { beneficiaryMultiAddress, fetchBeaconSlot, paraIdToChannelId, paraIdToSovereignAccount } from './utils' export enum SendValidationCode { @@ -83,7 +84,7 @@ export const getSendFee = async (context: Context, tokenAddress: string, destina export const validateSend = async (context: Context, source: ethers.Addressable, beneficiary: string, tokenAddress: string, destinationParaId: number, amount: bigint, destinationFee: bigint, options = { acceptableLatencyInSeconds: 28800 /* 3 Hours */ }): Promise => { - const { ethereum, ethereum: { contracts: { gateway } }, polkadot: { api: { assetHub, bridgeHub, relaychain } } } = context + const { ethereum, polkadot: { api: { assetHub, bridgeHub, relaychain } } } = context const sourceAddress = await source.getAddress() @@ -249,7 +250,7 @@ export type SendResult = { } } failure?: { - receipt: ContractTransactionReceipt + receipt: TransactionReceipt } } @@ -271,6 +272,9 @@ export const send = async (context: Context, signer: Signer, plan: SendValidatio ]) const contract = IGateway__factory.connect(context.config.appContracts.gateway, signer) + const fees = await context.ethereum.api.getFeeData() + + const response = await contract.sendToken( success.token, success.destinationParaId, @@ -282,6 +286,31 @@ export const send = async (context: Context, signer: Signer, plan: SendValidatio } ) 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') } @@ -559,7 +588,6 @@ export async function* trackSendProgress(context: Context, result: SendResult, o ), { defaultValue: undefined } ) - console.log(receivedEvents?.toHuman()) if (receivedEvents === undefined) { throw Error('Timeout while waiting for Bridge Hub delivery.') } diff --git a/web/packages/api/src/utils.ts b/web/packages/api/src/utils.ts index 0735a30aed..335da63a8c 100644 --- a/web/packages/api/src/utils.ts +++ b/web/packages/api/src/utils.ts @@ -1,5 +1,5 @@ import { Registry } from "@polkadot/types/types" -import { bnToU8a, isHex, stringToU8a, u8aToHex } from "@polkadot/util" +import { bnToU8a, hexToU8a, isHex, stringToU8a, u8aToHex } from "@polkadot/util" import { blake2AsU8a, decodeAddress, keccak256AsU8a } from "@polkadot/util-crypto" import { MultiAddressStruct } from "@snowbridge/contract-types/src/IGateway" import { ethers } from "ethers" @@ -12,9 +12,9 @@ export const paraIdToSovereignAccount = (type: 'para' | 'sibl', paraId: number): return u8aToHex(address) } -export const paraIdToAgentId = (register: Registry, paraId: number): string => { +export const paraIdToAgentId = (registry: Registry, paraId: number): string => { const typeEncoded = stringToU8a('SiblingChain') - const paraIdEncoded = register.createType('Compact', paraId).toU8a() + const paraIdEncoded = registry.createType('Compact', paraId).toU8a() const joined = new Uint8Array([...typeEncoded, ...paraIdEncoded, 0x00]) const agentId = blake2AsU8a(joined, 256) return u8aToHex(agentId) @@ -28,6 +28,16 @@ export const paraIdToChannelId = (paraId: number): string => { return u8aToHex(channelId) } +export const forwardedTopicId = (messageId: string): string => { + // From rust code + // (b"forward_id_for", original_id).using_encoded(sp_io::hashing::blake2_256) + const typeEncoded = stringToU8a('forward_id_for') + const paraIdEncoded = hexToU8a(messageId) + const joined = new Uint8Array([...typeEncoded, ...paraIdEncoded]) + const newTopicId = blake2AsU8a(joined, 256) + return u8aToHex(newTopicId) +} + export const beneficiaryMultiAddress = (beneficiary: string) => { const abi = ethers.AbiCoder.defaultAbiCoder() diff --git a/web/packages/contract-types/package.json b/web/packages/contract-types/package.json index b4aad7df35..22fff4b7cc 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.3", + "version": "0.1.4", "description": "Snowbridge contract type bindings", "license": "Apache-2.0", "repository": { diff --git a/web/packages/operations/src/transfer_token.ts b/web/packages/operations/src/transfer_token.ts index dbfa3212d8..2edac87cc4 100644 --- a/web/packages/operations/src/transfer_token.ts +++ b/web/packages/operations/src/transfer_token.ts @@ -1,6 +1,6 @@ -import { contextFactory, destroyContext, toEthereum, toPolkadot, environment } from '@snowbridge/api' import { Keyring } from '@polkadot/keyring' +import { contextFactory, destroyContext, environment, toEthereum, toPolkadot } from '@snowbridge/api' import { Wallet } from 'ethers' const monitor = async () => {