From b412d8cdf63b5c2ac84303c5dc3c901361f897af Mon Sep 17 00:00:00 2001 From: Ben Guidarelli Date: Tue, 2 Jan 2024 10:45:49 -0500 Subject: [PATCH] push everything into attestations --- connect/src/protocols/gatewayTransfer.ts | 156 ++++++++++++-------- connect/src/tasks.ts | 4 +- platforms/cosmwasm/protocols/ibc/src/ibc.ts | 2 +- 3 files changed, 101 insertions(+), 61 deletions(-) diff --git a/connect/src/protocols/gatewayTransfer.ts b/connect/src/protocols/gatewayTransfer.ts index 406a35dfea..8f337e0710 100644 --- a/connect/src/protocols/gatewayTransfer.ts +++ b/connect/src/protocols/gatewayTransfer.ts @@ -1,12 +1,14 @@ import { ChainToPlatform, Network, + PlatformToChains, chainToPlatform, encoding, toChain, - PlatformToChains, } from "@wormhole-foundation/sdk-base"; import { + AttestationId, + AttestationReceipt, ChainAddress, ChainContext, GatewayTransferDetails, @@ -23,16 +25,16 @@ import { WormholeMessageId, gatewayTransferMsg, isGatewayTransferDetails, + isIbcMessageId, isTransactionIdentifier, isWormholeMessageId, toGatewayMsg, toNative, - AttestationId, } from "@wormhole-foundation/sdk-definitions"; import { signSendWait } from "../common"; import { fetchIbcXfer, isTokenBridgeVaaRedeemed, retry } from "../tasks"; import { Wormhole } from "../wormhole"; -import { TransferState, WormholeTransfer } from "../wormholeTransfer"; +import { TransferReceipt, TransferState, WormholeTransfer } from "../wormholeTransfer"; type GatewayContext = ChainContext< N, @@ -40,11 +42,18 @@ type GatewayContext = ChainContext< typeof GatewayTransfer.chain >; -export class GatewayTransfer implements WormholeTransfer<"IbcBridge"> { +type GatewayProtocols = "IbcBridge" | "TokenBridge"; + +export class GatewayTransfer + implements WormholeTransfer +{ static chain: "Wormchain" = "Wormchain"; private readonly wh: Wormhole; + // state machine tracker + private _state: TransferState; + // Wormchain context private readonly gateway: GatewayContext; // Wormchain IBC Bridge @@ -52,9 +61,6 @@ export class GatewayTransfer implements WormholeTra // Contract address private readonly gatewayAddress: ChainAddress; - // state machine tracker - private _state: TransferState; - // cached message derived from transfer details // note: we dont want to create multiple different ones since // the nonce may change and we want to keep it consistent @@ -64,17 +70,14 @@ export class GatewayTransfer implements WormholeTra transfer: GatewayTransferDetails; // Transaction Ids from source chain - transactions: TransactionId[] = []; + txids: TransactionId[] = []; // The corresponding vaa representing the GatewayTransfer // on the source chain (if it came from outside cosmos and if its been completed and finalized) - vaas?: { - id: WormholeMessageId; - vaa?: TokenBridge.TransferVAA; - }[]; + attestations?: AttestationReceipt[]; // Any transfers we do over ibc - ibcTransfers: IbcTransferInfo[] = []; + //ibcTransfers: IbcTransferInfo[] = []; private constructor( wh: Wormhole, @@ -152,7 +155,7 @@ export class GatewayTransfer implements WormholeTra } const gt = new GatewayTransfer(wh, gtd, wc, wcibc); - gt.transactions = txns; + gt.txids = txns; // Since we're picking up from somewhere we can move the // state maching to initiated @@ -301,13 +304,11 @@ export class GatewayTransfer implements WormholeTra if (this._state !== TransferState.Created) throw new Error("Invalid state transition in `start`"); - this.transactions = await (this.fromGateway() - ? this._transferIbc(signer) - : this._transfer(signer)); + this.txids = await (this.fromGateway() ? this._transferIbc(signer) : this._transfer(signer)); // Update State Machine this._state = TransferState.SourceInitiated; - return this.transactions.map((tx) => tx.txid); + return this.txids.map((tx) => tx.txid); } private async _transfer(signer: Signer): Promise { @@ -353,9 +354,10 @@ export class GatewayTransfer implements WormholeTra if (this._state < TransferState.SourceInitiated || this._state > TransferState.Attested) throw new Error("Invalid state transition in `fetchAttestation`"); - const attestations: AttestationId[] = []; + if (!this.attestations) this.attestations = []; const chain = this.wh.getChain(this.transfer.from.chain); + // collect ibc transfers and additional transaction ids if (this.fromGateway()) { // assume all the txs are from the same chain @@ -368,16 +370,13 @@ export class GatewayTransfer implements WormholeTra // start by getting the IBC transfers into wormchain // from the cosmos chain - this.ibcTransfers = ( - await Promise.all( - this.transactions.map((tx) => originIbcbridge.lookupTransferFromTx(tx.txid)), - ) + const ibcTransfers = ( + await Promise.all(this.txids.map((tx) => originIbcbridge.lookupTransferFromTx(tx.txid))) ).flat(); - // I don't know why this would happen so lmk if you see this - if (this.ibcTransfers.length != 1) throw new Error("why?"); + this.attestations.push(...ibcTransfers); - const [xfer] = this.ibcTransfers; + const [xfer] = ibcTransfers; if (!this.toGateway()) { // If we're leaving cosmos, grab the VAA from the gateway // now find the corresponding wormchain transaction given the ibcTransfer info @@ -392,27 +391,23 @@ export class GatewayTransfer implements WormholeTra if (!whm) throw new Error("Matching wormhole message not found after retries exhausted"); const vaa = await GatewayTransfer.getTransferVaa(this.wh, whm); - this.vaas = [{ id: whm, vaa }]; - - attestations.push(whm); + this.attestations.push({ id: whm, attestation: vaa }); } else { // Otherwise we need to get the transfer on the destination chain const dstChain = this.wh.getChain(this.transfer.to.chain); const dstIbcBridge = await dstChain.getIbcBridge(); const ibcXfer = await dstIbcBridge.lookupTransferFromIbcMsgId(xfer!.id); - this.ibcTransfers.push(ibcXfer); + this.attestations.push(ibcXfer); } } else { // Otherwise, we're coming from outside cosmos and // we need to find the wormchain ibc transaction information // by searching for the transaction containing the // GatewayTransferMsg - const transferTransaction = this.transactions[this.transactions.length - 1]!; + const transferTransaction = this.txids[this.txids.length - 1]!; const [whm] = await Wormhole.parseMessageFromTx(chain, transferTransaction.txid); const vaa = await GatewayTransfer.getTransferVaa(this.wh, whm!); - this.vaas = [{ id: whm!, vaa }]; - - attestations.push(whm!); + this.attestations.push({ id: whm!, attestation: vaa }); // TODO: conf for these settings? how do we choose them? const vaaRedeemedRetryInterval = 2000; @@ -449,12 +444,15 @@ export class GatewayTransfer implements WormholeTra // TODO: check if pending and bail(?) if so } - this.ibcTransfers.push(wcTransfer); + this.attestations.push(wcTransfer); // Finally, get the IBC transfer to the destination chain const destChain = this.wh.getChain(this.transfer.to.chain); - const destIbcBridge = await destChain.getIbcBridge(); - //@ts-ignore + const destIbcBridge = (await destChain.getIbcBridge()) as IbcBridge< + N, + "Cosmwasm", + PlatformToChains<"Cosmwasm"> + >; const destTransferTask = () => fetchIbcXfer(destIbcBridge, wcTransfer.id); const destTransfer = await retry( destTransferTask, @@ -468,16 +466,11 @@ export class GatewayTransfer implements WormholeTra JSON.stringify(wcTransfer.id), ); - this.ibcTransfers.push(destTransfer); + this.attestations.push(destTransfer); } - - // Add transfers to attestations we return - // Note: there is no ordering guarantee here - attestations.push(...this.ibcTransfers.map((xfer) => xfer.id)); - this._state = TransferState.Attested; - return attestations; + return this.attestations.map((xfer) => xfer.id); } // finish the WormholeTransfer by submitting transactions to the destination chain @@ -494,26 +487,21 @@ export class GatewayTransfer implements WormholeTra if (this.toGateway()) // TODO: assuming the last transaction captured is the one from gateway to the destination - return [this.transactions[this.transactions.length - 1]!.txid]; - - if (!this.vaas) throw new Error("No VAA details available to redeem"); - if (this.vaas.length > 1) throw new Error("Expected 1 vaa"); + return [this.txids[this.txids.length - 1]!.txid]; const { chain, address } = this.transfer.to; const toChain = this.wh.getChain(chain); - // TODO: these could be different, but when? - //const signerAddress = toNative(signer.chain(), signer.address()); - const toAddress = address.toUniversalAddress(); - const tb = await toChain.getTokenBridge(); - const { vaa } = this.vaas[0]!; - if (!vaa) throw new Error(`No VAA found for ${this.vaas[0]!.id.sequence}`); + const toAddress = address.toUniversalAddress(); - const xfer = tb.redeem(toAddress, vaa); + const { attestation } = this.attestations.filter((a) => isWormholeMessageId(a.id))[0]!; + if (!attestation) throw new Error(`No VAA found for ${this.attestations[0]!.id.sequence}`); + + const xfer = tb.redeem(toAddress, attestation as TokenBridge.TransferVAA); const redeemTxs = await signSendWait(toChain, xfer, signer); - this.transactions.push(...redeemTxs); + this.txids.push(...redeemTxs); this._state = TransferState.DestinationInitiated; return redeemTxs.map(({ txid }) => txid); } @@ -528,13 +516,65 @@ export class GatewayTransfer implements WormholeTra return vaa; } + static getReceipt( + xfer: GatewayTransfer, + ): TransferReceipt { + const { transfer } = xfer; + + const protocol = xfer.fromGateway() ? "IbcBridge" : "TokenBridge"; + const from = transfer.from.chain; + const to = transfer.to.chain; + + let receipt: Partial> = { + protocol, + request: transfer, + from: from, + to: to, + state: TransferState.Created, + }; + + const originTxs = xfer.txids.filter((txid) => txid.chain === transfer.from.chain); + if (originTxs.length > 0) { + receipt = { ...receipt, state: TransferState.SourceInitiated, originTxs: originTxs }; + } + + const ibcAtt = xfer.attestations.filter((a) => + isIbcMessageId(a.id), + ) as AttestationReceipt<"IbcBridge">[]; + + const whAtt = xfer.attestations.filter((a) => + isWormholeMessageId(a.id), + ) as AttestationReceipt<"TokenBridge">[]; + + const attestation = whAtt.length > 0 ? whAtt[0] : ibcAtt.length > 0 ? ibcAtt[0] : 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 as TransferReceipt; + } + // Implicitly determine if the chain is Gateway enabled by // checking to see if the Gateway IBC bridge has a transfer channel setup // If this is a new chain, add the channels to the constants file - private fromGateway(): boolean { + fromGateway(): boolean { return this.gatewayIbcBridge.getTransferChannel(this.transfer.from.chain) !== null; } - private toGateway(): boolean { + toGateway(): boolean { return this.gatewayIbcBridge.getTransferChannel(this.transfer.to.chain) !== null; } } diff --git a/connect/src/tasks.ts b/connect/src/tasks.ts index f82f254b5c..33af81d656 100644 --- a/connect/src/tasks.ts +++ b/connect/src/tasks.ts @@ -75,8 +75,8 @@ export async function isTokenBridgeVaaRedeemed< } } -export async function fetchIbcXfer>( - wcIbc: IbcBridge, +export async function fetchIbcXfer( + wcIbc: IbcBridge>, msg: TxHash | TransactionId | IbcMessageId | GatewayTransferMsg | GatewayTransferWithPayloadMsg, ): Promise { try { diff --git a/platforms/cosmwasm/protocols/ibc/src/ibc.ts b/platforms/cosmwasm/protocols/ibc/src/ibc.ts index fa42d1b379..4e7e2b100f 100644 --- a/platforms/cosmwasm/protocols/ibc/src/ibc.ts +++ b/platforms/cosmwasm/protocols/ibc/src/ibc.ts @@ -99,7 +99,7 @@ export class CosmwasmIbcBridge recipient: ChainAddress, token: AnyCosmwasmAddress | "native", amount: bigint, - ): AsyncGenerator> { + ) { const senderAddress = new CosmwasmAddress(sender).toString(); const nonce = Math.round(Math.random() * 10000);