diff --git a/connect/src/routes/portico/automatic.ts b/connect/src/routes/portico/automatic.ts index b8bf35404..426ff843e 100644 --- a/connect/src/routes/portico/automatic.ts +++ b/connect/src/routes/portico/automatic.ts @@ -20,6 +20,7 @@ import type { SourceInitiatedTransferReceipt, TokenId, TransactionId, + TransferWarning, } from "./../../index.js"; import { PorticoBridge, @@ -328,21 +329,28 @@ export class AutomaticPorticoRoute if (isAttested(receipt)) { const toChain = this.wh.getChain(receipt.to); const toPorticoBridge = await toChain.getPorticoBridge(); - const isCompleted = await toPorticoBridge.isTransferCompleted( + + const { swapResult, receivedToken } = await toPorticoBridge.getTransferResult( receipt.attestation.attestation, ); - if (isCompleted) { + + if (swapResult === "success" || swapResult === "failed") { + const warnings = + swapResult === "failed" + ? [{ type: "SwapFailedWarning" } satisfies TransferWarning] + : undefined; + const transferResult = receivedToken ? { receivedToken, warnings } : undefined; + receipt = { ...receipt, state: TransferState.DestinationFinalized, + transferResult, } satisfies CompletedTransferReceipt>; yield receipt; } } - // TODO: handle swap failed case (highway token received) - yield receipt; } diff --git a/connect/src/types.ts b/connect/src/types.ts index f2cf45266..b82d058d7 100644 --- a/connect/src/types.ts +++ b/connect/src/types.ts @@ -6,7 +6,7 @@ import type { TokenId, TransactionId, } from "@wormhole-foundation/sdk-definitions"; -import type { QuoteWarning } from "./warnings.js"; +import type { TransferWarning, QuoteWarning } from "./warnings.js"; // Transfer state machine states export enum TransferState { @@ -105,6 +105,7 @@ export interface CompletedTransferReceipt[]; attestation: AT; destinationTxs?: TransactionId[]; + transferResult?: TransferResult; } export interface FailedTransferReceipt @@ -202,7 +203,7 @@ export interface TransferQuote { token: TokenId; amount: bigint; }; - // If the transfer being quoted asked for native gas dropoff + // If the transfer being quoted asked for native gas drop-off // this will contain the amount of native gas that is to be minted // on the destination chain given the current swap rates destinationNativeGas?: bigint; @@ -212,3 +213,14 @@ export interface TransferQuote { // Estimated time to completion in milliseconds eta?: number; } + +// Information about the result of a transfer +export interface TransferResult { + // How much of what token was received + receivedToken: { + token: TokenId; + amount: bigint; + } + // Any warnings that occurred (e.g. swap failed) + warnings?: TransferWarning[]; +} diff --git a/connect/src/warnings.ts b/connect/src/warnings.ts index c4c06efb5..33ec19bce 100644 --- a/connect/src/warnings.ts +++ b/connect/src/warnings.ts @@ -9,3 +9,9 @@ export type GovernorLimitWarning = { }; export type QuoteWarning = DestinationCapacityWarning | GovernorLimitWarning; + +export type SwapFailedWarning = { + type: "SwapFailedWarning"; +}; + +export type TransferWarning = SwapFailedWarning; diff --git a/core/definitions/src/protocols/portico/portico.ts b/core/definitions/src/protocols/portico/portico.ts index 7da0bdb94..75da8e392 100644 --- a/core/definitions/src/protocols/portico/portico.ts +++ b/core/definitions/src/protocols/portico/portico.ts @@ -34,6 +34,14 @@ export namespace PorticoBridge { relayerFee: bigint; }; + export type TransferResult = { + swapResult: "success" | "failed" | "pending"; + receivedToken?: { + token: TokenId; + amount: bigint; + }; + }; + const _transferPayloads = ["Transfer"] as const; const _payloads = [..._transferPayloads] as const; @@ -113,4 +121,7 @@ export interface PorticoBridge; } diff --git a/platforms/evm/protocols/portico/src/abis.ts b/platforms/evm/protocols/portico/src/abis.ts index 649eb019e..3ed1f1a30 100644 --- a/platforms/evm/protocols/portico/src/abis.ts +++ b/platforms/evm/protocols/portico/src/abis.ts @@ -3,6 +3,7 @@ import { ethers } from 'ethers'; export const porticoAbi = new ethers.Interface([ 'function start((bytes32,address,address,address,address,address,uint256,uint256,uint256,uint256)) returns (address,uint16,uint64)', 'function receiveMessageAndSwap(bytes)', + 'event PorticoSwapFinish(bool swapCompleted, uint256 finaluserAmount, uint256 relayerFeeAmount, tuple(bytes32 flags, address finalTokenAddress, address recipientAddress, uint256 canonAssetAmount, uint256 minAmountFinish, uint256 relayerFee) data)', ]); export const porticoSwapFinishedEvent = diff --git a/platforms/evm/protocols/portico/src/bridge.ts b/platforms/evm/protocols/portico/src/bridge.ts index 227f30a2e..9a7235201 100644 --- a/platforms/evm/protocols/portico/src/bridge.ts +++ b/platforms/evm/protocols/portico/src/bridge.ts @@ -37,6 +37,7 @@ import { EvmWormholeCore } from '@wormhole-foundation/sdk-evm-core'; import { EvmTokenBridge } from '@wormhole-foundation/sdk-evm-tokenbridge'; import '@wormhole-foundation/sdk-evm-tokenbridge'; +import { finality } from '@wormhole-foundation/sdk-connect'; export class EvmPorticoBridge< N extends Network, @@ -349,4 +350,88 @@ export class EvmPorticoBridge< } return portico.uniswapQuoterV2; } + + async getTransferResult( + vaa: PorticoBridge.VAA, + ): Promise { + // First check if the transfer is completed + const isCompleted = await this.isTransferCompleted(vaa); + if (!isCompleted) return { swapResult: 'pending' }; + + const finalToken = Wormhole.tokenId( + this.chain, + vaa.payload.payload.flagSet.flags.shouldUnwrapNative + ? 'native' + : vaa.payload.payload.finalTokenAddress.toNative(this.chain).toString(), + ); + + const finalAmount = (() => { + const amountLessFee = + vaa.payload.payload.minAmountFinish - vaa.payload.payload.relayerFee; + return amountLessFee < 0n ? 0n : amountLessFee; + })(); + + const defaultResult: PorticoBridge.TransferResult = { + swapResult: 'success', + receivedToken: { + token: finalToken, + amount: finalAmount, + }, + }; + + // This is a simplification since there is no swap on Ethereum + // since the highway token originates there + if (this.chain === 'Ethereum') return defaultResult; + + // Check if the swap succeeded or failed + // NOTE: If we can't find the event, assume the swap succeeded + const { tokenBridge } = this.tokenBridge; + const filter = tokenBridge.filters.TransferRedeemed( + toChainId(vaa.emitterChain), + vaa.emitterAddress.toString(), + vaa.sequence, + ); + + // Search for the event in the last 15 minutes or 5000 blocks (whichever is smaller) + const latestBlock = await EvmPlatform.getLatestBlock(this.provider); + const blockTime = finality.blockTime.get(this.chain)!; + const fromBlock = + latestBlock - Math.min(5000, Math.floor((15 * 60 * 1000) / blockTime)); + + const logs = await tokenBridge + .queryFilter(filter, fromBlock, latestBlock) + .catch(() => null); + if (!logs || logs.length === 0) return defaultResult; + + const txhash = logs[0]!.transactionHash; + const receipt = await this.provider.getTransactionReceipt(txhash); + if (!receipt) return defaultResult; + + const [event] = receipt.logs + .map((log) => porticoAbi.parseLog(log)) + .filter((log) => log); + if (!event) return defaultResult; + + const swapCompleted = event.args.swapCompleted; + const finaluserAmount = event.args.finaluserAmount; + + // If the swap failed, the highway / Wormhole-wrapped token is received instead + // of the finalToken + const token = swapCompleted + ? finalToken + : Wormhole.tokenId( + this.chain, + ( + await this.tokenBridge.getWrappedAsset(vaa.payload.token) + ).toString(), + ); + + return { + swapResult: swapCompleted ? 'success' : 'failed', + receivedToken: { + token, + amount: finaluserAmount, + }, + }; + } } diff --git a/platforms/evm/protocols/portico/src/index.ts b/platforms/evm/protocols/portico/src/index.ts index 02ad5d833..759707d3d 100644 --- a/platforms/evm/protocols/portico/src/index.ts +++ b/platforms/evm/protocols/portico/src/index.ts @@ -5,3 +5,4 @@ import { EvmPorticoBridge } from './bridge.js'; registerProtocol(_platform, 'PorticoBridge', EvmPorticoBridge); export * from './bridge.js'; +export * from './consts.js';