diff --git a/connect/src/protocols/cctpTransfer.ts b/connect/src/protocols/cctpTransfer.ts index d0f335f9c6..c89cb29f59 100644 --- a/connect/src/protocols/cctpTransfer.ts +++ b/connect/src/protocols/cctpTransfer.ts @@ -473,6 +473,22 @@ export class CircleTransfer ): TransferReceipt { const { from, to } = xfer.transfer; + // This attestation may be either the auto relay vaa or the circle attestation + // depending on the request + + let receipt: Partial> = { + protocol: xfer.transfer.automatic ? "AutomaticCircleBridge" : "CircleBridge", + request: xfer.transfer, + from: from.chain, + to: to.chain, + state: TransferState.Created, + }; + + const originTxs = xfer.txids.filter((txid) => txid.chain === xfer.transfer.from.chain); + if (originTxs.length > 0) { + receipt = { ...receipt, state: TransferState.SourceInitiated, originTxs }; + } + const att = xfer.attestations.filter((a) => isWormholeMessageId(a.id), ) as AttestationReceipt<"AutomaticCircleBridge">[]; @@ -481,27 +497,17 @@ export class CircleTransfer isCircleMessageId(a.id), ) as AttestationReceipt<"CircleBridge">[]; - // This attestation may be either the auto relay vaa or the circle attestation - // depending on the request const attestation = att.length > 0 ? att[0]! : ctt.length > 0 ? ctt[0]! : undefined; + if (attestation && attestation.attestation) { + receipt = { ...receipt, state: TransferState.Attested, attestation: attestation }; + } - const receipt: TransferReceipt = { - protocol: xfer.transfer.automatic ? "AutomaticCircleBridge" : "CircleBridge", - from: from.chain, - to: to.chain, - state: TransferState.Created, - originTxs: xfer.txids.filter((txid) => txid.chain === xfer.transfer.from.chain), - destinationTxs: xfer.txids.filter((txid) => txid.chain === xfer.transfer.to.chain), - request: xfer.transfer, - attestation, - }; - - if (receipt.originTxs.length > 0) receipt.state = TransferState.SourceInitiated; - if (receipt.attestation && receipt.attestation.attestation) - receipt.state = TransferState.Attested; - if (receipt.destinationTxs.length > 0) receipt.state = TransferState.DestinationInitiated; + const destinationTxs = xfer.txids.filter((txid) => txid.chain === xfer.transfer.to.chain); + if (destinationTxs.length > 0) { + receipt = { ...receipt, state: TransferState.DestinationInitiated, destinationTxs }; + } - return receipt; + return receipt as TransferReceipt; } // AsyncGenerator fn that produces status updates through an async generator @@ -522,83 +528,83 @@ export class CircleTransfer _fromChain = _fromChain ?? wh.getChain(receipt.from); _toChain = _toChain ?? wh.getChain(receipt.to); + type R = TransferReceipt< + typeof receipt.protocol, + typeof receipt.from, + typeof receipt.to, + TS + >; + // Check the source chain for initiation transaction // and capture the message id if (receipt.state === TransferState.SourceInitiated) { - if (receipt.originTxs.length === 0) + const _receipt = receipt as R; + if (_receipt.originTxs.length === 0) throw "Invalid state transition: no originating transactions"; - if (!receipt.attestation || !receipt.attestation.id) { - const initTx = receipt.originTxs[receipt.originTxs.length - 1]!; - const xfermsg = await CircleTransfer.getTransferMessage(_fromChain, initTx.txid); - receipt.attestation = { id: xfermsg }; - receipt.state = TransferState.SourceFinalized; - yield receipt; - } + const initTx = _receipt.originTxs[_receipt.originTxs.length - 1]!; + const xfermsg = await CircleTransfer.getTransferMessage(_fromChain, initTx.txid); + receipt = { ..._receipt, attestation: { id: xfermsg }, state: TransferState.SourceFinalized }; + yield receipt; } if (receipt.state == TransferState.SourceFinalized) { - if (!receipt.attestation) throw "Invalid state transition: no attestation id"; + const _receipt = receipt as R; + if (!_receipt.attestation) throw "Invalid state transition: no attestation id"; - if (receipt.protocol === "AutomaticCircleBridge") { + if (_receipt.protocol === "AutomaticCircleBridge") { // we need to get the attestation so we can deliver it // we can use the message id we parsed out of the logs, if we have them // or try to fetch it from the last origin transaction - let vaa = receipt.attestation.attestation ? receipt.attestation.attestation : undefined; + let vaa = _receipt.attestation.attestation ? _receipt.attestation.attestation : undefined; if (!vaa) { vaa = await CircleTransfer.getTransferVaa( wh, - receipt.attestation.id as WormholeMessageId, + _receipt.attestation.id as WormholeMessageId, leftover(start, timeout), ); - receipt.attestation.attestation = vaa; - receipt.state = TransferState.Attested; + receipt = { + ..._receipt, + attestation: { id: _receipt.attestation.id, attestation: vaa }, + state: TransferState.Attested, + }; yield receipt; } } } if (receipt.state == TransferState.Attested) { - if (!receipt.attestation) throw "Invalid state transition"; + const _receipt = receipt as R; + if (!_receipt.attestation) throw "Invalid state transition"; // First try to grab the tx status from the API // Note: this requires a subsequent async step on the backend // to have the dest txid populated, so it may be delayed by some time const txStatus = await wh.getTransactionStatus( - receipt.attestation.id as WormholeMessageId, + _receipt.attestation.id as WormholeMessageId, leftover(start, timeout), ); - if (!txStatus) { - yield receipt; - return; - } - if (txStatus.globalTx?.destinationTx?.txHash) { + if (txStatus && txStatus.globalTx?.destinationTx?.txHash) { const { chainId, txHash } = txStatus.globalTx.destinationTx; - - receipt.destinationTxs = [ - { - chain: toChain(chainId), - txid: txHash, - }, - ]; - - receipt.state = TransferState.DestinationFinalized; + receipt = { + ...receipt, + destinationTxs: [{ chain: toChain(chainId), txid: txHash }], + state: TransferState.DestinationFinalized, + }; yield receipt; } // Fall back to asking the destination chain if this VAA has been redeemed // assuming we have the full attestation if ( - receipt.attestation.attestation && - (await CircleTransfer.isTransferComplete(_toChain, receipt.attestation.attestation), + _receipt.attestation.attestation && + (await CircleTransfer.isTransferComplete(_toChain, _receipt.attestation.attestation), leftover(start, timeout)) ) { - receipt.state = TransferState.DestinationFinalized; + receipt = { ...receipt, state: TransferState.DestinationFinalized }; yield receipt; } } - yield receipt; - return; } } diff --git a/connect/src/protocols/tokenTransfer.ts b/connect/src/protocols/tokenTransfer.ts index 9a6f1219e7..96085f6a32 100644 --- a/connect/src/protocols/tokenTransfer.ts +++ b/connect/src/protocols/tokenTransfer.ts @@ -37,6 +37,7 @@ import { TransferReceipt, TransferState, WormholeTransfer, + hasReachedState, } from "../wormholeTransfer"; export type TokenTransferProtocol = "TokenBridge" | "AutomaticTokenBridge"; @@ -522,30 +523,45 @@ export class TokenTransfer ): TransferReceipt { const { transfer } = xfer; - const att = - xfer.attestations && xfer.attestations.length > 0 ? xfer.attestations![0]! : undefined; - const attestation = - att && att.id.emitter ? { id: att.id, attestation: att.attestation } : undefined; + const protocol = transfer.automatic ? "AutomaticTokenBridge" : "TokenBridge"; + const from = transfer.from.chain; + const to = transfer.to.chain; - const receipt = { - protocol: (transfer.automatic - ? "AutomaticTokenBridge" - : "TokenBridge") as TokenTransferProtocol, + let receipt: Partial> = { + protocol, request: transfer, - from: transfer.from.chain, - to: transfer.to.chain, + from: from, + to: to, state: TransferState.Created, - originTxs: xfer.txids.filter((txid) => txid.chain === transfer.from.chain), - destinationTxs: xfer.txids.filter((txid) => txid.chain === transfer.to.chain), - attestation, }; - if (receipt.originTxs.length > 0) receipt.state = TransferState.SourceInitiated; - if (receipt.attestation && receipt.attestation.attestation) - receipt.state = TransferState.Attested; - if (receipt.destinationTxs.length > 0) receipt.state = TransferState.DestinationInitiated; + const originTxs = xfer.txids.filter((txid) => txid.chain === transfer.from.chain); + if (originTxs.length > 0) { + receipt = { ...receipt, state: TransferState.SourceInitiated, originTxs: originTxs }; + } + + const att = + xfer.attestations && xfer.attestations.length > 0 ? xfer.attestations![0]! : undefined; + const attestation = + att && att.id.emitter ? { id: att.id, attestation: att.attestation } : undefined; + if (attestation && attestation.attestation) { + receipt = { + ...receipt, + state: TransferState.Attested, + attestation: attestation, + }; + } - return receipt; + const destinationTxs = xfer.txids.filter((txid) => txid.chain === transfer.to.chain); + if (destinationTxs.length > 0) { + receipt = { + ...receipt, + state: TransferState.DestinationInitiated, + destinationTxs: destinationTxs, + }; + } + + return receipt as TransferReceipt; } // AsyncGenerator fn that produces status updates through an async generator @@ -566,88 +582,92 @@ export class TokenTransfer _fromChain = _fromChain ?? wh.getChain(receipt.from); _toChain = _toChain ?? wh.getChain(receipt.to); - // Check the source chain for initiation transaction - // and capture the message id - if (receipt.state === TransferState.SourceInitiated) { - if (receipt.originTxs.length === 0) - throw "Invalid state transition: no originating transactions"; - - if (!receipt.attestation || !receipt.attestation.id) { - const initTx = receipt.originTxs[receipt.originTxs.length - 1]!; - const xfermsg = await TokenTransfer.getTransferMessage( - _fromChain, - initTx.txid, - leftover(start, timeout), - ); - receipt.attestation = { id: xfermsg }; - receipt.state = TransferState.SourceFinalized; - yield receipt; - } - } - - if (receipt.state == TransferState.SourceFinalized) { - if (!receipt.attestation) throw "Invalid state transition: no attestation id"; - - // we need to get the attestation so we can deliver it - // we can use the message id we parsed out of the logs, if we have them - // or try to fetch it from the last origin transaction - let vaa = receipt.attestation.attestation ? receipt.attestation.attestation : undefined; - if (!vaa) { - vaa = await TokenTransfer.getTransferVaa( - wh, - { ...receipt.attestation.id }, - leftover(start, timeout), - ); - receipt.attestation.attestation = vaa; - receipt.state = TransferState.Attested; - yield receipt; - } - } - - if (receipt.state == TransferState.Attested) { - if (!receipt.attestation) throw "Invalid state transition"; - - // First try to grab the tx status from the API - // Note: this requires a subsequent async step on the backend - // to have the dest txid populated, so it may be delayed by some time - const txStatus = await wh.getTransactionStatus( - receipt.attestation.id!, + type R = TransferReceipt< + typeof receipt.protocol, + typeof receipt.from, + typeof receipt.to, + TS + >; + + const fetchMessageId = async (_receipt: R) => { + if (_receipt.originTxs.length === 0) throw "Origin transactions required to fetch message id"; + const { txid } = _receipt.originTxs[_receipt.originTxs.length - 1]!; + const msg = await TokenTransfer.getTransferMessage( + _fromChain, + txid, leftover(start, timeout), ); - if (!txStatus) { - yield receipt; - return; - } - - if (txStatus.globalTx?.destinationTx?.txHash) { - const { chainId, txHash } = txStatus.globalTx.destinationTx; + return { ..._receipt, state: TransferState.SourceFinalized, attestation: { id: msg } }; + }; - receipt.destinationTxs = [ - { - chain: toChain(chainId), - txid: txHash, - }, - ]; + const fetchAttestation = async (_receipt: R) => { + if (!_receipt.attestation.id) throw "Attestation id required to fetch attestation"; + const { id } = _receipt.attestation; + const attestation = await TokenTransfer.getTransferVaa(wh, id, leftover(start, timeout)); + return { ..._receipt, attestation: { id, attestation }, state: TransferState.Attested }; + }; - receipt.state = TransferState.DestinationFinalized; - yield receipt; + const fetchRedeemTransactionStatus = async ( + _receipt: R, + ) => { + if (!_receipt.attestation.id) throw "Attestation id required to fetch redeem tx"; + const { id } = _receipt.attestation; + const txStatus = await wh.getTransactionStatus(id, leftover(start, timeout)); + if (txStatus && txStatus.globalTx?.destinationTx?.txHash) { + const { chainId, txHash } = txStatus.globalTx.destinationTx; + const destinationTxs = [{ chain: toChain(chainId), txid: txHash }]; + return { ..._receipt, destinationTxs, state: TransferState.DestinationFinalized }; } + return _receipt; + }; - // Fall back to asking the destination chain if this VAA has been redeemed - // assuming we have the full attestation - if ( - receipt.attestation.attestation && - (await TokenTransfer.isTransferComplete( + const fetchVaaRedeemed = async (_receipt: R) => { + if (!_receipt.attestation.attestation) + throw "Signed Attestation required to check for redeem"; + return { + ..._receipt, + state: (await TokenTransfer.isTransferComplete( _toChain, - receipt.attestation.attestation as TokenTransferVAA, - ), - leftover(start, timeout)) - ) { - receipt.state = TransferState.DestinationFinalized; - yield receipt; - } + _receipt.attestation.attestation as TokenTransferVAA, + )) + ? TransferState.DestinationFinalized + : TransferState.Attested, + }; + }; + + // Check the source chain for initiation transaction + // and capture the message id + if (hasReachedState(receipt, TransferState.SourceInitiated)) { + receipt = await fetchMessageId(receipt); + yield receipt; + } + + // If the source is finalized, we need to fetch the signed attestation + // so that we may deliver it to the destination chain + // or at least track the transfer through its progress + if (hasReachedState(receipt, TransferState.SourceFinalized)) { + receipt = await fetchAttestation(receipt); + yield receipt; } + + // First try to grab the tx status from the API + // Note: this requires a subsequent async step on the backend + // to have the dest txid populated, so it may be delayed by some time + if ( + hasReachedState(receipt, TransferState.Attested) || + hasReachedState(receipt, TransferState.SourceFinalized) + ) { + receipt = await fetchRedeemTransactionStatus(receipt); + yield receipt; + } + + // Fall back to asking the destination chain if this VAA has been redeemed + // Note: We do not get any destinationTxs with this method + if (hasReachedState(receipt, TransferState.Attested)) { + receipt = await fetchVaaRedeemed(receipt); + yield receipt; + } + yield receipt; - return; } } diff --git a/connect/src/wormholeTransfer.ts b/connect/src/wormholeTransfer.ts index b3f2a77ee9..60e954e8f9 100644 --- a/connect/src/wormholeTransfer.ts +++ b/connect/src/wormholeTransfer.ts @@ -32,7 +32,7 @@ export type TransferRequest = PN extends // Transfer state machine states export enum TransferState { Failed = -1, - Created = 1, // Will be set after the TokenTransfer object is created + Created = 0, // Will be set after the TokenTransfer object is created SourceInitiated, // Will be set after source chain transactions are submitted SourceFinalized, // Will be set after source chain transactions are finalized Attested, // Will be set after VAA or Circle Attestation is available @@ -40,20 +40,65 @@ export enum TransferState { DestinationFinalized, // Will be set after the transaction is finalized on the destination chain } +// Base type for common properties +interface BaseTransferReceipt { + protocol: PN; + from: SC; + to: DC; + state: TransferState; + request: TransferRequest; +} + +export interface SourceInitiatedTransferReceipt< + PN extends ProtocolName, + SC extends Chain, + DC extends Chain, +> extends BaseTransferReceipt { + originTxs: TransactionId[]; +} +export interface SourceFinalizedTransferReceipt< + PN extends ProtocolName, + SC extends Chain, + DC extends Chain, +> extends SourceInitiatedTransferReceipt { + attestation: AttestationReceipt; +} +export interface AttestedTransferReceipt< + PN extends ProtocolName, + SC extends Chain, + DC extends Chain, +> extends SourceFinalizedTransferReceipt { + attestation: Required>; +} +export interface CompletedTransferReceipt< + PN extends ProtocolName, + SC extends Chain, + DC extends Chain, +> extends AttestedTransferReceipt { + destinationTxs: TransactionId[]; +} + +export function hasReachedState( + receipt: TransferReceipt, + state: TS, +): receipt is TransferReceipt { + return receipt.state === state; +} + export type TransferReceipt< PN extends ProtocolName, SC extends Chain = Chain, DC extends Chain = Chain, -> = { - readonly protocol: PN; - readonly request: TransferRequest; - readonly from: SC; - readonly to: DC; - state: TransferState; - originTxs: TransactionId[]; - destinationTxs: TransactionId[]; - attestation?: AttestationReceipt; -}; + TS extends TransferState = TransferState, +> = TS extends TransferState.DestinationInitiated + ? CompletedTransferReceipt + : TS extends TransferState.Attested + ? AttestedTransferReceipt + : TS extends TransferState.SourceFinalized + ? SourceFinalizedTransferReceipt + : TS extends TransferState.SourceInitiated + ? SourceInitiatedTransferReceipt + : never; // Quote with optional relayer fees if the transfer // is requested to be automatic