diff --git a/sdk/src/contexts/cosmos/context.ts b/sdk/src/contexts/cosmos/context.ts index 919a8843c..ea196f83f 100644 --- a/sdk/src/contexts/cosmos/context.ts +++ b/sdk/src/contexts/cosmos/context.ts @@ -425,18 +425,50 @@ export class CosmosContext< tokenAddr: string, chain: ChainName | ChainId, ): Promise { - if ( - tokenAddr === - getNativeDenom(this.context.toChainName(chain), this.context.conf.env) - ) - return 6; - const client = await this.getCosmWasmClient(chain); - const { decimals } = await client.queryContractSmart(tokenAddr, { + const name = this.context.toChainName(chain); + if (tokenAddr === getNativeDenom(name, this.context.conf.env)) { + const config = this.context.conf.chains[name]; + if (!config) throw new Error(`Config not found for chain ${chain}`); + return config.nativeTokenDecimals; + } + + // extract the cw20 from the ibc denom if the target chain is not wormchain + const cw20 = + name === 'wormchain' + ? tokenAddr + : await this.ibcDenomToCW20(tokenAddr, chain); + const client = await this.getCosmWasmClient(CHAIN_ID_WORMCHAIN); + const { decimals } = await client.queryContractSmart(cw20, { token_info: {}, }); return decimals; } + private async ibcDenomToCW20( + denom: string, + chain: ChainName | ChainId, + ): Promise { + const client = await this.getQueryClient(chain); + + let res; + try { + res = await client.ibc.transfer.denomTrace(denom); + } catch (e: any) { + if (e.message.includes('denomination trace not found')) { + throw new Error(`denom trace for ${denom} not found`); + } + throw e; + } + + if (!res.denomTrace) throw new Error(`denom trace for ${denom} not found`); + const { baseDenom } = res.denomTrace; + const parts = baseDenom.split('/'); + if (parts.length !== 3) + throw new Error(`Can't convert ${denom} to cw20 address`); + const [, , address] = parts; + return cosmos.humanAddress('wormhole', base58.decode(address)); + } + async getMessage( id: string, chain: ChainName | ChainId, diff --git a/wormhole-connect/src/routes/cosmosGateway/cosmosGateway.ts b/wormhole-connect/src/routes/cosmosGateway/cosmosGateway.ts index 8a49524dc..507eb429a 100644 --- a/wormhole-connect/src/routes/cosmosGateway/cosmosGateway.ts +++ b/wormhole-connect/src/routes/cosmosGateway/cosmosGateway.ts @@ -42,10 +42,11 @@ import { fetchRedeemedEventCosmosSource, fetchRedeemedEventNonCosmosSource, fromCosmos, - getMessageFromCosmos, - getMessageFromNonCosmos, + getMessageFromWormchain, + getUnsignedMessageFromNonCosmos, getTranslatorAddress, toCosmos, + getUnsignedMessageFromCosmos, } from './utils'; export class CosmosGatewayRoute extends BaseRoute { @@ -290,27 +291,40 @@ export class CosmosGatewayRoute extends BaseRoute { ): Promise { const name = wh.toChainName(chain); return isGatewayChain(name) - ? getMessageFromCosmos(hash, name) - : getMessageFromNonCosmos(hash, name); + ? getUnsignedMessageFromCosmos(hash, name) + : getUnsignedMessageFromNonCosmos(hash, name); } async getSignedMessage( - message: TokenTransferMessage | RelayTransferMessage, + unsignedMessage: TokenTransferMessage | RelayTransferMessage, ): Promise { // if both chains are cosmos gateway chains, no vaa is emitted - if (isGatewayChain(message.fromChain) && isGatewayChain(message.toChain)) { + if ( + isGatewayChain(unsignedMessage.fromChain) && + isGatewayChain(unsignedMessage.toChain) + ) { return { - ...message, + ...unsignedMessage, vaa: '', }; } + // If the message comes from an external chain, it will already have the info to fetch the VAA + // If it comes from a gateway chain, it will not, since the unsigned message is generated + // for the first ibc transfer, before it reaches wormchain, so at that time there is no VAA info available + // so at this point we have to query wormchain to check if the IBC transfer was relayed and the contract was called + const signedMessage = isGatewayChain(unsignedMessage.fromChain) + ? await getMessageFromWormchain( + unsignedMessage.sendTx, + unsignedMessage.fromChain, + ) + : unsignedMessage; const vaa = await fetchVaa({ - ...message, + ...signedMessage, // transfers from cosmos vaas are emitted by wormchain and not by the source chain - fromChain: isGatewayChain(message.fromChain) + fromChain: isGatewayChain(signedMessage.fromChain) ? 'wormchain' - : message.fromChain, + : signedMessage.fromChain, }); if (!vaa) { @@ -318,7 +332,7 @@ export class CosmosGatewayRoute extends BaseRoute { } return { - ...message, + ...signedMessage, vaa: utils.hexlify(vaa.bytes), }; } diff --git a/wormhole-connect/src/routes/cosmosGateway/utils/getMessage.ts b/wormhole-connect/src/routes/cosmosGateway/utils/getMessage.ts index d84d993fa..02f4744cf 100644 --- a/wormhole-connect/src/routes/cosmosGateway/utils/getMessage.ts +++ b/wormhole-connect/src/routes/cosmosGateway/utils/getMessage.ts @@ -1,29 +1,125 @@ import { CHAIN_ID_WORMCHAIN, ChainId, cosmos } from '@certusone/wormhole-sdk'; -import { IndexedTx, logs as cosmosLogs } from '@cosmjs/stargate'; +import { logs as cosmosLogs } from '@cosmjs/stargate'; import { ChainName, CosmosContext, - searchCosmosLogs, WormholeContext, + searchCosmosLogs, } from '@wormhole-foundation/wormhole-connect-sdk'; +import { BigNumber, utils } from 'ethers'; import { arrayify, base58 } from 'ethers/lib/utils.js'; import { ParsedMessage, wh } from 'utils/sdk'; -import { adaptParsedMessage } from '../../utils'; +import { isGatewayChain } from '../../../utils/cosmos'; import { UnsignedMessage } from '../../types'; -import { getCosmWasmClient } from '../utils'; +import { adaptParsedMessage } from '../../utils'; import { FromCosmosPayload, GatewayTransferMsg, IBCTransferData, } from '../types'; +import { getCosmWasmClient } from '../utils'; import { findDestinationIBCTransferTx, getIBCTransferInfoFromLogs, } from './transaction'; -import { BigNumber, utils } from 'ethers'; -import { isGatewayChain } from '../../../utils/cosmos'; -export async function getMessageFromNonCosmos( +export async function getUnsignedMessageFromCosmos( + hash: string, + chain: ChainName, +): Promise { + // Get tx on the source chain (e.g. osmosis) + const sourceClient = await getCosmWasmClient(chain); + const tx = await sourceClient.getTx(hash); + if (!tx) { + throw new Error(`Transaction ${hash} not found on chain ${chain}`); + } + + const logs = cosmosLogs.parseRawLog(tx.rawLog); + const sender = searchCosmosLogs('sender', logs); + if (!sender) throw new Error('Missing sender in transaction logs'); + + // get the information of the ibc transfer started by the source chain + const ibcPacketInfo = getIBCTransferInfoFromLogs(tx, 'send_packet'); + + // extract the IBC transfer data payload from the packet + const data: IBCTransferData = JSON.parse(ibcPacketInfo.data); + const payload: FromCosmosPayload = JSON.parse(data.memo); + + const destChain = wh.toChainName( + payload.gateway_ibc_token_bridge_payload.gateway_transfer.chain, + ); + const destContext = wh.getContext(destChain); + const payloadRecipient = + payload.gateway_ibc_token_bridge_payload.gateway_transfer.recipient; + const recipient = isGatewayChain(destChain) + ? // cosmos addresses are base64 encoded + Buffer.from(payloadRecipient, 'base64').toString() + : // receiver is an external address, decode through the chain context + destContext.parseAddress( + '0x' + Buffer.from(payloadRecipient, 'base64').toString('hex'), + ); + + const { tokenAddress, tokenChain } = await getOriginalIbcDenomInfo( + data.denom, + ); + + const base = await adaptParsedMessage({ + fromChain: chain, + sendTx: tx.hash, + toChain: destChain, + amount: BigNumber.from(data.amount), + recipient, + block: tx.height, + sender: data.sender, + gasFee: BigNumber.from(tx.gasUsed.toString()), + payloadID: 3, // should not be required for this case + tokenChain, + tokenAddress, + tokenId: { + address: tokenAddress, + chain: tokenChain, + }, + emitterAddress: '', + sequence: BigNumber.from(0), + }); + + return { + ...base, + fromChain: chain, + sender, + }; +} + +async function getOriginalIbcDenomInfo( + denom: string, +): Promise<{ tokenAddress: string; tokenChain: ChainName }> { + // transfer ibc denom follows the scheme {port}/{channel}/{denom} + // with denom as {tokenfactory}/{ibc shim}/{bas58 cw20 address} + // so 5 elements total + const parts = denom.split('/'); + if (parts.length !== 5) { + throw new Error(`Unexpected transfer denom ${denom}`); + } + const factoryDenom = parts.slice(2).join('/'); + const cw20 = factoryToCW20(factoryDenom); + const context = wh.getContext( + CHAIN_ID_WORMCHAIN, + ) as CosmosContext; + const { chainId, assetAddress: tokenAddressBytes } = + await context.getOriginalAsset(CHAIN_ID_WORMCHAIN, cw20); + const tokenChain = wh.toChainName(chainId as ChainId); // wormhole-sdk adds 0 (unset) as a chainId + const tokenContext = wh.getContext(tokenChain); + const tokenAddress = await tokenContext.parseAssetAddress( + utils.hexlify(tokenAddressBytes), + ); + + return { + tokenAddress, + tokenChain, + }; +} + +export async function getUnsignedMessageFromNonCosmos( hash: string, chain: ChainName, ): Promise { @@ -53,13 +149,6 @@ export async function getMessageFromNonCosmos( }; } -async function parseWormchainBridgeMessage( - wormchainTx: IndexedTx, -): Promise { - const message = await wh.getMessage(wormchainTx.hash, CHAIN_ID_WORMCHAIN); - return adaptParsedMessage(message); -} - function factoryToCW20(denom: string): string { if (!denom.startsWith('factory/')) return ''; const encoded = denom.split('/')[2]; @@ -67,69 +156,7 @@ function factoryToCW20(denom: string): string { return cosmos.humanAddress('wormhole', base58.decode(encoded)); } -async function parseWormchainIBCForwardMessage( - wormchainTx: IndexedTx, -): Promise { - // get the information of the ibc transfer relayed by the packet forwarding middleware - const ibcFromSourceInfo = getIBCTransferInfoFromLogs( - wormchainTx, - 'recv_packet', - ); - - const data: IBCTransferData = JSON.parse(ibcFromSourceInfo.data); - const payload: FromCosmosPayload = JSON.parse(data.memo); - - const destChain = wh.toChainName( - payload.gateway_ibc_token_bridge_payload.gateway_transfer.chain, - ); - const ibcToDestInfo = getIBCTransferInfoFromLogs(wormchainTx, 'send_packet'); - const destTx = await findDestinationIBCTransferTx(destChain, ibcToDestInfo); - if (!destTx) { - throw new Error(`Transaction on destination ${destChain} not found`); - } - - // transfer ibc denom follows the scheme {port}/{channel}/{denom} - // with denom as {tokenfactory}/{ibc shim}/{bas58 cw20 address} - // so 5 elements total - const parts = data.denom.split('/'); - if (parts.length !== 5) { - throw new Error(`Unexpected transfer denom ${data.denom}`); - } - const denom = parts.slice(2).join('/'); - const cw20 = factoryToCW20(denom); - const context = wh.getContext( - CHAIN_ID_WORMCHAIN, - ) as CosmosContext; - const { chainId, assetAddress: tokenAddressBytes } = - await context.getOriginalAsset(CHAIN_ID_WORMCHAIN, cw20); - const tokenChain = wh.toChainName(chainId as ChainId); // wormhole-sdk adds 0 (unset) as a chainId - const tokenContext = wh.getContext(tokenChain); - const tokenAddress = await tokenContext.parseAssetAddress( - utils.hexlify(tokenAddressBytes), - ); - - return adaptParsedMessage({ - fromChain: wh.toChainName(CHAIN_ID_WORMCHAIN), // gets replaced later - sendTx: wormchainTx.hash, // gets replaced later - toChain: destChain, - amount: BigNumber.from(data.amount), - recipient: data.receiver, - block: destTx.height, - sender: data.sender, - gasFee: BigNumber.from(destTx.gasUsed.toString()), - payloadID: 3, // should not be required for this case - tokenChain, - tokenAddress, - tokenId: { - address: tokenAddress, - chain: tokenChain, - }, - emitterAddress: '', - sequence: BigNumber.from(0), - }); -} - -export async function getMessageFromCosmos( +export async function getMessageFromWormchain( hash: string, chain: ChainName, ): Promise { @@ -161,14 +188,8 @@ export async function getMessageFromCosmos( ); } - // TODO: refactor these two lines (repeated in parseWormchainIBCForwardMessage) - const data: IBCTransferData = JSON.parse(ibcInfo.data); - const payload: FromCosmosPayload = JSON.parse(data.memo); - const parsed = await (isGatewayChain( - payload.gateway_ibc_token_bridge_payload.gateway_transfer.chain, - ) - ? parseWormchainIBCForwardMessage(destTx) - : parseWormchainBridgeMessage(destTx)); + const message = await wh.getMessage(destTx.hash, CHAIN_ID_WORMCHAIN); + const parsed = await adaptParsedMessage(message); return { ...parsed,