diff --git a/connect/src/protocols/cctpTransfer.ts b/connect/src/protocols/cctpTransfer.ts index d0f335f9c..c203190ee 100644 --- a/connect/src/protocols/cctpTransfer.ts +++ b/connect/src/protocols/cctpTransfer.ts @@ -35,6 +35,7 @@ import { TransferReceipt, TransferState, WormholeTransfer, + hasReachedState, } from "../wormholeTransfer"; type CircleTransferProtocol = "CircleBridge" | "AutomaticCircleBridge"; @@ -473,6 +474,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,40 +498,30 @@ 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 // eventually producing a receipt // can be called repeatedly so the receipt is updated as it moves through the // steps of the transfer - static async *track( + static async *track( wh: Wormhole, - receipt: TransferReceipt, + receipt: TransferReceipt, timeout: number = DEFAULT_TASK_TIMEOUT, // Optional parameters to override chain context (typically for custom rpc) - _fromChain?: ChainContext, typeof receipt.from>, - _toChain?: ChainContext, typeof receipt.to>, + _fromChain?: ChainContext, SC>, + _toChain?: ChainContext, DC>, ) { const start = Date.now(); const leftover = (start: number, max: number) => Math.max(max - (Date.now() - start), 0); @@ -524,20 +531,17 @@ export class CircleTransfer // Check the source chain for initiation transaction // and capture the message id - if (receipt.state === TransferState.SourceInitiated) { + if (hasReachedState(receipt, 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 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 (hasReachedState(receipt, TransferState.SourceFinalized)) { if (!receipt.attestation) throw "Invalid state transition: no attestation id"; if (receipt.protocol === "AutomaticCircleBridge") { @@ -551,14 +555,17 @@ export class CircleTransfer 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 (hasReachedState(receipt, TransferState.Attested)) { if (!receipt.attestation) throw "Invalid state transition"; // First try to grab the tx status from the API @@ -568,37 +575,31 @@ export class CircleTransfer 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) as DC, 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), - leftover(start, timeout)) - ) { - receipt.state = TransferState.DestinationFinalized; + if (hasReachedState(receipt, TransferState.Attested)) { + receipt = { + ...receipt, + state: (await CircleTransfer.isTransferComplete( + _toChain, + receipt.attestation.attestation, + )) + ? TransferState.DestinationFinalized + : TransferState.Attested, + }; yield receipt; } } - yield receipt; - return; } } diff --git a/connect/src/protocols/tokenTransfer.ts b/connect/src/protocols/tokenTransfer.ts index 9a6f1219e..aa386e5ba 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,43 +523,58 @@ 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, + }; + } + + const destinationTxs = xfer.txids.filter((txid) => txid.chain === transfer.to.chain); + if (destinationTxs.length > 0) { + receipt = { + ...receipt, + state: TransferState.DestinationInitiated, + destinationTxs: destinationTxs, + }; + } - return receipt; + return receipt as TransferReceipt; } // AsyncGenerator fn that produces status updates through an async generator // eventually producing a receipt // can be called repeatedly so the receipt is updated as it moves through the // steps of the transfer - static async *track( + static async *track( wh: Wormhole, - receipt: TransferReceipt, + receipt: TransferReceipt, timeout: number = DEFAULT_TASK_TIMEOUT, // Optional parameters to override chain context (typically for custom rpc) - _fromChain?: ChainContext, typeof receipt.from>, - _toChain?: ChainContext, typeof receipt.to>, + _fromChain?: ChainContext, SC>, + _toChain?: ChainContext, DC>, ) { const start = Date.now(); const leftover = (start: number, max: number) => Math.max(max - (Date.now() - start), 0); @@ -568,86 +584,66 @@ export class TokenTransfer // 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 (hasReachedState(receipt, TransferState.SourceInitiated)) { + 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), + ); + receipt = { ...receipt, state: TransferState.SourceFinalized, attestation: { id: msg } }; + 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 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)) { + 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)); + receipt = { ...receipt, attestation: { id, attestation }, 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!, - leftover(start, timeout), - ); - if (!txStatus) { - yield receipt; - return; - } - - if (txStatus.globalTx?.destinationTx?.txHash) { + // 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) + ) { + 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; - - receipt.destinationTxs = [ - { - chain: toChain(chainId), - txid: txHash, - }, - ]; - - receipt.state = TransferState.DestinationFinalized; - yield receipt; + receipt = { + ...receipt, + destinationTxs: [{ chain: toChain(chainId) as DC, 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 TokenTransfer.isTransferComplete( + // 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)) { + if (!receipt.attestation.attestation) throw "Signed Attestation required to check for redeem"; + receipt = { + ...receipt, + state: (await TokenTransfer.isTransferComplete( _toChain, receipt.attestation.attestation as TokenTransferVAA, - ), - leftover(start, timeout)) - ) { - receipt.state = TransferState.DestinationFinalized; - yield receipt; - } + )) + ? TransferState.DestinationFinalized + : TransferState.Attested, + }; + yield receipt; } + yield receipt; - return; } } diff --git a/connect/src/wormholeTransfer.ts b/connect/src/wormholeTransfer.ts index b3f2a77ee..60e954e8f 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