@wormhole-foundation/sdk-evm-tokenbridge
Version:
SDK for EVM chains, used in conjunction with @wormhole-foundation/sdk
167 lines • 8.95 kB
JavaScript
import { ErrNotWrapped, UniversalAddress, isNative, keccak256, nativeChainIds, serialize, toChain, toChainId, toNative, } from '@wormhole-foundation/sdk-connect';
import { ethers_contracts } from './index.js';
import { EvmAddress, EvmPlatform, EvmUnsignedTransaction, EvmZeroAddress, addChainId, addFrom, unusedArbiterFee, unusedNonce, WETH_CONTRACTS, } from '@wormhole-foundation/sdk-evm';
import '@wormhole-foundation/sdk-evm-core';
import { EvmWormholeCore } from '@wormhole-foundation/sdk-evm-core';
export class EvmTokenBridge {
network;
chain;
provider;
contracts;
tokenBridge;
core;
tokenBridgeAddress;
chainId;
constructor(network, chain, provider, contracts) {
this.network = network;
this.chain = chain;
this.provider = provider;
this.contracts = contracts;
this.chainId = nativeChainIds.networkChainToNativeChainId.get(network, chain);
const tokenBridgeAddress = this.contracts.tokenBridge;
if (!tokenBridgeAddress)
throw new Error(`Wormhole Token Bridge contract for domain ${chain} not found`);
this.tokenBridgeAddress = tokenBridgeAddress;
this.tokenBridge = ethers_contracts.Bridge__factory.connect(this.tokenBridgeAddress, provider);
this.core = new EvmWormholeCore(network, chain, provider, contracts);
}
static async fromRpc(provider, config) {
const [network, chain] = await EvmPlatform.chainFromRpc(provider);
const conf = config[chain];
if (conf.network !== network)
throw new Error(`Network mismatch: ${conf.network} != ${network}`);
return new EvmTokenBridge(network, chain, provider, conf.contracts);
}
async isWrappedAsset(token) {
return await this.tokenBridge.isWrappedAsset(token.toString());
}
async getOriginalAsset(token) {
if (!(await this.isWrappedAsset(token)))
throw ErrNotWrapped(token.toString());
const tokenContract = EvmPlatform.getTokenImplementation(this.provider, token.toString());
const [chain, address] = await Promise.all([
tokenContract.chainId().then(Number).then(toChainId).then(toChain),
tokenContract.nativeContract().then((addr) => new UniversalAddress(addr)),
]);
return { chain, address };
}
async getTokenUniversalAddress(token) {
return new EvmAddress(token).toUniversalAddress();
}
async getTokenNativeAddress(originChain, token) {
return new EvmAddress(token).toNative();
}
async hasWrappedAsset(token) {
try {
await this.getWrappedAsset(token);
return true;
}
catch (e) { }
return false;
}
async getWrappedAsset(token) {
if (isNative(token.address))
throw new Error('native asset cannot be a wrapped asset');
const wrappedAddress = await this.tokenBridge.wrappedAsset(toChainId(token.chain), token.address.toUniversalAddress().toString());
if (wrappedAddress === EvmZeroAddress)
throw ErrNotWrapped(token.address.toUniversalAddress().toString());
return new EvmAddress(wrappedAddress);
}
async isTransferCompleted(vaa) {
//The double keccak here is neccessary due to a fuckup in the original implementation of the
// EVM core bridge:
//Guardians don't sign messages (bodies) but explicitly hash them via keccak256 first.
//However, they use an ECDSA scheme for signing where the first step is to hash the "message"
// (which at this point is already the digest of the original message/body!)
//Now, on EVM, ecrecover expects the final digest (i.e. a bytes32 rather than a dynamic bytes)
// i.e. it does no hashing itself. Therefore the EVM core bridge has to hash the body twice
// before calling ecrecover. But in the process of doing so, it erroneously sets the doubly
// hashed value as vm.hash instead of using the only once hashed value.
//And finally this double digest is then used in a mapping to store whether a VAA has already
// been redeemed or not, which is ultimately the reason why we have to keccak the hash one
// more time here.
return this.tokenBridge.isTransferCompleted(keccak256(vaa.hash));
}
async *createAttestation(token) {
const messageFee = await this.core.getMessageFee();
const ignoredNonce = 0;
yield this.createUnsignedTx(await this.tokenBridge.attestToken.populateTransaction(token.toString(), ignoredNonce, {
value: messageFee,
}), 'TokenBridge.createAttestation');
}
async *submitAttestation(vaa) {
const func = (await this.hasWrappedAsset({
...vaa.payload.token,
}))
? 'updateWrapped'
: 'createWrapped';
yield this.createUnsignedTx(await this.tokenBridge[func].populateTransaction(serialize(vaa)), 'TokenBridge.' + func);
}
async *transfer(sender, recipient, token, amount, payload) {
const senderAddr = new EvmAddress(sender).toString();
const recipientChainId = toChainId(recipient.chain);
const recipientAddress = recipient.address
.toUniversalAddress()
.toUint8Array();
const messageFee = await this.core.getMessageFee();
if (isNative(token)) {
const txReq = await (payload === undefined
? this.tokenBridge.wrapAndTransferETH.populateTransaction(recipientChainId, recipientAddress, unusedArbiterFee, unusedNonce, { value: amount + messageFee })
: this.tokenBridge.wrapAndTransferETHWithPayload.populateTransaction(recipientChainId, recipientAddress, unusedNonce, payload, { value: amount + messageFee }));
yield this.createUnsignedTx(addFrom(txReq, senderAddr), 'TokenBridge.wrapAndTransferETH' +
(payload === undefined ? '' : 'WithPayload'));
}
else {
//TODO check for ERC-2612 (permit) support on token?
const tokenAddr = new EvmAddress(token).toString();
const tokenContract = EvmPlatform.getTokenImplementation(this.provider, tokenAddr);
const allowance = await tokenContract.allowance(senderAddr, this.tokenBridge.target);
if (allowance < amount) {
const txReq = await tokenContract.approve.populateTransaction(this.tokenBridge.target, amount);
yield this.createUnsignedTx(addFrom(txReq, senderAddr), 'TokenBridge.Approve');
}
const sharedParams = [
tokenAddr,
amount,
recipientChainId,
recipientAddress,
];
const txReq = await (payload === undefined
? this.tokenBridge.transferTokens.populateTransaction(...sharedParams, unusedArbiterFee, unusedNonce, { value: messageFee })
: this.tokenBridge.transferTokensWithPayload.populateTransaction(...sharedParams, unusedNonce, payload, { value: messageFee }));
yield this.createUnsignedTx(addFrom(txReq, senderAddr), 'TokenBridge.transferTokens' +
(payload === undefined ? '' : 'WithPayload'));
}
}
async *redeem(sender, vaa, unwrapNative = true) {
const senderAddr = new EvmAddress(sender).toString();
if (vaa.payloadName === 'TransferWithPayload' &&
vaa.payload.token.chain !== this.chain) {
const fromAddr = new EvmAddress(vaa.payload.from).unwrap();
if (fromAddr !== senderAddr)
throw new Error(`VAA.from (${fromAddr}) does not match sender (${senderAddr})`);
}
if (vaa.payload.token.chain === this.chain) {
const wrappedNativeAddr = await this.getWeth();
const tokenAddr = new EvmAddress(vaa.payload.token.address).unwrap();
if (tokenAddr === wrappedNativeAddr && unwrapNative) {
const txReq = await this.tokenBridge.completeTransferAndUnwrapETH.populateTransaction(serialize(vaa));
yield this.createUnsignedTx(addFrom(txReq, senderAddr), 'TokenBridge.completeTransferAndUnwrapETH');
return;
}
}
const txReq = await this.tokenBridge.completeTransfer.populateTransaction(serialize(vaa));
yield this.createUnsignedTx(addFrom(txReq, senderAddr), 'TokenBridge.completeTransfer');
}
async getWrappedNative() {
const address = await this.getWeth();
return toNative(this.chain, address);
}
async getWeth() {
return WETH_CONTRACTS[this.network]?.[this.chain] ?? this.tokenBridge.WETH();
}
createUnsignedTx(txReq, description, parallelizable = false) {
return new EvmUnsignedTransaction(addChainId(txReq, this.chainId), this.network, this.chain, description, parallelizable);
}
}
//# sourceMappingURL=tokenBridge.js.map