From 884916060f8e2487a7875bbe290942b03e2e5e2d Mon Sep 17 00:00:00 2001 From: Artur Sapek Date: Thu, 4 Jan 2024 10:30:13 -0500 Subject: [PATCH] Use union types for TransferReceipt (#197) --- connect/src/protocols/cctpTransfer.ts | 89 ++++++++++++++++-------- connect/src/protocols/tokenTransfer.ts | 94 +++++++++++++++++--------- connect/src/wormholeTransfer.ts | 79 ++++++++++++++-------- 3 files changed, 173 insertions(+), 89 deletions(-) diff --git a/connect/src/protocols/cctpTransfer.ts b/connect/src/protocols/cctpTransfer.ts index 764a49af4..825bb8053 100644 --- a/connect/src/protocols/cctpTransfer.ts +++ b/connect/src/protocols/cctpTransfer.ts @@ -31,11 +31,17 @@ import { signSendWait } from "../common"; import { DEFAULT_TASK_TIMEOUT } from "../config"; import { Wormhole } from "../wormhole"; import { + AttestedTransferReceipt, + CompletedTransferReceipt, + SourceFinalizedTransferReceipt, + SourceInitiatedTransferReceipt, TransferQuote, TransferReceipt, TransferState, WormholeTransfer, - hasReachedState, + isAttested, + isSourceFinalized, + isSourceInitiated, } from "../wormholeTransfer"; type CircleTransferProtocol = "CircleBridge" | "AutomaticCircleBridge"; @@ -473,7 +479,7 @@ export class CircleTransfer // This attestation may be either the auto relay vaa or the circle attestation // depending on the request - let receipt: Partial> = { + let receipt: TransferReceipt = { protocol: xfer.transfer.automatic ? "AutomaticCircleBridge" : "CircleBridge", request: xfer.transfer, from: from.chain, @@ -483,22 +489,44 @@ export class CircleTransfer const originTxs = xfer.txids.filter((txid) => txid.chain === xfer.transfer.from.chain); if (originTxs.length > 0) { - receipt = { ...receipt, state: TransferState.SourceInitiated, originTxs }; + receipt = { + ...receipt, + state: TransferState.SourceInitiated, + originTxs, + } satisfies SourceInitiatedTransferReceipt; } const att = xfer.attestations?.filter((a) => isWormholeMessageId(a.id)) ?? []; const ctt = xfer.attestations?.filter((a) => isCircleMessageId(a.id)) ?? []; const attestation = att.length > 0 ? att[0]! : ctt.length > 0 ? ctt[0]! : undefined; - if (attestation && attestation.attestation) { - receipt = { ...receipt, state: TransferState.Attested, attestation: attestation }; + if (attestation) { + if (attestation.id) { + receipt = { + ...(receipt as SourceInitiatedTransferReceipt), + state: TransferState.SourceFinalized, + attestation: attestation, + } satisfies SourceFinalizedTransferReceipt; + + if (attestation.attestation) { + receipt = { + ...receipt, + state: TransferState.Attested, + attestation: { id: attestation.id, attestation: attestation.attestation }, + } satisfies AttestedTransferReceipt; + } + } } const destinationTxs = xfer.txids.filter((txid) => txid.chain === xfer.transfer.to.chain); if (destinationTxs.length > 0) { - receipt = { ...receipt, state: TransferState.DestinationInitiated, destinationTxs }; + receipt = { + ...(receipt as AttestedTransferReceipt), + state: TransferState.DestinationInitiated, + destinationTxs, + } satisfies CompletedTransferReceipt; } - return receipt as TransferReceipt; + return receipt; } // AsyncGenerator fn that produces status updates through an async generator @@ -521,17 +549,21 @@ export class CircleTransfer // Check the source chain for initiation transaction // and capture the message id - if (hasReachedState(receipt, TransferState.SourceInitiated)) { + if (isSourceInitiated(receipt)) { if (receipt.originTxs.length === 0) throw "Invalid state transition: no originating transactions"; const initTx = receipt.originTxs[receipt.originTxs.length - 1]!; const xfermsg = await CircleTransfer.getTransferMessage(_fromChain, initTx.txid); - receipt = { ...receipt, attestation: { id: xfermsg }, state: TransferState.SourceFinalized }; + receipt = { + ...receipt, + attestation: { id: xfermsg }, + state: TransferState.SourceFinalized, + } satisfies SourceFinalizedTransferReceipt; yield receipt; } - if (hasReachedState(receipt, TransferState.SourceFinalized)) { + if (isSourceFinalized(receipt)) { if (!receipt.attestation) throw "Invalid state transition: no attestation id"; if (receipt.protocol === "AutomaticCircleBridge") { @@ -549,18 +581,17 @@ export class CircleTransfer ...receipt, attestation: { id: receipt.attestation.id, attestation: vaa }, state: TransferState.Attested, - }; + } satisfies AttestedTransferReceipt; yield receipt; } } } - if (hasReachedState(receipt, TransferState.Attested)) { + // 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 (isAttested(receipt) || isSourceFinalized(receipt)) { 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, leftover(start, timeout), @@ -572,24 +603,26 @@ export class CircleTransfer ...receipt, destinationTxs: [{ chain: toChain(chainId) as DC, txid: txHash }], state: TransferState.DestinationFinalized, - }; + } satisfies CompletedTransferReceipt; yield receipt; } + } - // Fall back to asking the destination chain if this VAA has been redeemed - // assuming we have the full attestation - if (hasReachedState(receipt, TransferState.Attested)) { + // Fall back to asking the destination chain if this VAA has been redeemed + // assuming we have the full attestation + if (isAttested(receipt)) { + const isComplete = await CircleTransfer.isTransferComplete( + _toChain, + receipt.attestation.attestation, + ); + if (isComplete) { receipt = { ...receipt, - state: (await CircleTransfer.isTransferComplete( - _toChain, - receipt.attestation.attestation, - )) - ? TransferState.DestinationFinalized - : TransferState.Attested, - }; - yield receipt; + state: TransferState.DestinationFinalized, + destinationTxs: [], + } as CompletedTransferReceipt; } + yield receipt; } } } diff --git a/connect/src/protocols/tokenTransfer.ts b/connect/src/protocols/tokenTransfer.ts index aa386e5ba..355638c87 100644 --- a/connect/src/protocols/tokenTransfer.ts +++ b/connect/src/protocols/tokenTransfer.ts @@ -33,11 +33,17 @@ import { signSendWait } from "../common"; import { DEFAULT_TASK_TIMEOUT } from "../config"; import { Wormhole } from "../wormhole"; import { + AttestedTransferReceipt, + CompletedTransferReceipt, + SourceFinalizedTransferReceipt, + SourceInitiatedTransferReceipt, TransferQuote, TransferReceipt, TransferState, WormholeTransfer, - hasReachedState, + isAttested, + isSourceFinalized, + isSourceInitiated, } from "../wormholeTransfer"; export type TokenTransferProtocol = "TokenBridge" | "AutomaticTokenBridge"; @@ -527,7 +533,7 @@ export class TokenTransfer const from = transfer.from.chain; const to = transfer.to.chain; - let receipt: Partial> = { + let receipt: TransferReceipt = { protocol, request: transfer, from: from, @@ -537,31 +543,44 @@ export class TokenTransfer const originTxs = xfer.txids.filter((txid) => txid.chain === transfer.from.chain); if (originTxs.length > 0) { - receipt = { ...receipt, state: TransferState.SourceInitiated, originTxs: originTxs }; + receipt = { + ...receipt, + state: TransferState.SourceInitiated, + originTxs: originTxs, + } satisfies SourceInitiatedTransferReceipt; } 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 attestation = att && att.id ? { id: att.id, attestation: att.attestation } : undefined; + if (attestation) { + if (attestation.id) { + receipt = { + ...(receipt as SourceInitiatedTransferReceipt), + state: TransferState.SourceFinalized, + attestation: { id: attestation.id }, + } satisfies SourceFinalizedTransferReceipt; + + if (attestation.attestation) { + receipt = { + ...receipt, + state: TransferState.Attested, + attestation: { id: attestation.id, attestation: attestation.attestation }, + } satisfies AttestedTransferReceipt; + } + } } const destinationTxs = xfer.txids.filter((txid) => txid.chain === transfer.to.chain); if (destinationTxs.length > 0) { receipt = { - ...receipt, + ...(receipt as AttestedTransferReceipt), state: TransferState.DestinationInitiated, destinationTxs: destinationTxs, - }; + } satisfies CompletedTransferReceipt; } - return receipt as TransferReceipt; + return receipt; } // AsyncGenerator fn that produces status updates through an async generator @@ -584,7 +603,7 @@ export class TokenTransfer // Check the source chain for initiation transaction // and capture the message id - if (hasReachedState(receipt, TransferState.SourceInitiated)) { + if (isSourceInitiated(receipt)) { 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( @@ -592,28 +611,33 @@ export class TokenTransfer txid, leftover(start, timeout), ); - receipt = { ...receipt, state: TransferState.SourceFinalized, attestation: { id: msg } }; + receipt = { + ...receipt, + state: TransferState.SourceFinalized, + attestation: { id: msg }, + } satisfies SourceFinalizedTransferReceipt; 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 (isSourceFinalized(receipt)) { 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 }; + receipt = { + ...receipt, + attestation: { id, attestation }, + state: TransferState.Attested, + } satisfies AttestedTransferReceipt; 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) - ) { + if (isAttested(receipt) || isSourceFinalized(receipt)) { 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)); @@ -623,24 +647,28 @@ export class TokenTransfer ...receipt, destinationTxs: [{ chain: toChain(chainId) as DC, txid: txHash }], state: TransferState.DestinationFinalized, - }; + } satisfies CompletedTransferReceipt; } 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)) { + if (isAttested(receipt)) { if (!receipt.attestation.attestation) throw "Signed Attestation required to check for redeem"; - receipt = { - ...receipt, - state: (await TokenTransfer.isTransferComplete( - _toChain, - receipt.attestation.attestation as TokenTransferVAA, - )) - ? TransferState.DestinationFinalized - : TransferState.Attested, - }; + + let isComplete = await TokenTransfer.isTransferComplete( + _toChain, + receipt.attestation.attestation as TokenTransferVAA, + ); + + if (isComplete) { + receipt = { + ...receipt, + state: TransferState.DestinationFinalized, + } satisfies CompletedTransferReceipt; + } + yield receipt; } diff --git a/connect/src/wormholeTransfer.ts b/connect/src/wormholeTransfer.ts index 60e954e8f..784f08889 100644 --- a/connect/src/wormholeTransfer.ts +++ b/connect/src/wormholeTransfer.ts @@ -45,60 +45,83 @@ interface BaseTransferReceipt; + state: TransferState; +} + +export interface CreatedTransferReceipt< + PN extends ProtocolName, + SC extends Chain = Chain, + DC extends Chain = Chain, +> extends BaseTransferReceipt { + state: TransferState.Created; } export interface SourceInitiatedTransferReceipt< PN extends ProtocolName, - SC extends Chain, - DC extends Chain, + SC extends Chain = Chain, + DC extends Chain = Chain, > extends BaseTransferReceipt { + state: TransferState.SourceInitiated; originTxs: TransactionId[]; } export interface SourceFinalizedTransferReceipt< PN extends ProtocolName, - SC extends Chain, - DC extends Chain, -> extends SourceInitiatedTransferReceipt { + SC extends Chain = Chain, + DC extends Chain = Chain, +> extends BaseTransferReceipt { + state: TransferState.SourceFinalized; + originTxs: TransactionId[]; attestation: AttestationReceipt; } export interface AttestedTransferReceipt< PN extends ProtocolName, - SC extends Chain, - DC extends Chain, -> extends SourceFinalizedTransferReceipt { + SC extends Chain = Chain, + DC extends Chain = Chain, +> extends BaseTransferReceipt { + state: TransferState.Attested; + originTxs: TransactionId[]; attestation: Required>; } export interface CompletedTransferReceipt< PN extends ProtocolName, - SC extends Chain, - DC extends Chain, -> extends AttestedTransferReceipt { - destinationTxs: TransactionId[]; + SC extends Chain = Chain, + DC extends Chain = Chain, +> extends BaseTransferReceipt { + state: TransferState.DestinationInitiated | TransferState.DestinationFinalized; + originTxs: TransactionId[]; + attestation: AttestationReceipt; + destinationTxs?: TransactionId[]; +} + +export function isAttested( + receipt: TransferReceipt, +): receipt is AttestedTransferReceipt { + return receipt.state === TransferState.Attested; } -export function hasReachedState( - receipt: TransferReceipt, - state: TS, -): receipt is TransferReceipt { - return receipt.state === state; +export function isSourceInitiated( + receipt: TransferReceipt, +): receipt is SourceInitiatedTransferReceipt { + return receipt.state === TransferState.SourceInitiated; +} + +export function isSourceFinalized( + receipt: TransferReceipt, +): receipt is SourceFinalizedTransferReceipt { + return receipt.state === TransferState.SourceFinalized; } export type TransferReceipt< PN extends ProtocolName, SC extends Chain = Chain, DC extends Chain = Chain, - 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; +> = + | CreatedTransferReceipt + | SourceInitiatedTransferReceipt + | SourceFinalizedTransferReceipt + | AttestedTransferReceipt + | CompletedTransferReceipt; // Quote with optional relayer fees if the transfer // is requested to be automatic