Wormhole TS SDK
The Wormhole Typescript SDK is useful for interacting with the chains Wormhole supports and the protocols built on top of Wormhole.
Warning
:warning: This package is a Work in Progress so the interface may change and there are likely bugs. Please report any issues you find. :warning:
Installation
Basic
Install the (meta) package
npm install @wormhole-foundation/sdk
This package combines all the individual packages in a way that makes setup easier while still allowing for tree shaking.
Advanced
Alternatively, for an advanced user, install a specific set of the packages published.
npm install @wormhole-foundation/sdk-base
npm install @wormhole-foundation/sdk-definitions
npm install @wormhole-foundation/sdk-evm
npm install @wormhole-foundation/sdk-evm-tokenbridge
Usage
Getting started is simple, just import Wormhole and the Platform modules you wish to support
import { wormhole } from "@wormhole-foundation/sdk";
import algorand from "@wormhole-foundation/sdk/algorand";
import cosmwasm from "@wormhole-foundation/sdk/cosmwasm";
import evm from "@wormhole-foundation/sdk/evm";
import solana from "@wormhole-foundation/sdk/solana";
import sui from "@wormhole-foundation/sdk/sui";
See example here
And pass those to the Wormhole constructor to make them available for use
const wh = await wormhole("Testnet", [evm, solana, algorand, sui, cosmwasm]);
See example here
With a configured Wormhole object, we have the ability to do things like; parse addresses for the platforms we passed, get a ChainContext object, or fetch VAAs.
const ctx = wh.getChain("Solana");
See example here
const vaa = await wh.getVaa(
whm!,
"TokenBridge:Transfer",
60_000,
);
See example here
Optionally, the default configuration may be overriden in the case that you want to support, eg a different RPC endpoint.
const wh = await wormhole("Testnet", [solana], {
chains: {
Solana: {
contracts: {
coreBridge: "11111111111111111111111111111",
},
rpc: "https://api.devnet.solana.com",
},
},
});
See example here
Concepts
Understanding several higher level concepts of the SDK will help in using it effectively.
Platforms
Every chain is its own special snowflake but many of them share similar functionality. The Platform
modules provide a consistent interface for interacting with the chains that share a platform.
Each platform can be installed separately so that dependencies can stay as slim as possible.
Chain Context
The Wormhole
class provides a getChain
method that returns a ChainContext
object for a given chain. This object provides access to the chain specific methods and utilities. Much of the functionality in the ChainContext
is provided by the Platform
methods but the specific chain may have overridden methods.
The ChainContext object is also responsible for holding a cached rpc client and protocol clients.
const srcChain = wh.getChain(senderAddress.chain);
const dstChain = wh.getChain(receiverAddress.chain);
const tb = await srcChain.getTokenBridge();
srcChain.getRpcClient();
Addresses
Within the Wormhole context, addresses are often normalized to 32 bytes and referred to in this SDK as a UniversalAddresses
.
Each platform comes with an address type that understands the native address formats, unsurprisingly referred to as NativeAddress. This abstraction allows the SDK to work with addresses in a consistent way regardless of the underlying chain.
const ethAddr: NativeAddress<"Evm"> = toNative("Ethereum", "0xbeef...");
const senderAddress: ChainAddress = Wormhole.chainAddress("Ethereum","0xbeef...");
const receiverAddress: ChainAddress = Wormhole.chainAddress("Solana","Sol1111...");
const strAddress = Wormhole.canonicalAddress(senderAddress);
const emitterAddr = ethAddr.toUniversalAddress().toString()
Tokens
Similar to the ChainAddress
type, the TokenId
type provides the Chain and Address of a given Token.
const sourceToken: TokenId = Wormhole.tokenId("Ethereum","0xbeef...");
const gasToken: TokenId = Wormhole.tokenId("Ethereum","native");
const strAddress = Wormhole.canonicalAddress(senderAddress);
Signers
In order to sign transactions, an object that fulfils the Signer
interface is required. This is a simple interface that can be implemented by wrapping a web wallet or other signing mechanism.
export type Signer = SignOnlySigner | SignAndSendSigner;
export interface SignOnlySigner {
chain(): ChainName;
address(): string;
sign(tx: UnsignedTransaction[]): Promise<SignedTx[]>;
}
export interface SignAndSendSigner {
chain(): ChainName;
address(): string;
signAndSend(tx: UnsignedTransaction[]): Promise<TxHash[]>;
}
See the testing signers (Evm, Solana, ...) for an example of how to implement a signer for a specific chain or platform.
Protocols
While Wormhole itself is a Generic Message Passing protocol, a number of protocols have been built on top of it to provide specific functionality.
Each Protocol, if available, will have a Platform specific implementation. These implementations provide methods to generate transactions or read state from the contract on-chain.
Wormhole Core
The protocol that underlies all Wormhole activity is the Core protocol. This protocol is responsible for emitting the message containing the information necessary to perform bridging including Emitter address, the Sequence number for the message and the Payload of the message itself.
const wh = await wormhole("Testnet", [solana]);
const chain = wh.getChain("Solana");
const { signer, address } = await getSigner(chain);
const coreBridge = await chain.getWormholeCore();
const publishTxs = coreBridge.publishMessage(
address.address,
encoding.bytes.encode("lol"),
0,
0,
);
const txids = await signSendWait(chain, publishTxs, signer);
const txid = txids[txids.length - 1];
const [whm] = await chain.parseTransaction(txid!.txid);
const vaa = await wh.getVaa(whm!, "Uint8Array", 60_000);
console.log(vaa);
const verifyTxs = coreBridge.verifyMessage(address.address, vaa!);
console.log(await signSendWait(chain, verifyTxs, signer));
See example here
Within the payload is the information necessary to perform whatever action is required based on the Protocol that uses it.
Token Bridge
The most familiar protocol built on Wormhole is the Token Bridge.
Every chain has a TokenBridge
protocol client that provides a consistent interface for interacting with the Token Bridge. This includes methods to generate the transactions required to transfer tokens, as well as methods to generate and redeem attestations.
Using the WormholeTransfer
abstractions is the recommended way to interact with these protocols but it is possible to use them directly
import { signSendWait } from "@wormhole-foundation/sdk";
const tb = await srcChain.getTokenBridge();
const token = "0xdeadbeef...";
const txGenerator = tb.createAttestation(token);
const txids = await signSendWait(srcChain, txGenerator, src.signer);
Supported protocols are defined in the definitions module.
Transfers
While using the ChainContext and Protocol clients directly is possible, to do things like transfer tokens, the SDK provides some helpful abstractions.
The WormholeTransfer
interface provides a convenient abstraction to encapsulate the steps involved in a cross-chain transfer.
Token Transfers
Performing a Token Transfer is trivial for any source and destination chains.
We can create a new Wormhole
object and use it to to create TokenTransfer
, CircleTransfer
, GatewayTransfer
, etc. objects to transfer tokens between chains. The transfer object is responsible for tracking the transfer through the process and providing updates on its status.
const xfer = await wh.tokenTransfer(
route.token,
route.amount,
route.source.address,
route.destination.address,
route.delivery?.automatic ?? false,
route.payload,
route.delivery?.nativeGas,
);
const quote = await TokenTransfer.quoteTransfer(
wh,
route.source.chain,
route.destination.chain,
xfer.transfer,
);
console.log(quote);
if (xfer.transfer.automatic && quote.destinationToken.amount < 0)
throw "The amount requested is too low to cover the fee and any native gas requested.";
console.log("Starting transfer");
const srcTxids = await xfer.initiateTransfer(route.source.signer);
console.log(`Started transfer: `, srcTxids);
if (route.delivery?.automatic) return xfer;
console.log("Getting Attestation");
const attestIds = await xfer.fetchAttestation(60_000);
console.log(`Got Attestation: `, attestIds);
console.log("Completing Transfer");
const destTxids = await xfer.completeTransfer(route.destination.signer);
console.log(`Completed Transfer: `, destTxids);
See example here
Internally, this uses the TokenBridge protocol client to transfer tokens. The TokenBridge
protocol, like other Protocols, provides a consistent set of methods across all chains to generate a set of transactions for that specific chain.
Native USDC Transfers
We can also transfer native USDC using Circle's CCTP
const xfer = await wh.circleTransfer(
req.amount,
src.address,
dst.address,
req.automatic,
undefined,
req.nativeGas,
);
const quote = await CircleTransfer.quoteTransfer(src.chain, dst.chain, xfer.transfer);
console.log("Quote", quote);
console.log("Starting Transfer");
const srcTxids = await xfer.initiateTransfer(src.signer);
console.log(`Started Transfer: `, srcTxids);
console.log("Waiting for Attestation");
const attestIds = await xfer.fetchAttestation(60_000);
console.log(`Got Attestation: `, attestIds);
console.log("Completing Transfer");
const dstTxids = await xfer.completeTransfer(dst.signer);
console.log(`Completed Transfer: `, dstTxids);
See example here
Gateway Transfers
Gateway transfers are transfers that are passed through the Wormhole Gateway to or from Cosmos chains.
A transfer into Cosmos from outside cosmos will be automatically delivered to the destination via IBC from the Gateway chain (fka Wormchain)
console.log(
`Beginning transfer into Cosmos from ${src.chain.chain}:${src.address.address.toString()} to ${
dst.chain.chain
}:${dst.address.address.toString()}`,
);
const xfer = await GatewayTransfer.from(wh, {
token: token,
amount: amount,
from: src.address,
to: dst.address,
} as GatewayTransferDetails);
console.log("Created GatewayTransfer: ", xfer.transfer);
const srcTxIds = await xfer.initiateTransfer(src.signer);
console.log("Started transfer on source chain", srcTxIds);
const attests = await xfer.fetchAttestation(600_000);
console.log("Got Attestations", attests);
See example here
A transfer within Cosmos will use IBC to transfer from the origin to the Gateway chain, then out from the Gateway to the destination chain
console.log(
`Beginning transfer within cosmos from ${
src.chain.chain
}:${src.address.address.toString()} to ${dst.chain.chain}:${dst.address.address.toString()}`,
);
const xfer = await GatewayTransfer.from(wh, {
token: token,
amount: amount,
from: src.address,
to: dst.address,
} as GatewayTransferDetails);
console.log("Created GatewayTransfer: ", xfer.transfer);
const srcTxIds = await xfer.initiateTransfer(src.signer);
console.log("Started transfer on source chain", srcTxIds);
const attests = await xfer.fetchAttestation(60_000);
console.log("Got attests: ", attests);
See example here
A transfer leaving Cosmos will produce a VAA from the Gateway that must be manually redeemed on the destination chain
console.log(
`Beginning transfer out of cosmos from ${
src.chain.chain
}:${src.address.address.toString()} to ${dst.chain.chain}:${dst.address.address.toString()}`,
);
const xfer = await GatewayTransfer.from(wh, {
token: token,
amount: amount,
from: src.address,
to: dst.address,
} as GatewayTransferDetails);
console.log("Created GatewayTransfer: ", xfer.transfer);
const srcTxIds = await xfer.initiateTransfer(src.signer);
console.log("Started transfer on source chain", srcTxIds);
const attests = await xfer.fetchAttestation(600_000);
console.log("Got attests", attests);
const dstTxIds = await xfer.completeTransfer(dst.signer);
console.log("Completed transfer on destination chain", dstTxIds);
See example here
Recovering Transfers
It may be necessary to recover a transfer that was abandoned before being completed. This can be done by instantiating the Transfer class with the from
static method and passing one of several types of identifiers.
A TransactionId
or WormholeMessageId
may be used to recover the transfer
const xfer = await CircleTransfer.from(wh, txid);
const attestIds = await xfer.fetchAttestation(60 * 60 * 1000);
console.log("Got attestation: ", attestIds);
const dstTxIds = await xfer.completeTransfer(signer);
console.log("Completed transfer: ", dstTxIds);
See example here
Routes
While a specific WormholeTransfer
may be used (TokenTransfer, CCTPTransfer, ...), it requires the developer know exactly which transfer type to use for a given request.
To provide a more flexible and generic interface, the Wormhole
class provides a method to produce a RouteResolver
that can be configured with a set of possible routes to be supported.
const resolver = wh.resolver([
routes.TokenBridgeRoute,
routes.AutomaticTokenBridgeRoute,
routes.CCTPRoute,
routes.AutomaticCCTPRoute,
routes.AutomaticPorticoRoute,
]);
See example here
Once created, the resolver can be used to provide a list of input and possible output tokens.
const srcTokens = await resolver.supportedSourceTokens(sendChain);
console.log(
"Allowed source tokens: ",
srcTokens.map((t) => canonicalAddress(t)),
);
const destTokens = await resolver.supportedDestinationTokens(sendToken, sendChain, destChain);
console.log(
"For the given source token and routes configured, the following tokens may be receivable: ",
destTokens.map((t) => canonicalAddress(t)),
);
See example here
Once the tokens are selected, a RouteTransferRequest
may be created to provide a list of routes that can fulfil the request
const tr = await routes.RouteTransferRequest.create(wh, {
from: sender.address,
to: receiver.address,
source: sendToken,
destination: destTokens.pop()!,
});
const foundRoutes = await resolver.findRoutes(tr);
console.log("For the transfer parameters, we found these routes: ", foundRoutes);
See example here
Choosing the best route is currently left to the developer but strategies might include sorting by output amount or expected time to complete the transfer (no estimate currently provided).
After choosing the best route, extra parameters like amount
, nativeGasDropoff
, and slippage
can be passed, depending on the specific route selected and a quote can be retrieved with the validated request.
console.log("This route offers the following default options", bestRoute.getDefaultOptions());
const amt = "0.5";
const transferParams = { amount: amt, options: { nativeGas: 0.1 } };
const validated = await bestRoute.validate(transferParams);
if (!validated.valid) throw validated.error;
console.log("Validated parameters: ", validated.params);
const quote = await bestRoute.quote(validated.params);
if (!quote.success) throw quote.error;
console.log("Best route quote: ", quote);
See example here
Finally, assuming the quote looks good, the route can initiate the request with the quote and the signer
const receipt = await bestRoute.initiate(sender.signer, quote);
console.log("Initiated transfer with receipt: ", receipt);
See example here
Note: See the router.ts
example in the examples directory for a full working example
See also
The tsdoc is available here