UNPKG

@wormhole-foundation/sdk-evm-portico

Version:

SDK for EVM chains, used in conjunction with @wormhole-foundation/sdk

188 lines 9.26 kB
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