/* IMPORTANT: keep imports minimal, only allowed to depend on: ethers, wh-sdk, web3js, raydium-sdk, generated solana-code (maybe remove this?), generated typechain lib files in "client_dependencies folder" */ import * as clientLib from "."; import { BigNumber, Contract, ContractReceipt, ethers, Signer } from "ethers"; import { dbg, info, _undef } from "./utilities"; import * as context from "./context"; import * as whHelpers from "./wh_helpers"; import * as wh from "@certusone/wormhole-sdk"; import { PublicKey } from "@solana/web3.js"; import erc20_abi from "./erc20_abi"; import { LogDescription } from "ethers/lib/utils"; import * as _spl from "@solana/spl-token"; const spl: any = _spl; // set this when running from browswer // wh.setDefaultWasm("bundler") export type ClientContext = context.Context< context.EvmContext, context.SolanaContextNoSigner >; export type SeqAndXTxId = { seq: string; xTxId: number }; const chainIdToWETH = { [wh.CHAIN_ID_AVAX]: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", [wh.CHAIN_ID_ETH]: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", }; export async function sendSolOnRampFromEth( ctx: ClientContext, { dstSolanaWallet, amountIn, minLamportsOut, }: { dstSolanaWallet: PublicKey; amountIn: BigNumber; minLamportsOut: BigNumber; }, ): Promise { const contract = clientLib.XRaydiumBridge__factory.connect( ctx.xRaydiumEvmAddr, ctx.evm.signer, ); // check solana mint exists before sending swap // todo: grab weth address from wormhole token bridge contract const weth = clientLib.XTokenLocatorExt.fromNative( chainIdToWETH[ctx.evm.chainId], ctx.evm.chainId, ); await throwIfSolanaMintNotFound(ctx.sol, weth); const fee = await calcFee(1, contract); const [solanaProxyPDA] = await getCustodySigner(ctx, ctx.evm.evmWalletAddr); const receipt = await contract .swapFromEth( clientLib.XTokenLocatorExt.fromNative( wh.WSOL_ADDRESS, wh.CHAIN_ID_SOLANA, ).toEvmFormat(), minLamportsOut, solanaProxyPDA.toBuffer(), dstSolanaWallet.toBuffer(), { value: amountIn.add(fee), gasLimit: 7600000 }, ) .then(x => x.wait()); return await extractSeqsAndXTxId(contract, receipt, ctx.coreBridgeEvm); } export async function sendSolOnRampFromERC20( ctx: ClientContext, { dstSolanaWallet, amountIn, minLamportsOut, }: { dstSolanaWallet: PublicKey; amountIn: clientLib.XTokenAmount; minLamportsOut: BigNumber; }, ): Promise { const contract = clientLib.XRaydiumBridge__factory.connect( ctx.xRaydiumEvmAddr, ctx.evm.signer, ); // check solana mint exists before sending swap await throwIfSolanaMintNotFound(ctx.sol, amountIn.token); await approveERC20( ctx.evm, ctx.evm.evmWalletAddr, contract.address, amountIn, ); const fee = await calcFee(1, contract); const [solanaProxyPDA] = await getCustodySigner(ctx, ctx.evm.evmWalletAddr); const receipt = await contract .swap( amountIn.token.toEvmFormat(), clientLib.XTokenLocatorExt.fromNative( wh.WSOL_ADDRESS, wh.CHAIN_ID_SOLANA, ).toEvmFormat(), amountIn.rawAmt, minLamportsOut, solanaProxyPDA.toBuffer(), dstSolanaWallet.toBuffer(), { value: fee, gasLimit: 7600000 }, ) .then(x => x.wait()); return await extractSeqsAndXTxId(contract, receipt, ctx.coreBridgeEvm); } export async function sendSwapFromERC20( ctx: ClientContext, { amountIn, minAmountOut, maybeSolanaRecipient, }: { amountIn: clientLib.XTokenAmount; minAmountOut: clientLib.XTokenAmount; maybeSolanaRecipient?: PublicKey; }, ): Promise { const contract = clientLib.XRaydiumBridge__factory.connect( ctx.xRaydiumEvmAddr, ctx.evm.signer, ); // check solana mint exists before sending swap await Promise.all([ throwIfSolanaMintNotFound(ctx.sol, amountIn.token), throwIfSolanaMintNotFound(ctx.sol, minAmountOut.token), ]); await contract.provider.getGasPrice().then(x => dbg(x.toString(), "gas")); const fee = await calcFee(1, contract); await approveERC20( ctx.evm, ctx.evm.evmWalletAddr, contract.address, amountIn, ); const [solanaProxyPDA] = await getCustodySigner(ctx, ctx.evm.evmWalletAddr); const receipt = await contract .swap( amountIn.token.toEvmFormat(), minAmountOut.token.toEvmFormat(), amountIn.rawAmt, minAmountOut.rawAmt, solanaProxyPDA.toBuffer(), maybeSolanaRecipient ? maybeSolanaRecipient.toBuffer() : dbg(Buffer.alloc(32, 0), "0 buf"), { value: fee, gasLimit: 760000 }, ) .then(x => x.wait()); return await extractSeqsAndXTxId(contract, receipt, ctx.coreBridgeEvm); } export async function sendSwapFromETH( ctx: ClientContext, { amountIn, minAmountOut, maybeSolanaRecipient, }: { amountIn: BigNumber; minAmountOut: clientLib.XTokenAmount; maybeSolanaRecipient?: PublicKey; }, solOnRampArgs?: { usersSolanaWallet: PublicKey }, ): Promise { const contract = clientLib.XRaydiumBridge__factory.connect( ctx.xRaydiumEvmAddr, ctx.evm.signer, ); const fee = await calcFee(1, contract); // check solana mint exists before sending swap // todo: grab weth address from wormhole token bridge contract const weth = clientLib.XTokenLocatorExt.fromNative( chainIdToWETH[ctx.evm.chainId], ctx.evm.chainId, ); await Promise.all([ throwIfSolanaMintNotFound(ctx.sol, weth), throwIfSolanaMintNotFound(ctx.sol, minAmountOut.token), ]); const [solanaProxyPDA] = await getCustodySigner(ctx, ctx.evm.evmWalletAddr); const receipt = await contract .swapFromEth( minAmountOut.token.toEvmFormat(), minAmountOut.rawAmt, solanaProxyPDA.toBuffer(), maybeSolanaRecipient ? maybeSolanaRecipient.toBuffer() : dbg(Buffer.alloc(32, 0), "0 buf"), { value: amountIn.add(fee), gasLimit: 760000 }, ) .then(x => x.wait()); return await extractSeqsAndXTxId(contract, receipt, ctx.coreBridgeEvm); } async function throwIfSolanaMintNotFound( ctx: context.SolanaContextNoSigner, token: clientLib.XTokenLocator, ) { const solanaMintExists = await clientLib.getMintSolana(ctx, token); if (!solanaMintExists) { throw new Error( "Input token does not exist on Solana, therefore no pool exists to swap token into SOL. Please try another token or create the pool before attempting a swap", ); } } /** * @deprecated The method should not be used */ export async function sendSwapWithUIAmts( ctx: ClientContext, { inputMint, outputMint, amountInUISpace, minAmtOutUISpace, maybeSolanaRecipient, }: { inputMint: PublicKey; outputMint: PublicKey; amountInUISpace: number; minAmtOutUISpace: number; maybeSolanaRecipient?: PublicKey; }, ): Promise { const [input, output] = await Promise.all([ whHelpers.getXTokenLocatorFromSolanaMint(ctx, inputMint), whHelpers.getXTokenLocatorFromSolanaMint(ctx, outputMint), ]); const [amountInDecimalSpace, minAmtOut] = await Promise.all([ toDecimal(ctx, input, amountInUISpace), toDecimal(ctx, output, minAmtOutUISpace), ]); const amountIn = new clientLib.XTokenAmount(input, amountInDecimalSpace); const minAmountOut = new clientLib.XTokenAmount(output, minAmtOut); dbg("above sendSwapFromERC20"); return await sendSwapFromERC20(ctx, { amountIn, minAmountOut, maybeSolanaRecipient, }); } export async function toDecimal( ctx: context.Context, loc: clientLib.XTokenLocator, amtUISpace: number, ): Promise { if (loc.chainId === wh.CHAIN_ID_SOLANA) { const mint = await spl.getMint( ctx.sol.conn, await whHelpers.getMintSolana(ctx.sol, loc), ); return toDecimalSpace(amtUISpace, mint.decimals); } const [inDecimalSpace] = await toDecimalAndERC20(ctx.evm, loc, amtUISpace); return inDecimalSpace; } export function toDecimalSpace(ui: number, decimals: number): BigNumber { if (ui < 1) { return BigNumber.from(10) .pow(BigNumber.from(decimals)) .div(BigNumber.from(1 / ui)); } return BigNumber.from(10) .pow(BigNumber.from(decimals)) .mul(BigNumber.from(ui)); } export async function toDecimalAndERC20( ctx: context.EvmContext, loc: clientLib.XTokenLocator, amtUISpace: number, ): Promise<[BigNumber, ERC20]> { dbg(loc.toNative(), "toDecimalAndERC20"); const mint = _undef(await whHelpers.getMintEVM(ctx, loc)); const erc20 = await loadERC20(mint, ctx.signer); const decimals = await erc20.decimals(); return [toDecimalSpace(amtUISpace, decimals), erc20]; } async function getCustodySigner( pids: { solanaProxy: PublicKey }, evmWallet: string, // native evm address ): Promise<[PublicKey, number]> { const evmWalletBuf = wh.tryNativeToUint8Array( evmWallet, "ethereum", ); return await PublicKey.findProgramAddress( [Buffer.from("custody"), evmWalletBuf], pids.solanaProxy, ); } async function extractSeqsAndXTxId( contract: clientLib.XRaydiumBridge, receipt: ethers.ContractReceipt, coreBridgeEvm: string, ): Promise { printEvents(contract, receipt); for (let log of receipt.logs) { try { const parsed: LogDescription = contract.interface.parseLog(log); if (parsed.name === "XTxID") { return { seq: wh.parseSequenceFromLogEth(receipt, coreBridgeEvm), xTxId: parsed.args.id.toNumber(), }; } } catch (e) {} } throw new Error("Could not parse xTxID from tx events"); } export async function sendAbort({ id, signer, contract, raydiumAddr, solanaProxyAddr, tokenBridgeEvmAddr, coreBridgeEvmAddr, dummySolanaToken = new PublicKey( "So11111111111111111111111111111111111111112", ), }: { id: BigNumber; signer: Signer; contract: clientLib.XRaydiumBridge; raydiumAddr: PublicKey; solanaProxyAddr: PublicKey; tokenBridgeEvmAddr: string; coreBridgeEvmAddr: string; dummySolanaToken?: PublicKey; }): Promise { const fee = await calcFee(1, contract); const tx = await contract.abort(BigNumber.from(id), { value: fee, gasLimit: 760000, }); const receipt = await tx.wait(); printEvents(contract, receipt); return wh.parseSequenceFromLogEth(receipt, coreBridgeEvmAddr); } export function printEvents(contract: Contract, receipt: ContractReceipt) { dbg(receipt.status, "Events for receipt. Status:"); dbg(receipt.transactionHash, "Tx Hash"); if (!receipt.logs.length) { console.log("No logs, printing full receipt"); console.log(receipt); } receipt.logs.forEach(log => { try { const parsed = contract.interface.parseLog(log); console.log(parsed.signature, Array.from(parsed.args)); } catch (e) {} }); } export type ERC20 = Contract & { balanceOf: (addr: string) => Promise; decimals: () => Promise; }; export async function loadERC20( evmAddress: string, signer: Signer, // types broken on ethers from hardhat. Should be ethers.Signer path?: string, ): Promise { // @ts-ignore return await new ethers.Contract(evmAddress, erc20_abi, signer); } /** * Approve tokens for a spender along with debug statements */ export async function approveERC20( ctx: context.EvmContext, approverAddr: string, spenderAddr: string, amount: clientLib.XTokenAmount, silent: boolean = false, ): Promise { const erc20Contract = await loadERC20( await clientLib.getMintEVM(ctx, amount.token).then(_undef), ctx.signer, ); if (!silent) { (async () => { const symbol = await erc20Contract.symbol(); const name = await erc20Contract.name(); erc20Contract .balanceOf(approverAddr) .then(bal => info( bal, `Token Balance (symbol: ${symbol}, name: ${name})`, ), ); const decimals = await erc20Contract.decimals(); info( `Transferring ${ amount.rawAmt .div( BigNumber.from(10).pow( BigNumber.from(Math.max(decimals - 2, 0)), ), ) .toNumber() / Math.pow(10, Math.min(2, Math.max(decimals - 2, 0))) } token, but in decimals: ${amount.rawAmt.toString()}`, ); })(); } await erc20Contract .approve( spenderAddr, BigNumber.from(amount.rawAmt).mul(6).div(5), // fix "transfer amount exceeds allowance error" ) .then(x => x.wait()); } async function calcFee( numTokenReturns: number, contract: clientLib.XRaydiumBridge, ): Promise { const base = await contract.relayerFee(); dbg(base.toString(), "calcFee::fee"); return base; } export async function sweepFees( ctx: context.EvmContext, ): Promise { const contract = clientLib.XRaydiumBridge__factory.connect( ctx.xRaydiumEvmAddr, ctx.signer, ); const tx = await contract.sweepFees(); const receipt = await tx.wait(); dbg(receipt.blockHash, "sweepFees blockHash"); printEvents(contract, receipt); return receipt; }