@wormhole-foundation/sdk-connect
Version:
The core package for the Connect SDK, used in conjunction with 1 or more of the chain packages
399 lines • 21 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GatewayTransfer = void 0;
const sdk_base_1 = require("@wormhole-foundation/sdk-base");
const sdk_definitions_1 = require("@wormhole-foundation/sdk-definitions");
const common_js_1 = require("../../common.js");
const tasks_js_1 = require("../../tasks.js");
const types_js_1 = require("../../types.js");
const wormhole_js_1 = require("../../wormhole.js");
class GatewayTransfer {
static chain = "Wormchain";
wh;
// Wormchain context
gateway;
// Wormchain IBC Bridge
gatewayIbcBridge;
// Contract address
gatewayAddress;
// state machine tracker
_state;
// 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
msg;
// Initial Transfer Settings
transfer;
// Transaction Ids from source chain
transactions = [];
// The corresponding vaa representing the GatewayTransfer
// on the source chain (if it came from outside cosmos and if its been completed and finalized)
vaas;
// Any transfers we do over ibc
ibcTransfers = [];
constructor(wh, transfer, gateway, gatewayIbc) {
this._state = types_js_1.TransferState.Created;
this.wh = wh;
this.transfer = transfer;
// reference from conf instead of asking the module
// so we can prevent weird imports
this.gatewayAddress = {
chain: GatewayTransfer.chain,
address: (0, sdk_definitions_1.toNative)(GatewayTransfer.chain, this.wh.config.chains[GatewayTransfer.chain].contracts.gateway),
};
// cache the wormchain chain context since we need it for checks
this.gateway = gateway;
this.gatewayIbcBridge = gatewayIbc;
// cache the message since we don't want to regenerate it any time we need it
this.msg = (0, sdk_definitions_1.gatewayTransferMsg)(this.transfer);
}
getTransferState() {
return this._state;
}
static async from(wh, from, timeout) {
// we need this regardless of the type of `from`
const wc = wh.getChain(GatewayTransfer.chain);
const wcibc = await wc.getIbcBridge();
// Fresh new transfer
if ((0, sdk_definitions_1.isGatewayTransferDetails)(from)) {
const fromChain = wh.getChain(from.from.chain);
const toChain = wh.getChain(from.to.chain);
const overrides = await GatewayTransfer.destinationOverrides(fromChain, toChain, wc, from);
// Override transfer params if necessary
from = { ...from, ...overrides };
return new GatewayTransfer(wh, from, wc, wcibc);
}
// Picking up where we left off
let gtd;
let txns = [];
if ((0, sdk_definitions_1.isTransactionIdentifier)(from)) {
txns.push(from);
gtd = await GatewayTransfer._fromTransaction(wh, from);
}
else if ((0, sdk_definitions_1.isWormholeMessageId)(from)) {
// TODO: we're missing the transaction that created this
// get it from transaction status search on wormholescan?
gtd = await GatewayTransfer._fromMsgId(wh, from);
}
else {
throw new Error("Invalid `from` parameter for GatewayTransfer");
}
const gt = new GatewayTransfer(wh, gtd, wc, wcibc);
gt.transactions = txns;
// Since we're picking up from somewhere we can move the
// state maching to initiated
gt._state = types_js_1.TransferState.SourceInitiated;
// Wait for what _can_ complete to complete
await gt.fetchAttestation(timeout);
return gt;
}
// Recover Transfer info from VAA details
static async _fromMsgId(wh, from, timeout) {
// Starting with the VAA
const vaa = await GatewayTransfer.getTransferVaa(wh, from, timeout);
// The VAA may have a payload which may have a nested GatewayTransferMessage
let payload = vaa.payloadName === "TransferWithPayload" ? vaa.payload.payload : undefined;
// Nonce for GatewayTransferMessage may be in the payload
// and since we use the payload to find the Wormchain transacton
// we need to preserve it
let nonce;
let to = { ...vaa.payload.to };
// The payload here may be the message for Gateway
// Lets be sure to pull the real payload if its set
// Otherwise revert to undefined
if (payload) {
try {
const maybeWithPayload = (0, sdk_definitions_1.toGatewayMsg)(sdk_base_1.encoding.bytes.decode(payload));
nonce = maybeWithPayload.nonce;
payload = maybeWithPayload.payload
? sdk_base_1.encoding.bytes.encode(maybeWithPayload.payload)
: undefined;
const destChain = (0, sdk_base_1.toChain)(maybeWithPayload.chain);
// b64 decode the address to its string representation
const recipientAddress = sdk_base_1.encoding.bytes.decode(sdk_base_1.encoding.b64.decode(maybeWithPayload.recipient));
to = wormhole_js_1.Wormhole.chainAddress(destChain, recipientAddress);
}
catch {
/*Ignoring, throws if not the payload isnt JSON*/
}
}
const { chain, address, amount } = vaa.payload.token;
// Reconstruct the details
const details = {
token: { chain, address },
amount: amount,
// TODO: the `from.address` here is a lie, but we don't
// immediately have enough info to get the _correct_ one
from: { chain: from.chain, address: from.emitter },
to,
nonce,
payload: payload,
};
return details;
}
// Init from source tx hash, depending on the source chain
// we pull Transfer info from either IBC or a wh message
static async _fromTransaction(wh, from, timeout) {
const { chain, txid } = from;
const originChain = wh.getChain(chain);
// If its origin chain supports IBC, it should be an IBC message
if (originChain.supportsIbcBridge()) {
// Get the ibc tx info from the origin
const ibcBridge = await originChain.getIbcBridge();
const [xfer] = await ibcBridge.lookupTransferFromTx(from.txid);
return GatewayTransfer.ibcTransfertoGatewayTransfer(xfer);
}
// Otherwise grab the vaa details from the origin tx
const msgs = await wormhole_js_1.Wormhole.parseMessageFromTx(originChain, txid);
if (!msgs)
throw new Error("No messages found in transaction");
return await GatewayTransfer._fromMsgId(wh, msgs[0], timeout);
}
// Recover transfer info the first step in the transfer
static ibcTransfertoGatewayTransfer(xfer) {
const token = wormhole_js_1.Wormhole.tokenId(xfer.id.chain, xfer.data.denom);
const msg = (0, sdk_definitions_1.toGatewayMsg)(xfer.data.memo);
const destChain = (0, sdk_base_1.toChain)(msg.chain);
const _recip = sdk_base_1.encoding.b64.decode(msg.recipient);
const recipient = (0, sdk_base_1.chainToPlatform)(destChain) === "Cosmwasm"
? wormhole_js_1.Wormhole.chainAddress(destChain, sdk_base_1.encoding.bytes.decode(_recip))
: {
chain: destChain,
address: new sdk_definitions_1.UniversalAddress(_recip).toNative(destChain),
};
const payload = msg.payload ? sdk_base_1.encoding.bytes.encode(msg.payload) : undefined;
const details = {
token,
amount: BigInt(xfer.data.amount),
from: {
chain: xfer.id.chain,
address: (0, sdk_definitions_1.toNative)(xfer.id.chain, xfer.data.sender),
},
to: recipient,
fee: BigInt(msg.fee),
payload,
};
return details;
}
// start the WormholeTransfer by submitting transactions to the source chain
// returns a transaction hash
async initiateTransfer(signer) {
if (this._state !== types_js_1.TransferState.Created)
throw new Error("Invalid state transition in `start`");
this.transactions = await (this.fromGateway()
? this._transferIbc(signer)
: this._transfer(signer));
// Update State Machine
this._state = types_js_1.TransferState.SourceInitiated;
return this.transactions.map((tx) => tx.txid);
}
async _transfer(signer) {
const tokenAddress = this.transfer.token.address;
const fromChain = this.wh.getChain(this.transfer.from.chain);
// Build the message needed to send a transfer through the gateway
const tb = await fromChain.getTokenBridge();
const xfer = tb.transfer(this.transfer.from.address, this.gatewayAddress, tokenAddress, this.transfer.amount, sdk_base_1.encoding.bytes.encode(JSON.stringify(this.msg)));
return (0, common_js_1.signSendWait)(fromChain, xfer, signer);
}
async _transferIbc(signer) {
if ((0, sdk_definitions_1.isNative)(this.transfer.token.address))
throw new Error("Native not supported for IBC transfers");
const fromChain = this.wh.getChain(this.transfer.from.chain);
const ibcBridge = await fromChain.getIbcBridge();
const xfer = ibcBridge.transfer(this.transfer.from.address, this.transfer.to, this.transfer.token.address, this.transfer.amount);
return (0, common_js_1.signSendWait)(fromChain, xfer, signer);
}
// wait for the Attestations to be ready
async fetchAttestation(timeout) {
// Note: this method probably does too much
if (this._state < types_js_1.TransferState.SourceInitiated || this._state > types_js_1.TransferState.Attested)
throw new Error("Invalid state transition in `fetchAttestation`");
const 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
// and get the ibc bridge once
const originIbcbridge = await chain.getIbcBridge();
// Ultimately we need to find the corresponding Wormchain transaction
// from the intitiating cosmos chain, this will contain the details of the
// outbound transaction to the destination chain
// 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)))).flat();
// I don't know why this would happen so lmk if you see this
if (this.ibcTransfers.length != 1)
throw new Error("why?");
const [xfer] = this.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
const retryInterval = 5000;
const task = () => this.gatewayIbcBridge.lookupMessageFromIbcMsgId(xfer.id);
const whm = await (0, tasks_js_1.retry)(task, retryInterval, timeout, "Gateway:IbcBridge:LookupWormholeMessageFromIncomingIbcMessage");
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);
}
else {
// Otherwise we need to get the transfer on the destination chain
// using the transfer from wormchain
const gatewayTransferTask = () => (0, tasks_js_1.fetchIbcXfer)(this.gatewayIbcBridge, xfer.id);
const gatewayIbcTransfers = await (0, tasks_js_1.retry)(gatewayTransferTask, 5000, timeout, "Gateway:IbcBridge:LookupWormchainIbcTransfer");
if (gatewayIbcTransfers === null)
throw new Error("Gateway IBC transfer not found after retries exhausted");
const toDestChainIbcTransfer = gatewayIbcTransfers[1];
const dstChain = this.wh.getChain(this.transfer.to.chain);
const dstIbcBridge = await dstChain.getIbcBridge();
const dstMsgTask = () => (0, tasks_js_1.fetchIbcXfer)(dstIbcBridge, toDestChainIbcTransfer.id);
const dstIbcTransfers = await (0, tasks_js_1.retry)(dstMsgTask, 5000, timeout, "Gateway:IbcBridge:LookupDestinationIbcTransfer");
if (!dstIbcTransfers)
throw new Error("Destination IBC transfer not found after retries exhausted");
this.ibcTransfers.push(dstIbcTransfers[0]);
}
}
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 [whm] = await wormhole_js_1.Wormhole.parseMessageFromTx(chain, transferTransaction.txid);
const vaa = await GatewayTransfer.getTransferVaa(this.wh, whm);
this.vaas = [{ id: whm, vaa }];
attestations.push(whm);
// TODO: conf for these settings? how do we choose them?
const vaaRedeemedRetryInterval = 2000;
const transferCompleteInterval = 5000;
// Wait until the vaa is redeemed before trying to look up the
// transfer message
const wcTb = await this.gateway.getTokenBridge();
// Since we want to retry until its redeemed, return null
// in the case that its not redeemed
const isRedeemedTask = () => (0, tasks_js_1.isTokenBridgeVaaRedeemed)(wcTb, vaa);
const redeemed = await (0, tasks_js_1.retry)(isRedeemedTask, vaaRedeemedRetryInterval, timeout, "Gateway:TokenBridge:IsVaaRedeemed");
if (!redeemed)
throw new Error("VAA not redeemed after retries exhausted");
// Next, get the IBC transactions from wormchain
// Note: Because we search by GatewayTransferMsg payload
// there is a possibility of dupe messages being returned
// using a nonce should help
const wcTransferTask = () => (0, tasks_js_1.fetchIbcXfer)(this.gatewayIbcBridge, this.msg);
const wcTransfers = await (0, tasks_js_1.retry)(wcTransferTask, vaaRedeemedRetryInterval, timeout, "Gateway:IbcBridge:WormchainTransferInitiated");
if (!wcTransfers)
throw new Error("Wormchain transfer not found after retries exhausted");
const [wcTransfer] = wcTransfers;
if (wcTransfer.pending) {
// TODO: check if pending and bail(?) if so
}
this.ibcTransfers.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();
const destTransferTask = () => (0, tasks_js_1.fetchIbcXfer)(destIbcBridge, wcTransfer.id);
const destTransfer = await (0, tasks_js_1.retry)(destTransferTask, transferCompleteInterval, timeout, "Destination:IbcBridge:WormchainTransferCompleted");
if (!destTransfer)
throw new Error("IBC Transfer into destination not found after retries exhausted" +
JSON.stringify(wcTransfer));
this.ibcTransfers.push(destTransfer[0]);
}
// Add transfers to attestations we return
// Note: there is no ordering guarantee here
attestations.push(...this.ibcTransfers.map((xfer) => xfer.id));
this._state = types_js_1.TransferState.Attested;
return attestations;
}
// finish the WormholeTransfer by submitting transactions to the destination chain
// returns a transaction hash
async completeTransfer(signer) {
if (this._state < types_js_1.TransferState.Attested)
throw new Error("Invalid state transition in `finish`. Be sure to call `fetchAttestation`.");
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");
const toChain = this.wh.getChain(signer.chain());
const toAddress = (0, sdk_definitions_1.toNative)(signer.chain(), signer.address());
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 xfer = tb.redeem(toAddress, vaa);
const redeemTxs = await (0, common_js_1.signSendWait)(toChain, xfer, signer);
this.transactions.push(...redeemTxs);
this._state = types_js_1.TransferState.DestinationInitiated;
return redeemTxs.map(({ txid }) => txid);
}
// 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
fromGateway() {
return this.gatewayIbcBridge.getTransferChannel(this.transfer.from.chain) !== null;
}
toGateway() {
return this.gatewayIbcBridge.getTransferChannel(this.transfer.to.chain) !== null;
}
}
exports.GatewayTransfer = GatewayTransfer;
(function (GatewayTransfer) {
async function getTransferVaa(wh, whm, timeout) {
const vaa = await wh.getVaa(whm, sdk_definitions_1.TokenBridge.getTransferDiscriminator(), timeout);
if (!vaa)
throw new Error(`No VAA Available: ${whm.chain}/${whm.emitter}/${whm.sequence}`);
return vaa;
}
GatewayTransfer.getTransferVaa = getTransferVaa;
async function destinationOverrides(srcChain, dstChain, gatewayChain, transfer) {
const _transfer = { ...transfer };
// Bit of (temporary) hackery until solana contracts support being
// sent a VAA with the primary address
if ((0, sdk_base_1.chainToPlatform)(transfer.to.chain) === "Solana") {
const destinationToken = await GatewayTransfer.lookupDestinationToken(srcChain, dstChain, gatewayChain, _transfer.token);
_transfer.to = await dstChain.getTokenAccount(_transfer.to.address, destinationToken.address);
}
return _transfer;
}
GatewayTransfer.destinationOverrides = destinationOverrides;
// Lookup the token id for the destination chain given the source chain
// and token id
async function lookupDestinationToken(srcChain, dstChain, gatewayChain, token) {
// that will be minted when the transfer is redeemed
let lookup;
if ((0, sdk_definitions_1.isNative)(token.address)) {
// if native, get the wrapped asset id
lookup = await srcChain.getNativeWrappedTokenId();
}
else {
try {
// otherwise, check to see if it is a wrapped token locally
const tb = await srcChain.getTokenBridge();
lookup = await tb.getOriginalAsset(token.address);
}
catch (e) {
// not a from-chain native token, check the gateway chain
try {
const gtb = await gatewayChain.getTokenBridge();
lookup = await gtb.getOriginalAsset(token.address);
}
catch (e) {
lookup = token;
}
}
}
// if the token id is actually native to the destination, return it
if (lookup.chain === dstChain.chain) {
return lookup;
}
// otherwise, figure out what the token address representing the wormhole-wrapped token we're transferring
const dstTb = await dstChain.getTokenBridge();
const dstAddress = await dstTb.getWrappedAsset(lookup);
return { chain: dstChain.chain, address: dstAddress };
}
GatewayTransfer.lookupDestinationToken = lookupDestinationToken;
})(GatewayTransfer || (exports.GatewayTransfer = GatewayTransfer = {}));
//# sourceMappingURL=gatewayTransfer.js.map