@wormhole-foundation/sdk-evm-portico
Version:
SDK for EVM chains, used in conjunction with @wormhole-foundation/sdk
188 lines • 9.26 kB
JavaScript
import { PorticoBridge, Wormhole, canonicalAddress, isEqualCaseInsensitive, nativeChainIds, resolveWrappedToken, serialize, toChain, toChainId, } from '@wormhole-foundation/sdk-connect';
import { EvmAddress, EvmPlatform, EvmUnsignedTransaction, addChainId, addFrom, } from '@wormhole-foundation/sdk-evm';
import { ethers, keccak256 } from 'ethers';
import { porticoAbi, uniswapQuoterV2Abi } from './abis.js';
import { PorticoApi } from './api.js';
import { FEE_TIER, supportedTokens } from './consts.js';
import { EvmWormholeCore } from '@wormhole-foundation/sdk-evm-core';
import { EvmTokenBridge } from '@wormhole-foundation/sdk-evm-tokenbridge';
import '@wormhole-foundation/sdk-evm-tokenbridge';
export class EvmPorticoBridge {
network;
chain;
provider;
contracts;
chainId;
core;
tokenBridge;
constructor(network, chain, provider, contracts) {
this.network = network;
this.chain = chain;
this.provider = provider;
this.contracts = contracts;
if (!contracts.portico)
throw new Error('Unsupported chain, no contract addresses for: ' + chain);
this.core = new EvmWormholeCore(network, chain, provider, contracts);
this.tokenBridge = new EvmTokenBridge(network, chain, provider, contracts);
this.chainId = nativeChainIds.networkChainToNativeChainId.get(network, chain);
}
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 EvmPorticoBridge(network, chain, provider, conf.contracts);
}
async *transfer(sender, receiver, token, amount, destToken, destinationPorticoAddress, quote) {
const { minAmountStart, minAmountFinish } = quote.swapAmounts;
if (minAmountStart === 0n)
throw new Error('Invalid min swap amount');
if (minAmountFinish === 0n)
throw new Error('Invalid min swap amount');
const senderAddress = new EvmAddress(sender).toString();
const [isStartTokenNative, startToken] = resolveWrappedToken(this.network, this.chain, token);
const [isFinalTokenNative, finalToken] = resolveWrappedToken(this.network, receiver.chain, destToken);
const startTokenAddress = canonicalAddress(startToken);
const cannonTokenAddress = canonicalAddress(await this.getTransferrableToken(startTokenAddress));
const receiverAddress = canonicalAddress(receiver);
const finalTokenAddress = canonicalAddress(finalToken);
const nonce = new Date().valueOf() % 2 ** 4;
const flags = PorticoBridge.serializeFlagSet({
flags: {
shouldWrapNative: isStartTokenNative,
shouldUnwrapNative: isFinalTokenNative,
},
recipientChain: toChainId(receiver.chain),
bridgeNonce: nonce,
feeTierStart: FEE_TIER,
feeTierFinish: FEE_TIER,
padding: new Uint8Array(19),
});
const transactionData = porticoAbi.encodeFunctionData('start', [
[
flags,
startTokenAddress.toLowerCase(),
cannonTokenAddress,
finalTokenAddress.toLowerCase(),
receiverAddress,
destinationPorticoAddress,
amount.toString(),
minAmountStart.toString(),
minAmountFinish.toString(),
quote.relayerFee.toString(),
],
]);
const group = this.getTokenGroup(startToken.address.toString());
const porticoAddress = this.getPorticoAddress(group);
// Approve the token if necessary
if (!isStartTokenNative)
yield* this.approve(startTokenAddress, senderAddress, amount, porticoAddress);
const messageFee = await this.core.getMessageFee();
const tx = {
to: porticoAddress,
data: transactionData,
value: messageFee + (isStartTokenNative ? amount : 0n),
};
yield this.createUnsignedTransaction(addFrom(tx, senderAddress), 'PorticoBridge.Transfer');
}
async *redeem(sender, vaa) {
const recipientChain = toChain(vaa.payload.payload.flagSet.recipientChain);
const tokenAddress = vaa.payload.payload.finalTokenAddress
.toNative(recipientChain)
.toString();
const group = this.getTokenGroup(tokenAddress);
const porticoAddress = this.getPorticoAddress(group);
const contract = new ethers.Contract(porticoAddress, porticoAbi.fragments, this.provider);
const txReq = await contract
.getFunction('receiveMessageAndSwap')
.populateTransaction(serialize(vaa));
const address = new EvmAddress(sender).toString();
yield this.createUnsignedTransaction(addFrom(txReq, address), 'PorticoBridge.Redeem');
}
async isTransferCompleted(vaa) {
const isCompleted = await this.tokenBridge.tokenBridge.isTransferCompleted(keccak256(vaa.hash));
return isCompleted;
}
async quoteSwap(input, output, tokenGroup, amount) {
const [, inputTokenId] = resolveWrappedToken(this.network, this.chain, input);
const [, outputTokenId] = resolveWrappedToken(this.network, this.chain, output);
const inputAddress = canonicalAddress(inputTokenId);
const outputAddress = canonicalAddress(outputTokenId);
if (isEqualCaseInsensitive(inputAddress, outputAddress))
return amount;
const quoterAddress = this.getQuoterAddress(tokenGroup);
const quoter = new ethers.Contract(quoterAddress, uniswapQuoterV2Abi.fragments, this.provider);
const result = await quoter
.getFunction('quoteExactInputSingle')
.staticCall([inputAddress, outputAddress, amount, FEE_TIER, 0]);
return result[0];
}
async quoteRelay(startToken, endToken) {
return await PorticoApi.quoteRelayer(this.chain, startToken, endToken);
}
// For a given token, return the Wormhole-wrapped/highway token
// that actually gets bridged from this chain
async getTransferrableToken(address) {
const token = Wormhole.tokenId(this.chain, address);
const [, wrappedToken] = resolveWrappedToken(this.network, this.chain, token);
if (this.chain === 'Ethereum')
return wrappedToken;
// Find the group that this token belongs to
const group = Object.values(supportedTokens).find((tokens) => tokens.find((t) => t.chain === this.chain &&
canonicalAddress(t) === canonicalAddress(wrappedToken)));
if (!group)
throw new Error(`No token group found for ${address} on ${this.chain}`);
// Find the token in this group that originates on Ethereum
const tokenOnEthereum = group.find((t) => t.chain === 'Ethereum');
if (!tokenOnEthereum)
throw new Error(`No Ethereum origin token found for ${address} on ${this.chain}`);
// Now find the corresponding Wormhole-wrapped/highway token on this chain
const highwayTokenAddr = await this.tokenBridge.getWrappedAsset(tokenOnEthereum);
return Wormhole.tokenId(this.chain, highwayTokenAddr.toString());
}
supportedTokens() {
const result = [];
for (const [group, tokens] of Object.entries(supportedTokens)) {
for (const token of tokens) {
if (token.chain === this.chain)
result.push({ group, token });
}
}
return result;
}
getTokenGroup(address) {
const tokens = this.supportedTokens();
const token = tokens.find((t) => canonicalAddress(t.token) === address);
if (!token)
throw new Error('Token not found');
return token.group;
}
async *approve(token, senderAddr, amount, contract) {
const tokenContract = EvmPlatform.getTokenImplementation(this.provider, token);
const allowance = await tokenContract.allowance(senderAddr, contract);
if (allowance < amount) {
const txReq = await tokenContract.approve.populateTransaction(contract, amount);
yield this.createUnsignedTransaction(addFrom(txReq, senderAddr), 'PorticoBridge.Approve');
}
}
createUnsignedTransaction(txReq, description) {
return new EvmUnsignedTransaction(addChainId(txReq, this.chainId), this.network, this.chain, description, false);
}
getPorticoAddress(group) {
const portico = this.contracts.portico;
if (group === 'USDT') {
// Use PancakeSwap if available for USDT
return portico.porticoPancakeSwap || portico.porticoUniswap;
}
return portico.porticoUniswap;
}
getQuoterAddress(group) {
const portico = this.contracts.portico;
if (group === 'USDT') {
// Use PancakeSwap if available for USDT
return portico.pancakeSwapQuoterV2 || portico.uniswapQuoterV2;
}
return portico.uniswapQuoterV2;
}
}
//# sourceMappingURL=bridge.js.map