@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
JavaScript
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