From 4698ab1798ce8ab3924ad71f511d10f68e4a3ddf Mon Sep 17 00:00:00 2001 From: Ben Guidarelli Date: Thu, 28 Dec 2023 10:29:22 -0500 Subject: [PATCH 1/3] yield the receipt from the tracker generator --- connect/src/protocols/tokenTransfer.ts | 38 ++++++++++++++------------ connect/src/wormholeTransfer.ts | 9 ++++-- examples/src/helpers/helpers.ts | 19 +++++++++---- examples/src/tokenBridge.ts | 2 +- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/connect/src/protocols/tokenTransfer.ts b/connect/src/protocols/tokenTransfer.ts index eea8537a6..3c212b284 100644 --- a/connect/src/protocols/tokenTransfer.ts +++ b/connect/src/protocols/tokenTransfer.ts @@ -38,11 +38,11 @@ import { WormholeTransfer, } from "../wormholeTransfer"; -type TransferProtocol = "TokenBridge" | "AutomaticTokenBridge"; -type TransferVAA = TokenBridge.TransferVAA | AutomaticTokenBridge.VAA; +type TokenTransferProtocol = "TokenBridge" | "AutomaticTokenBridge"; +type TokenTransferVAA = TokenBridge.TransferVAA | AutomaticTokenBridge.VAA; export class TokenTransfer - implements WormholeTransfer + implements WormholeTransfer { private readonly wh: Wormhole; @@ -59,7 +59,7 @@ export class TokenTransfer // on the source chain (if its been completed and finalized) vaas?: { id: WormholeMessageId; - vaa?: TransferVAA; + vaa?: TokenTransferVAA; }[]; private constructor(wh: Wormhole, transfer: TokenTransferDetails) { @@ -257,15 +257,13 @@ export class TokenTransfer return redeemTxids.map(({ txid }) => txid); } - // Async fn that produces status updates through an async generator + // 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 - // Note: it should be possible to call this fn as many times - // as it takes until the destination is finalized 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>, @@ -292,7 +290,7 @@ export class TokenTransfer ); receipt.attestation = { id: xfermsg }; receipt.state = TransferState.SourceFinalized; - yield receipt.state; + yield receipt; } } @@ -311,7 +309,7 @@ export class TokenTransfer ); receipt.attestation.attestation = vaa; receipt.state = TransferState.Attested; - yield receipt.state; + yield receipt; } } @@ -325,7 +323,10 @@ export class TokenTransfer receipt.attestation.id!, leftover(start, timeout), ); - if (!txStatus) return receipt; + if (!txStatus) { + yield receipt; + return; + } if (txStatus.globalTx?.destinationTx?.txHash) { const { chainId, txHash } = txStatus.globalTx.destinationTx; @@ -338,7 +339,7 @@ export class TokenTransfer ]; receipt.state = TransferState.DestinationFinalized; - yield receipt.state; + yield receipt; } // Fall back to asking the destination chain if this VAA has been redeemed @@ -347,15 +348,16 @@ export class TokenTransfer receipt.attestation.attestation && (await TokenTransfer.isTransferComplete( _toChain, - receipt.attestation.attestation as TransferVAA, + receipt.attestation.attestation as TokenTransferVAA, ), leftover(start, timeout)) ) { receipt.state = TransferState.DestinationFinalized; - yield receipt.state; + yield receipt; } } - return receipt; + yield receipt; + return; } // Static method to perform the transfer so a custom RPC may be used @@ -387,7 +389,7 @@ export class TokenTransfer // Static method to allow passing a custom RPC static async redeem( toChain: ChainContext, - vaa: TokenBridge.TransferVAA | AutomaticTokenBridge.VAA, + vaa: TokenTransferVAA, signer: Signer, ): Promise { const signerAddress = toNative(signer.chain(), signer.address()); @@ -404,7 +406,7 @@ export class TokenTransfer N extends Network, P extends Platform, C extends PlatformToChains

, - >(toChain: ChainContext, vaa: TransferVAA): Promise { + >(toChain: ChainContext, vaa: TokenTransferVAA): Promise { // TODO: converter? if (vaa.protocolName === "AutomaticTokenBridge") vaa = deserialize("TokenBridge:TransferWithPayload", serialize(vaa)); @@ -428,7 +430,7 @@ export class TokenTransfer wh: Wormhole, key: WormholeMessageId | TxHash, timeout?: number, - ): Promise { + ): Promise { const vaa = typeof key === "string" ? await wh.getVaaByTxHash(key, TokenBridge.getTransferDiscriminator(), timeout) diff --git a/connect/src/wormholeTransfer.ts b/connect/src/wormholeTransfer.ts index 5f32af1ec..06ad3b1d9 100644 --- a/connect/src/wormholeTransfer.ts +++ b/connect/src/wormholeTransfer.ts @@ -86,20 +86,23 @@ export type TransferQuote = { }; // Static methods on the Transfer protocol types +// e.g. `TokenTransfer.constructor` export interface TransferProtocol { isTransferComplete>( toChain: ChainContext, vaa: VAA, ): Promise; - isAutomatic(wh: Wormhole, vaa: VAA): boolean; - //validateTransfer(wh: Wormhole, transfer: ) + validateTransferDetails( + wh: Wormhole, + transfer: TransferRequest, + ): Promise; quoteTransfer(xfer: WormholeTransfer): Promise; getReceipt(xfer: WormholeTransfer): TransferReceipt; track( wh: Wormhole, xfer: WormholeTransfer, timeout: number, - ): AsyncGenerator, unknown>; + ): AsyncGenerator, unknown, unknown>; } // WormholeTransfer abstracts the process and state transitions diff --git a/examples/src/helpers/helpers.ts b/examples/src/helpers/helpers.ts index 0de3ed6fe..79fb47d15 100644 --- a/examples/src/helpers/helpers.ts +++ b/examples/src/helpers/helpers.ts @@ -11,6 +11,7 @@ import { Wormhole, api, tasks, + DEFAULT_TASK_TIMEOUT, } from "@wormhole-foundation/connect-sdk"; // Importing from src so we dont have to rebuild to see debug stuff in signer @@ -70,12 +71,18 @@ export async function getStuff< }; } -export async function waitLog(wh: Wormhole, xfer: TokenTransfer) { - const it = TokenTransfer.track(wh, TokenTransfer.getReceipt(xfer)); - let res; - for (res = await it.next(); !res.done; res = await it.next()) - console.log("Current Transfer State: ", TransferState[res.value as TransferState]); - return res.value; +export async function waitLog( + wh: Wormhole, + xfer: TokenTransfer, + tag: string = "WaitLog", + timeout: number = DEFAULT_TASK_TIMEOUT, +) { + const tracker = TokenTransfer.track(wh, TokenTransfer.getReceipt(xfer), timeout); + let receipt; + for await (receipt of tracker) { + console.log(`${tag}: Current trasfer state: `, TransferState[receipt.state]); + } + return receipt; } // Note: This API may change but it is currently the best place to pull diff --git a/examples/src/tokenBridge.ts b/examples/src/tokenBridge.ts index 5d841844e..51290d518 100644 --- a/examples/src/tokenBridge.ts +++ b/examples/src/tokenBridge.ts @@ -49,7 +49,7 @@ import "@wormhole-foundation/connect-sdk-solana-tokenbridge"; // of the token // On the destination side, a wrapped version of the token will be minted // to the address specified in the transfer VAA - const automatic = true; + const automatic = false; // The automatic relayer has the ability to deliver some native gas funds to the destination account // The amount specified for native gas will be swapped for the native gas token according From 40e74a16592cb79fb4e11989a06a46d0266896e2 Mon Sep 17 00:00:00 2001 From: Ben Guidarelli Date: Thu, 28 Dec 2023 11:25:38 -0500 Subject: [PATCH 2/3] add circle transfer receipt and tracker --- connect/src/protocols/cctpTransfer.ts | 253 ++++++++++++++--- connect/src/protocols/tokenTransfer.ts | 267 +++++++++--------- connect/src/wormholeTransfer.ts | 13 +- core/definitions/src/attestation.ts | 11 +- .../definitions/src/protocols/circleBridge.ts | 5 + 5 files changed, 367 insertions(+), 182 deletions(-) diff --git a/connect/src/protocols/cctpTransfer.ts b/connect/src/protocols/cctpTransfer.ts index 7ad7cb104..708447073 100644 --- a/connect/src/protocols/cctpTransfer.ts +++ b/connect/src/protocols/cctpTransfer.ts @@ -1,9 +1,18 @@ -import { Chain, Network, Platform, circle, encoding } from "@wormhole-foundation/sdk-base"; import { + Chain, + ChainToPlatform, + Network, + Platform, + circle, + encoding, + toChain, +} from "@wormhole-foundation/sdk-base"; +import { + Attestation, AttestationId, + AttestationReceipt, AutomaticCircleBridge, ChainContext, - CircleAttestation, CircleBridge, CircleMessageId, CircleTransferDetails, @@ -21,10 +30,17 @@ import { import { signSendWait } from "../common"; import { DEFAULT_TASK_TIMEOUT } from "../config"; import { Wormhole } from "../wormhole"; -import { TransferQuote, TransferState, WormholeTransfer } from "../wormholeTransfer"; +import { + TransferQuote, + TransferReceipt, + TransferState, + WormholeTransfer, +} from "../wormholeTransfer"; + +type CircleTransferProtocol = "CircleBridge" | "AutomaticCircleBridge"; export class CircleTransfer - implements WormholeTransfer<"CircleBridge" | "AutomaticCircleBridge"> + implements WormholeTransfer { private readonly wh: Wormhole; @@ -37,18 +53,7 @@ export class CircleTransfer // Populated after Initialized txids: TransactionId[] = []; - // Populated if !automatic and after initialized - circleAttestations?: { - id: CircleMessageId; - message: CircleBridge.Message; - attestation?: CircleAttestation; - }[]; - - // Populated if automatic and after initialized - vaas?: { - id: WormholeMessageId; - vaa?: AutomaticCircleBridge.VAA; - }[]; + attestations?: AttestationReceipt[]; private constructor(wh: Wormhole, transfer: CircleTransferDetails) { this._state = TransferState.Created; @@ -137,7 +142,7 @@ export class CircleTransfer }; const tt = new CircleTransfer(wh, details); - tt.vaas = [{ id: { emitter, sequence: vaa.sequence, chain: chain }, vaa }]; + tt.attestations = [{ id: { emitter, sequence: vaa.sequence, chain: chain }, attestation: vaa }]; tt._state = TransferState.Attested; return tt; @@ -165,7 +170,7 @@ export class CircleTransfer }; const xfer = new CircleTransfer(wh, details); - xfer.circleAttestations = [{ id: { hash }, message: msg }]; + xfer.attestations = [{ id: { hash }, attestation: { message: msg } }]; xfer._state = TransferState.SourceInitiated; return xfer; @@ -200,7 +205,7 @@ export class CircleTransfer }; ct = new CircleTransfer(wh, details); - ct.circleAttestations = [{ id: circleMessage.id, message: circleMessage.message }]; + ct.attestations = [{ id: circleMessage.id, attestation: { message: circleMessage.message } }]; } ct._state = TransferState.SourceInitiated; @@ -249,20 +254,27 @@ export class CircleTransfer } private async _fetchWormholeAttestation(timeout?: number): Promise { - if (!this.vaas || this.vaas.length == 0) throw new Error("No VAA details available"); + let attestations = (this.attestations ?? []) as AttestationReceipt<"AutomaticCircleBridge">[]; + if (!attestations || attestations.length == 0) throw new Error("No VAA details available"); // Check if we already have the VAA - for (const idx in this.vaas) { - // already got it - if (this.vaas[idx]!.vaa) continue; - this.vaas[idx]!.vaa = await CircleTransfer.getTransferVaa(this.wh, this.vaas[idx]!.id); + for (const idx in attestations) { + if (attestations[idx]!.attestation) continue; + + attestations[idx]!.attestation = await CircleTransfer.getTransferVaa( + this.wh, + attestations[idx]!.id, + timeout, + ); } + this.attestations = attestations; - return this.vaas.map((v) => v.id); + return attestations.map((v) => v.id); } private async _fetchCircleAttestation(timeout?: number): Promise { - if (!this.circleAttestations || this.circleAttestations.length == 0) { + let attestations = (this.attestations ?? []) as AttestationReceipt<"CircleBridge">[]; + if (!attestations || attestations.length == 0) { // If we dont have any circle attestations yet, we need to start by // fetching the transaction details from the source chain if (this.txids.length === 0) @@ -275,20 +287,22 @@ export class CircleTransfer const cb = await fromChain.getCircleBridge(); const circleMessage = await cb.parseTransactionDetails(txid!.txid); - this.circleAttestations = [{ id: circleMessage.id, message: circleMessage.message }]; + attestations = [{ id: circleMessage.id, attestation: { message: circleMessage.message } }]; } - for (const idx in this.circleAttestations) { - const ca = this.circleAttestations[idx]!; - if (ca.attestation) continue; // already got it + for (const idx in attestations) { + const ca = attestations[idx]!; + if (ca.attestation.attestation) continue; // already got it const attestation = await this.wh.getCircleAttestation(ca.id.hash, timeout); if (attestation === null) throw new Error("No attestation available after timeout exhausted"); - this.circleAttestations[idx]!.attestation = attestation; + attestations[idx].attestation.attestation = attestation; } - return this.circleAttestations.map((v) => v.id); + this.attestations = attestations; + + return attestations.map((v) => v.id); } // wait for the VAA to be ready @@ -326,10 +340,10 @@ export class CircleTransfer // If its automatic, this does not need to be called if (this.transfer.automatic) { - if (!this.vaas) throw new Error("No VAA details available"); - if (this.vaas.length > 1) throw new Error(`Expected a VAA, found ${this.vaas.length}`); - - const { vaa } = this.vaas[0]!; + if (!this.attestations) throw new Error("No VAA details available"); + const vaa = this.attestations.find((a) => + isWormholeMessageId(a.id), + ) as AttestationReceipt<"AutomaticCircleBridge">; if (!vaa) throw new Error("No VAA found"); //const tb = await toChain.getAutomaticCircleBridge(); @@ -338,16 +352,21 @@ export class CircleTransfer throw new Error("No method to redeem auto circle bridge tx (yet)"); } - if (!this.circleAttestations) throw new Error("No Circle Attestations found"); + if (!this.attestations) throw new Error("No Circle Attestations found"); - if (this.circleAttestations.length > 1) - throw new Error( - `Expected a single circle attestation, found ${this.circleAttestations.length}`, - ); + const circleAttestations = this.attestations.filter((a) => + isCircleMessageId(a.id), + ) as AttestationReceipt<"CircleBridge">[]; + + if (circleAttestations.length > 1) + throw new Error(`Expected a single circle attestation, found ${circleAttestations.length}`); const toChain = this.wh.getChain(this.transfer.to.chain); - const { id, message, attestation } = this.circleAttestations[0]!; + const { + id, + attestation: { message, attestation }, + } = circleAttestations[0]!; if (!attestation) throw new Error(`No Circle Attestation for ${id.hash}`); @@ -411,6 +430,13 @@ export class CircleTransfer }; } + static async isTransferComplete( + toChain: ChainContext, + attestation: Attestation, + ) { + throw new Error("Not implemented"); + } + static async getTransferVaa( wh: Wormhole, wormholeMessageId: WormholeMessageId, @@ -424,4 +450,147 @@ export class CircleTransfer if (!vaa) throw new Error(`No VAA available after timeout exhausted`); return vaa; } + + static async getTransferMessage( + fromChain: ChainContext, + txid: TxHash, + ) { + const cb = await fromChain.getCircleBridge(); + const circleMessage = await cb.parseTransactionDetails(txid); + return circleMessage.id; + } + + static getReceipt( + xfer: CircleTransfer, + ): TransferReceipt { + const { from, to } = xfer.transfer; + + const att = xfer.attestations.filter((a) => + isWormholeMessageId(a.id), + ) as AttestationReceipt<"AutomaticCircleBridge">[]; + + const ctt = xfer.attestations.filter((a) => + 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; + + 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; + + return receipt; + } + + // 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( + wh: Wormhole, + 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>, + ) { + const start = Date.now(); + const leftover = (start: number, max: number) => Math.max(max - (Date.now() - start), 0); + + _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 CircleTransfer.getTransferMessage(_fromChain, initTx.txid); + 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"; + + 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; + if (!vaa) { + vaa = await CircleTransfer.getTransferVaa( + wh, + receipt.attestation.id as WormholeMessageId, + 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 as WormholeMessageId, + leftover(start, timeout), + ); + if (!txStatus) { + yield receipt; + return; + } + + if (txStatus.globalTx?.destinationTx?.txHash) { + const { chainId, txHash } = txStatus.globalTx.destinationTx; + + receipt.destinationTxs = [ + { + chain: toChain(chainId), + txid: txHash, + }, + ]; + + receipt.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; + yield receipt; + } + } + yield receipt; + return; + } } diff --git a/connect/src/protocols/tokenTransfer.ts b/connect/src/protocols/tokenTransfer.ts index 3c212b284..97fcf030f 100644 --- a/connect/src/protocols/tokenTransfer.ts +++ b/connect/src/protocols/tokenTransfer.ts @@ -9,6 +9,7 @@ import { } from "@wormhole-foundation/sdk-base"; import { AttestationId, + AttestationReceipt, AutomaticTokenBridge, ChainContext, Signer, @@ -57,10 +58,7 @@ export class TokenTransfer // The corresponding vaa representing the TokenTransfer // on the source chain (if its been completed and finalized) - vaas?: { - id: WormholeMessageId; - vaa?: TokenTransferVAA; - }[]; + attestations?: AttestationReceipt[]; private constructor(wh: Wormhole, transfer: TokenTransferDetails) { this._state = TransferState.Created; @@ -154,7 +152,7 @@ export class TokenTransfer // TODO: grab at least the init tx from the api const tt = new TokenTransfer(wh, details); - tt.vaas = [{ id: id, vaa }]; + tt.attestations = [{ id: id, attestation: vaa }]; tt._state = TransferState.Attested; return tt; } @@ -203,7 +201,7 @@ export class TokenTransfer if (this._state < TransferState.SourceInitiated || this._state > TransferState.Attested) throw new Error("Invalid state transition in `ready`"); - if (!this.vaas || this.vaas.length === 0) { + if (!this.attestations || this.attestations.length === 0) { if (this.txids.length === 0) throw new Error("No VAAs set and txids available to look them up"); @@ -214,22 +212,22 @@ export class TokenTransfer txid.txid, timeout, ); - this.vaas = [{ id: msgId }]; + this.attestations = [{ id: msgId }]; } - for (const idx in this.vaas) { + for (const idx in this.attestations) { // Check if we already have the VAA - if (this.vaas[idx]!.vaa) continue; + if (this.attestations[idx]!.attestation) continue; - this.vaas[idx]!.vaa = await TokenTransfer.getTransferVaa( + this.attestations[idx]!.attestation = await TokenTransfer.getTransferVaa( this.wh, - this.vaas[idx]!.id, + this.attestations[idx]!.id, timeout, ); } this._state = TransferState.Attested; - return this.vaas.map((vaa) => vaa.id); + return this.attestations.map((vaa) => vaa.id); } // finish the WormholeTransfer by submitting transactions to the destination chain @@ -244,122 +242,23 @@ export class TokenTransfer if (this._state < TransferState.Attested) throw new Error("Invalid state transition in `finish`. Be sure to call `fetchAttestation`."); - if (!this.vaas) throw new Error("No VAA details available"); + if (!this.attestations) throw new Error("No VAA details available"); // TODO: when do we get >1? - const { vaa } = this.vaas[0]!; - if (!vaa) throw new Error(`No VAA found for ${this.vaas[0]!.id.sequence}`); + const { attestation } = this.attestations[0]!; + if (!attestation) throw new Error(`No VAA found for ${this.attestations[0]!.id.sequence}`); const toChain = this.wh.getChain(this.transfer.to.chain); - const redeemTxids = await TokenTransfer.redeem(toChain, vaa, signer); + const redeemTxids = await TokenTransfer.redeem( + toChain, + attestation as TokenTransferVAA, + signer, + ); this.txids.push(...redeemTxids); return redeemTxids.map(({ txid }) => txid); } - // 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( - wh: Wormhole, - 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>, - ) { - const start = Date.now(); - const leftover = (start: number, max: number) => Math.max(max - (Date.now() - start), 0); - - _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.emitter) { - 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!, - leftover(start, timeout), - ); - if (!txStatus) { - yield receipt; - return; - } - - if (txStatus.globalTx?.destinationTx?.txHash) { - const { chainId, txHash } = txStatus.globalTx.destinationTx; - - receipt.destinationTxs = [ - { - chain: toChain(chainId), - txid: txHash, - }, - ]; - - receipt.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( - _toChain, - receipt.attestation.attestation as TokenTransferVAA, - ), - leftover(start, timeout)) - ) { - receipt.state = TransferState.DestinationFinalized; - yield receipt; - } - } - yield receipt; - return; - } - // Static method to perform the transfer so a custom RPC may be used // Note: this assumes the transfer has already been validated with `validateTransfer` static async transfer( @@ -622,21 +521,24 @@ export class TokenTransfer static getReceipt( xfer: TokenTransfer, - ): TransferReceipt< - "TokenBridge" | "AutomaticTokenBridge", - typeof xfer.transfer.from.chain, - typeof xfer.transfer.to.chain - > { - const att = xfer.vaas && xfer.vaas.length > 0 ? xfer.vaas![0]! : undefined; + ): TransferReceipt { + const { transfer } = xfer; - const attestation = att && att.id.emitter ? { id: att.id, attestation: att.vaa } : undefined; + 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 receipt = { - from: xfer.transfer.from.chain, - to: xfer.transfer.to.chain, - state: TransferState.SourceInitiated, - originTxs: xfer.txids.filter((txid) => txid.chain === xfer.transfer.from.chain), - destinationTxs: xfer.txids.filter((txid) => txid.chain === xfer.transfer.to.chain), + protocol: (transfer.automatic + ? "AutomaticTokenBridge" + : "TokenBridge") as TokenTransferProtocol, + request: transfer, + from: transfer.from.chain, + to: transfer.to.chain, + state: TransferState.Created, + originTxs: xfer.txids.filter((txid) => txid.chain === transfer.from.chain), + destinationTxs: xfer.txids.filter((txid) => txid.chain === transfer.to.chain), attestation, }; @@ -647,4 +549,107 @@ export class TokenTransfer return receipt; } + + // 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( + wh: Wormhole, + 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>, + ) { + const start = Date.now(); + const leftover = (start: number, max: number) => Math.max(max - (Date.now() - start), 0); + + _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!, + leftover(start, timeout), + ); + if (!txStatus) { + yield receipt; + return; + } + + if (txStatus.globalTx?.destinationTx?.txHash) { + const { chainId, txHash } = txStatus.globalTx.destinationTx; + + receipt.destinationTxs = [ + { + chain: toChain(chainId), + txid: txHash, + }, + ]; + + receipt.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( + _toChain, + receipt.attestation.attestation as TokenTransferVAA, + ), + leftover(start, timeout)) + ) { + receipt.state = TransferState.DestinationFinalized; + yield receipt; + } + } + yield receipt; + return; + } } diff --git a/connect/src/wormholeTransfer.ts b/connect/src/wormholeTransfer.ts index 06ad3b1d9..42048e8fc 100644 --- a/connect/src/wormholeTransfer.ts +++ b/connect/src/wormholeTransfer.ts @@ -6,8 +6,8 @@ import { ProtocolName, } from "@wormhole-foundation/sdk-base"; import { - Attestation, AttestationId, + AttestationReceipt, ChainContext, CircleTransferDetails, GatewayTransferDetails, @@ -46,15 +46,14 @@ export type TransferReceipt< SC extends Chain = Chain, DC extends Chain = Chain, > = { + readonly protocol: PN; + readonly request: TransferRequest; + readonly from: SC; + readonly to: DC; state: TransferState; - from: SC; - to: DC; originTxs: TransactionId[]; destinationTxs: TransactionId[]; - attestation?: { - id: AttestationId; - attestation?: Attestation; - }; + attestation?: AttestationReceipt; }; // Quote with optional relayer fees if the transfer diff --git a/core/definitions/src/attestation.ts b/core/definitions/src/attestation.ts index 604528356..37e704e24 100644 --- a/core/definitions/src/attestation.ts +++ b/core/definitions/src/attestation.ts @@ -3,7 +3,7 @@ import { SequenceId } from "./types"; import { UniversalAddress } from "./universalAddress"; import { VAA } from "./vaa"; import { AutomaticTokenBridge, TokenBridge } from "./protocols/tokenBridge"; -import { AutomaticCircleBridge } from "./protocols/circleBridge"; +import { AutomaticCircleBridge, CircleBridge } from "./protocols/circleBridge"; import { IbcTransferData } from "./protocols/ibc"; // Could be VAA or Circle or ..? @@ -25,11 +25,18 @@ export type Attestation = PN extends : PN extends "AutomaticCircleBridge" ? AutomaticCircleBridge.VAA : PN extends "CircleBridge" - ? CircleAttestation + ? CircleBridge.Attestation : PN extends "IbcBridge" ? IbcTransferData : never; +// Attestation Receipt contains the Id to lookup the attestation +// and possibly a cached/parsed attestation +export type AttestationReceipt = { + id: AttestationId; + attestation?: Attestation; +}; + // Wormhole Message Identifier used to fetch a VAA // Possibly with a VAA already set export type WormholeMessageId = { diff --git a/core/definitions/src/protocols/circleBridge.ts b/core/definitions/src/protocols/circleBridge.ts index 0a7af31a0..3ca99f228 100644 --- a/core/definitions/src/protocols/circleBridge.ts +++ b/core/definitions/src/protocols/circleBridge.ts @@ -28,6 +28,11 @@ export namespace CircleBridge { export type Message = LayoutToType; + export type Attestation = { + message: Message; + attestation?: string; + }; + export const deserialize = (data: Uint8Array): [CircleBridge.Message, string] => { const msg = deserializeLayout(circleMessageLayout, data); const messsageHash = encoding.hex.encode(keccak256(data), true); From 090ca53f5209112d8757abb9ab7e0759a748fd22 Mon Sep 17 00:00:00 2001 From: Ben Guidarelli Date: Thu, 28 Dec 2023 12:43:27 -0500 Subject: [PATCH 3/3] add isTransferComplete for solana cctp --- connect/src/protocols/cctpTransfer.ts | 10 +++++- connect/src/wormholeTransfer.ts | 3 +- .../definitions/src/protocols/circleBridge.ts | 1 + .../evm/protocols/cctp/src/circleBridge.ts | 11 +++++- .../solana/protocols/cctp/src/circleBridge.ts | 33 ++++++++++++++++- .../src/utils/instructions/receiveMessage.ts | 36 ++++++++++++++----- .../protocols/cctp/src/utils/program.ts | 4 +-- 7 files changed, 82 insertions(+), 16 deletions(-) diff --git a/connect/src/protocols/cctpTransfer.ts b/connect/src/protocols/cctpTransfer.ts index 708447073..d0f335f9c 100644 --- a/connect/src/protocols/cctpTransfer.ts +++ b/connect/src/protocols/cctpTransfer.ts @@ -434,7 +434,15 @@ export class CircleTransfer toChain: ChainContext, attestation: Attestation, ) { - throw new Error("Not implemented"); + // TODO: inferring from fields what type this is, we should + // have typeguards or require another argument to better deterimine + if ("message" in attestation) { + const cb = await toChain.getCircleBridge(); + return cb.isTransferCompleted(attestation.message); + } + throw new Error("Not implemented for automatic circle bridge"); + // const acb = await toChain.getAutomaticCircleBridge(); + // return acb.isTransferCompleted(attestation); } static async getTransferVaa( diff --git a/connect/src/wormholeTransfer.ts b/connect/src/wormholeTransfer.ts index 42048e8fc..b3f2a77ee 100644 --- a/connect/src/wormholeTransfer.ts +++ b/connect/src/wormholeTransfer.ts @@ -16,7 +16,6 @@ import { TokenTransferDetails, TransactionId, TxHash, - VAA, } from "@wormhole-foundation/sdk-definitions"; import { Wormhole } from "./wormhole"; @@ -89,7 +88,7 @@ export type TransferQuote = { export interface TransferProtocol { isTransferComplete>( toChain: ChainContext, - vaa: VAA, + attestation: AttestationId, ): Promise; validateTransferDetails( wh: Wormhole, diff --git a/core/definitions/src/protocols/circleBridge.ts b/core/definitions/src/protocols/circleBridge.ts index 3ca99f228..25c877218 100644 --- a/core/definitions/src/protocols/circleBridge.ts +++ b/core/definitions/src/protocols/circleBridge.ts @@ -108,6 +108,7 @@ export interface CircleBridge< recipient: ChainAddress, amount: bigint, ): AsyncGenerator>; + isTransferCompleted(message: CircleBridge.Message): Promise; parseTransactionDetails(txid: string): Promise; } diff --git a/platforms/evm/protocols/cctp/src/circleBridge.ts b/platforms/evm/protocols/cctp/src/circleBridge.ts index cf6fb351d..1fe8abdd7 100644 --- a/platforms/evm/protocols/cctp/src/circleBridge.ts +++ b/platforms/evm/protocols/cctp/src/circleBridge.ts @@ -23,7 +23,7 @@ import { addChainId, addFrom, } from '@wormhole-foundation/connect-sdk-evm'; -import { LogDescription, Provider, TransactionRequest } from 'ethers'; +import { LogDescription, Provider, TransactionRequest, ethers } from 'ethers'; import { ethers_contracts } from '.'; //https://github.com/circlefin/evm-cctp-contracts @@ -160,6 +160,15 @@ export class EvmCircleBridge ); } + async isTransferCompleted(message: CircleBridge.Message): Promise { + const cctpDomain = circle.circleChainId(message.sourceDomain); + const hash = ethers.keccak256( + ethers.solidityPacked(['uint32', 'uint64'], [cctpDomain, message.nonce]), + ); + const result = this.msgTransmitter.usedNonces.staticCall(hash); + return result.toString() === '1'; + } + // Fetch the transaction logs and parse the CircleTransferMessage async parseTransactionDetails(txid: string): Promise { const receipt = await this.provider.getTransactionReceipt(txid); diff --git a/platforms/solana/protocols/cctp/src/circleBridge.ts b/platforms/solana/protocols/cctp/src/circleBridge.ts index 279d80a53..55bc2ba56 100644 --- a/platforms/solana/protocols/cctp/src/circleBridge.ts +++ b/platforms/solana/protocols/cctp/src/circleBridge.ts @@ -11,7 +11,7 @@ import { circle, } from '@wormhole-foundation/connect-sdk'; -import { EventParser, Program } from '@project-serum/anchor'; +import { BN, EventParser, Program } from '@project-serum/anchor'; import { getAssociatedTokenAddressSync } from '@solana/spl-token'; import { SolanaAddress, @@ -26,8 +26,10 @@ import { createReadOnlyTokenMessengerProgramInterface, } from './utils'; import { + calculateFirstNonce, createDepositForBurnInstruction, createReceiveMessageInstruction, + nonceAccount, } from './utils/instructions'; export class SolanaCircleBridge @@ -147,6 +149,35 @@ export class SolanaCircleBridge yield this.createUnsignedTx(transaction, 'CircleBridge.Transfer'); } + async isTransferCompleted(message: CircleBridge.Message): Promise { + const usedNoncesAddress = nonceAccount( + message.nonce, + message.sourceDomain, + this.messageTransmitter.programId, + ); + + const firstNonce = calculateFirstNonce(message.nonce); + + // usedNonces should be a [u64;100] where each bit is a nonce flag + const { usedNonces } = + // @ts-ignore -- + await this.messageTransmitter.account.usedNonces.fetch(usedNoncesAddress); + + // get the nonce index based on the account's first nonce + const nonceIndex = Number(message.nonce - firstNonce); + + // get the the u64 the nonce's flag is in + const nonceElement = usedNonces[Math.floor(nonceIndex / 64)]; + if (!nonceElement) throw new Error('Invalid nonce byte index'); + + // get the nonce flag index and build a bitmask + const nonceBitIndex = nonceIndex % 64; + const mask = new BN(1 << nonceBitIndex); + + // If the flag is 0 it is _not_ used + return !nonceElement.and(mask).isZero(); + } + // Fetch the transaction logs and parse the CircleTransferMessage async parseTransactionDetails(txid: string): Promise { const tx = await this.connection.getTransaction(txid); diff --git a/platforms/solana/protocols/cctp/src/utils/instructions/receiveMessage.ts b/platforms/solana/protocols/cctp/src/utils/instructions/receiveMessage.ts index 02ac78041..599810156 100644 --- a/platforms/solana/protocols/cctp/src/utils/instructions/receiveMessage.ts +++ b/platforms/solana/protocols/cctp/src/utils/instructions/receiveMessage.ts @@ -15,6 +15,29 @@ import { SolanaAddress } from '@wormhole-foundation/connect-sdk-solana'; import { findProgramAddress } from '../accounts'; import { createMessageTransmitterProgramInterface } from '../program'; +const MAX_NONCES_PER_ACCOUNT = 6400n; + +export function calculateFirstNonce(nonce: bigint) { + return ( + ((nonce - BigInt(1)) / MAX_NONCES_PER_ACCOUNT) * MAX_NONCES_PER_ACCOUNT + + BigInt(1) + ); +} +export function nonceAccount( + nonce: bigint, + sourceChain: circle.CircleChain, + messageTransmitterProgramId: PublicKey, +) { + const srcDomain = circle.toCircleChainId(sourceChain).toString(); + const usedNonces = findProgramAddress( + 'used_nonces', + messageTransmitterProgramId, + [srcDomain, calculateFirstNonce(nonce).toString()], + ).publicKey; + + return usedNonces; +} + export async function createReceiveMessageInstruction( messageTransmitterProgramId: PublicKey, tokenMessengerProgramId: PublicKey, @@ -84,16 +107,11 @@ export async function createReceiveMessageInstruction( ).publicKey; // Calculate the nonce PDA. - const maxNoncesPerAccount = 6400n; - const firstNonce = - ((circleMessage.nonce - BigInt(1)) / maxNoncesPerAccount) * - maxNoncesPerAccount + - BigInt(1); - const usedNonces = findProgramAddress( - 'used_nonces', + const usedNonces = nonceAccount( + circleMessage.nonce, + circleMessage.sourceDomain, messageTransmitterProgramId, - [srcDomain, firstNonce.toString()], - ).publicKey; + ); // Build the accountMetas list. These are passed as remainingAccounts for the TokenMessengerMinter CPI const accountMetas: AccountMeta[] = []; diff --git a/platforms/solana/protocols/cctp/src/utils/program.ts b/platforms/solana/protocols/cctp/src/utils/program.ts index 97d747c6f..cb570a223 100644 --- a/platforms/solana/protocols/cctp/src/utils/program.ts +++ b/platforms/solana/protocols/cctp/src/utils/program.ts @@ -9,7 +9,7 @@ export function createTokenMessengerProgramInterface( provider?: Provider, ): Program { return new Program( - idl.TokenMessengerIdl as TokenMessenger, + idl.TokenMessengerIdl, new PublicKey(programId), provider === undefined ? ({ connection: null } as any) : provider, ); @@ -30,7 +30,7 @@ export function createMessageTransmitterProgramInterface( provider?: Provider, ): Program { return new Program( - idl.MessageTransmitterIdl as MessageTransmitter, + idl.MessageTransmitterIdl, new PublicKey(programId), provider === undefined ? ({ connection: null } as any) : provider, );