UNPKG

@wormhole-foundation/sdk-connect

Version:

The core package for the Connect SDK, used in conjunction with 1 or more of the chain packages

494 lines 23.5 kB
import { time } from "@wormhole-foundation/sdk-base"; import { circle, encoding, finality, guardians, toChain } from "@wormhole-foundation/sdk-base"; import { CircleBridge, isCircleMessageId, isCircleTransferDetails, isTransactionIdentifier, isWormholeMessageId, } from "@wormhole-foundation/sdk-definitions"; import { signSendWait } from "../../common.js"; import { DEFAULT_TASK_TIMEOUT } from "../../config.js"; import { TransferState, isAttested, isRedeemed, isSourceFinalized, isSourceInitiated, } from "../../types.js"; import { Wormhole } from "../../wormhole.js"; import { chainToPlatform } from "@wormhole-foundation/sdk-base"; export class CircleTransfer { wh; fromChain; toChain; // state machine tracker _state; // transfer details transfer; // Populated after Initialized txids = []; attestations; constructor(wh, transfer, fromChain, toChain) { this._state = TransferState.Created; this.wh = wh; this.transfer = transfer; this.fromChain = fromChain ?? wh.getChain(transfer.from.chain); this.toChain = toChain ?? wh.getChain(transfer.to.chain); } getTransferState() { return this._state; } static async from(wh, from, timeout = DEFAULT_TASK_TIMEOUT, fromChain, toChain) { // This is a new transfer, just return the object if (isCircleTransferDetails(from)) { from = { ...from, ...(await CircleTransfer.destinationOverrides(wh.getChain(from.from.chain), wh.getChain(from.to.chain), from)), }; return new CircleTransfer(wh, from, fromChain, toChain); } // This is an existing transfer, fetch the details let tt; if (isWormholeMessageId(from)) { tt = await CircleTransfer.fromWormholeMessageId(wh, from, timeout); } else if (isTransactionIdentifier(from)) { tt = await CircleTransfer.fromTransaction(wh, from, timeout, fromChain); } else if (isCircleMessageId(from)) { tt = await CircleTransfer.fromCircleMessage(wh, from); } else { throw new Error("Invalid `from` parameter for CircleTransfer"); } tt.fromChain = fromChain ?? wh.getChain(tt.transfer.from.chain); tt.toChain = toChain ?? wh.getChain(tt.transfer.to.chain); await tt.fetchAttestation(timeout); return tt; } // init from the seq id static async fromWormholeMessageId(wh, from, timeout) { const { chain, emitter } = from; const vaa = await CircleTransfer.getTransferVaa(wh, from); const rcvAddress = vaa.payload.mintRecipient; const rcvChain = circle.toCircleChain(wh.network, vaa.payload.targetDomain); // Check if its a payload 3 targeted at a relayer on the destination chain const { wormholeRelayer } = wh.config.chains[rcvChain].contracts.cctp; let automatic = false; if (wormholeRelayer) { const relayerAddress = Wormhole.chainAddress(chain, wormholeRelayer).address.toUniversalAddress(); automatic = vaa.payloadName === "TransferWithRelay" && rcvAddress.equals(relayerAddress); } const details = { from: { chain: from.chain, address: vaa.payload.caller }, to: { chain: rcvChain, address: rcvAddress }, amount: vaa.payload.token.amount, automatic, }; const tt = new CircleTransfer(wh, details); tt.attestations = [{ id: { emitter, sequence: vaa.sequence, chain: chain }, attestation: vaa }]; tt._state = TransferState.Attested; return tt; } static async fromCircleMessage(wh, message) { const [msg, hash] = CircleBridge.deserialize(encoding.hex.decode(message)); const { payload: burnMessage } = msg; const xferSender = burnMessage.messageSender; const xferReceiver = burnMessage.mintRecipient; const sendChain = circle.toCircleChain(wh.network, msg.sourceDomain); const rcvChain = circle.toCircleChain(wh.network, msg.destinationDomain); const details = { from: { chain: sendChain, address: xferSender }, to: { chain: rcvChain, address: xferReceiver }, amount: burnMessage.amount, automatic: false, }; const xfer = new CircleTransfer(wh, details); xfer.attestations = [{ id: { hash }, attestation: { message: msg } }]; xfer._state = TransferState.SourceInitiated; return xfer; } // init from source tx hash static async fromTransaction(wh, from, timeout, fromChain) { const { chain, txid } = from; fromChain = fromChain ?? wh.getChain(chain); // First try to parse out a WormholeMessage // If we get one or more, we assume its a Wormhole attested // transfer let msgIds = []; try { msgIds = await fromChain.parseTransaction(txid); } catch (e) { if (e.message.includes("no bridge messages found") || e.message.includes("not found")) { // This means it's a Circle attestation; swallow } else { throw e; } } // If we found a VAA message, use it let ct; if (msgIds.length > 0) { ct = await CircleTransfer.fromWormholeMessageId(wh, msgIds[0], timeout); } else { // Otherwise try to parse out a circle message const cb = await fromChain.getCircleBridge(); const circleMessage = await cb.parseTransactionDetails(txid); const details = { ...circleMessage, // Note: assuming automatic is false since we didn't find a VAA automatic: false, }; ct = new CircleTransfer(wh, details); ct.attestations = [{ id: circleMessage.id, attestation: { message: circleMessage.message } }]; } ct._state = TransferState.SourceInitiated; ct.txids = [from]; return ct; } // start the WormholeTransfer by submitting transactions to the source chain // returns a transaction hash async initiateTransfer(signer) { if (this._state !== TransferState.Created) throw new Error("Invalid state transition in `start`"); this.txids = await CircleTransfer.transfer(this.fromChain, this.transfer, signer); this._state = TransferState.SourceInitiated; return this.txids.map(({ txid }) => txid); } async _fetchWormholeAttestation(timeout) { let attestations = (this.attestations ?? []); if (!attestations || attestations.length == 0) throw new Error("No VAA details available"); // Check if we already have the VAA 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 attestations.map((v) => v.id); } async _fetchCircleAttestation(timeout) { let attestations = (this.attestations ?? []); 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) throw new Error("No circle attestations or transactions to fetch"); // The last tx should be the circle transfer, its possible there was // a contract spend approval transaction const txid = this.txids[this.txids?.length - 1]; const fromChain = this.wh.getChain(this.transfer.from.chain); const cb = await fromChain.getCircleBridge(); const circleMessage = await cb.parseTransactionDetails(txid.txid); attestations = [{ id: circleMessage.id, attestation: { message: circleMessage.message } }]; } 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"); attestations[idx].attestation.attestation = attestation; } this.attestations = attestations; return attestations.map((v) => v.id); } // wait for the VAA to be ready // returns the sequence number async fetchAttestation(timeout) { if (this._state < TransferState.SourceInitiated) throw new Error("Invalid state transition in `fetchAttestation`"); const ids = this.transfer.automatic ? (await Promise.all([ this._fetchWormholeAttestation(timeout), this._fetchCircleAttestation(timeout), ])).flat() : await this._fetchCircleAttestation(timeout); this._state = TransferState.Attested; if (this.attestations && this.attestations.length > 0) { for (const _attestation of this.attestations) { const { attestation } = _attestation; if (!CircleBridge.isCircleAttestation(attestation)) continue; const completed = await CircleTransfer.isTransferComplete(this.toChain, attestation); if (completed) this._state = TransferState.DestinationFinalized; } } return ids; } // finish the WormholeTransfer by submitting transactions to the destination chain // returns a transaction hash async completeTransfer(signer) { if (this._state < TransferState.Attested) throw new Error("Invalid state transition in `finish`"); // If its automatic, this does not need to be called if (this.transfer.automatic) { if (!this.attestations) throw new Error("No VAA details available"); const vaa = this.attestations.find((a) => isWormholeMessageId(a.id)); if (!vaa) throw new Error("No VAA found"); throw new Error("No method to redeem auto circle bridge tx (yet)"); } if (!this.attestations) throw new Error("No Circle Attestations found"); const circleAttestations = this.attestations.filter((a) => isCircleMessageId(a.id)); if (circleAttestations.length > 1) throw new Error(`Expected a single circle attestation, found ${circleAttestations.length}`); const { id, attestation } = circleAttestations[0]; if (!attestation) throw new Error(`No Circle Attestation for ${id.hash}`); const { message, attestation: signatures } = attestation; if (!signatures) throw new Error(`No Circle Attestation for ${id.hash}`); const tb = await this.toChain.getCircleBridge(); const sender = Wormhole.parseAddress(signer.chain(), signer.address()); const xfer = tb.redeem(sender, message, signatures); const txids = await signSendWait(this.toChain, xfer, signer); this.txids?.push(...txids); return txids.map(({ txid }) => txid); } } (function (CircleTransfer) { async function transfer(fromChain, transfer, signer) { let xfer; if (transfer.automatic) { const cr = await fromChain.getAutomaticCircleBridge(); xfer = cr.transfer(transfer.from.address, { chain: transfer.to.chain, address: transfer.to.address }, transfer.amount, transfer.nativeGas); } else { const cb = await fromChain.getCircleBridge(); xfer = cb.transfer(transfer.from.address, { chain: transfer.to.chain, address: transfer.to.address }, transfer.amount); } return await signSendWait(fromChain, xfer, signer); } CircleTransfer.transfer = transfer; // 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 async function* track(wh, receipt, timeout = DEFAULT_TASK_TIMEOUT, // Optional parameters to override chain context (typically for custom rpc) _fromChain, _toChain) { const start = Date.now(); const leftover = (start, max) => 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 (isSourceInitiated(receipt)) { if (receipt.originTxs.length === 0) throw "Invalid state transition: no originating transactions"; const initTx = receipt.originTxs[receipt.originTxs.length - 1]; const msg = await CircleTransfer.getTransferMessage(_fromChain, initTx.txid); receipt = { ...receipt, attestation: { id: msg.id, attestation: { message: msg.message } }, state: TransferState.SourceFinalized, }; yield receipt; } if (isSourceFinalized(receipt)) { if (!receipt.attestation) throw "Invalid state transition: no attestation id"; if (isWormholeMessageId(receipt.attestation.id)) { // Automatic tx // 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, leftover(start, timeout)); receipt = { ...receipt, attestation: { id: receipt.attestation.id, attestation: vaa }, state: TransferState.Attested, }; yield receipt; } } else if (isCircleMessageId(receipt.attestation.id)) { // Manual tx const attestation = await wh.getCircleAttestation(receipt.attestation.id.hash, timeout); const initTx = receipt.originTxs[receipt.originTxs.length - 1]; const cb = await _fromChain.getCircleBridge(); const message = await cb.parseTransactionDetails(initTx.txid); if (attestation) { receipt = { ...receipt, attestation: { id: receipt.attestation.id, attestation: { attestation, message: message.message, }, }, state: TransferState.Attested, }; 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 (isAttested(receipt) || isSourceFinalized(receipt)) { if (!receipt.attestation) throw "Invalid state transition"; if (isWormholeMessageId(receipt.attestation.id)) { const txStatus = await wh.getTransactionStatus(receipt.attestation.id, leftover(start, timeout)); if (txStatus && txStatus.globalTx?.destinationTx?.txHash) { const { chainId, txHash } = txStatus.globalTx.destinationTx; receipt = { ...receipt, destinationTxs: [{ chain: toChain(chainId), txid: txHash }], state: TransferState.DestinationInitiated, }; yield receipt; } } } // Fall back to asking the destination chain if this VAA has been redeemed // assuming we have the full attestation if (isAttested(receipt) || isRedeemed(receipt)) { const isComplete = await CircleTransfer.isTransferComplete(_toChain, receipt.attestation.attestation); if (isComplete) { receipt = { ...receipt, state: TransferState.DestinationFinalized, destinationTxs: [], }; } yield receipt; } } CircleTransfer.track = track; async function destinationOverrides(srcChain, dstChain, transfer) { const _transfer = { ...transfer }; if (chainToPlatform(dstChain.chain) === "Solana" && !_transfer.automatic) { const usdcAddress = Wormhole.parseAddress(dstChain.chain, circle.usdcContract.get(dstChain.network, dstChain.chain)); _transfer.to = await dstChain.getTokenAccount(_transfer.to.address, usdcAddress); } return _transfer; } CircleTransfer.destinationOverrides = destinationOverrides; async function quoteTransfer(srcChain, dstChain, transfer) { if (!circle.isCircleChain(dstChain.network, dstChain.chain)) throw new Error(`Invalid destination chain ${dstChain.chain} for Circle transfer`); const dstUsdcAddress = circle.usdcContract.get(dstChain.network, dstChain.chain); if (!dstUsdcAddress) throw "Invalid transfer, no USDC contract on destination"; if (!circle.isCircleChain(srcChain.network, srcChain.chain)) throw new Error(`Invalid source chain ${srcChain.chain} for Circle transfer`); const srcUsdcAddress = circle.usdcContract.get(srcChain.network, srcChain.chain); if (!srcUsdcAddress) throw "Invalid transfer, no USDC contract on source"; const dstToken = Wormhole.chainAddress(dstChain.chain, dstUsdcAddress); const srcToken = Wormhole.chainAddress(srcChain.chain, srcUsdcAddress); // https://developers.circle.com/stablecoins/docs/required-block-confirmations const eta = (srcChain.chain === "Polygon" ? 2_000 * 200 : finality.estimateFinalityTime(srcChain.chain)) + guardians.guardianAttestationEta; const expires = transfer.automatic ? time.expiration(0, 5, 0) // 5 minutes for automatic transfers : time.expiration(24, 0, 0); // 24 hours for manual if (!transfer.automatic) { return { sourceToken: { token: srcToken, amount: transfer.amount }, destinationToken: { token: dstToken, amount: transfer.amount }, eta, expires, }; } // Otherwise automatic let dstAmount = transfer.amount; // If a native gas dropoff is requested, remove that from the amount they'll get const _nativeGas = transfer.nativeGas ? transfer.nativeGas : 0n; dstAmount -= _nativeGas; // The fee is also removed from the amount transferred // quoted on the source chain const scb = await srcChain.getAutomaticCircleBridge(); const fee = await scb.getRelayerFee(dstChain.chain); dstAmount -= fee; // The expected destination gas can be pulled from the destination token bridge let destinationNativeGas = 0n; if (transfer.nativeGas) { const dcb = await dstChain.getAutomaticCircleBridge(); destinationNativeGas = await dcb.nativeTokenAmount(_nativeGas); } return { sourceToken: { token: srcToken, amount: transfer.amount, }, destinationToken: { token: dstToken, amount: dstAmount }, relayFee: { token: srcToken, amount: fee }, destinationNativeGas, eta, expires, }; } CircleTransfer.quoteTransfer = quoteTransfer; async function isTransferComplete(toChain, attestation) { if (!CircleBridge.isCircleAttestation(attestation)) throw new Error("Must check for completion with circle message"); const cb = await toChain.getCircleBridge(); return await cb.isTransferCompleted(attestation.message); } CircleTransfer.isTransferComplete = isTransferComplete; async function getTransferVaa(wh, wormholeMessageId, timeout) { const vaa = await wh.getVaa(wormholeMessageId, "AutomaticCircleBridge:TransferWithRelay", timeout); if (!vaa) throw new Error(`No VAA available after timeout exhausted`); return vaa; } CircleTransfer.getTransferVaa = getTransferVaa; async function getTransferMessage(fromChain, txid) { const cb = await fromChain.getCircleBridge(); const circleMessage = await cb.parseTransactionDetails(txid); return circleMessage; } CircleTransfer.getTransferMessage = getTransferMessage; function getReceipt(xfer) { const { from, to } = xfer.transfer; // This attestation may be either the auto relay vaa or the circle attestation // depending on the request let receipt = { 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)) ?? []; const ctt = xfer.attestations?.filter((a) => isCircleMessageId(a.id)) ?? []; const attestation = ctt.length > 0 ? ctt[0] : att.length > 0 ? att[0] : undefined; if (attestation) { if (attestation.id) { receipt = { ...receipt, state: TransferState.SourceFinalized, attestation: attestation, }; if (attestation.attestation) { receipt = { ...receipt, state: TransferState.Attested, attestation: { id: attestation.id, attestation: attestation.attestation }, }; } } } const destinationTxs = xfer.txids.filter((txid) => txid.chain === xfer.transfer.to.chain); if (destinationTxs.length > 0) { receipt = { ...receipt, state: TransferState.DestinationInitiated, destinationTxs, }; } return receipt; } CircleTransfer.getReceipt = getReceipt; })(CircleTransfer || (CircleTransfer = {})); //# sourceMappingURL=cctpTransfer.js.map