import * as wh from "@certusone/wormhole-sdk"; import * as ethers from "ethers"; import * as grpcWebNodeHttpTransport from "@improbable-eng/grpc-web-node-http-transport"; import * as web3 from "@solana/web3.js"; import * as context from "./context"; import * as types from "."; import * as utilities from "./utilities"; import { PublicKey } from "@solana/web3.js"; import { dbg, info, warn, _undef } from "./utilities"; import { ForeignAddrExt } from "./type_extensions"; import { ClientContext, loadERC20 } from "./client"; import * as _spl from "@solana/spl-token"; import { Header } from "."; const spl: any = _spl; export interface TransferPayload { amount: bigint; originAddress: string; originChain: wh.ChainId; targetAddress: string; targetChain: wh.ChainId; fee: bigint; payload3?: Uint8Array; } export interface TransferPayloadWithData { amount: bigint; originAddress: types.ForeignAddr; originChain: wh.ChainId; targetAddress: types.ForeignAddr; targetChain: wh.ChainId; fromAddress: types.ForeignAddr; payload3: Uint8Array; } export interface BaseVAA { version: number; guardianSetIndex: number; timestamp: number; nonce: number; emitterChain: wh.ChainId; emitterAddress: Uint8Array; // 32 bytes sequence: number; consistencyLevel: number; payload: Uint8Array; } export type Provider = ethers.providers.Provider; export async function getMintEVM( ctx: context.EvmContextNoSigner, token: types.XTokenLocator, ): Promise { if (token.chainId === ctx.chainId) { return wh.tryUint8ArrayToNative( new Uint8Array(token.address.inner), token.chainId, ); } const wrappedMint = await wh.getForeignAssetEth( ctx.tokenBridgeEvm, ctx.provider, token.chainId, Buffer.from(token.address.inner), ); return !wrappedMint || wrappedMint === "0x0000000000000000000000000000000000000000" ? undefined : wrappedMint; } export async function getMintSolana( ctx: context.SolanaContextNoSigner, token: types.XTokenLocator, ): Promise { if (token.chainId === context.solanaChainId) { return new PublicKey(Buffer.from(token.address.inner)); } const wrappedMint = await wh.getForeignAssetSolana( ctx.conn, ctx.tokenBridgeSolana.toBase58(), token.chainId, Buffer.from(token.address.inner), ); if (!wrappedMint) { return undefined; } return new PublicKey(_undef(wrappedMint)); } // Assumes the token is either EVM or Solana native export async function getXTokenLocatorFromSolanaMint( ctx: ClientContext, mint: PublicKey, ): Promise { if ( await wh.getIsWrappedAssetSol( ctx.sol.conn, ctx.tokenBridgeSolana.toBase58(), mint.toBase58(), ) ) { info( "Solana mint address " + mint.toBase58() + " is a WH-wrapped asset on Solana", ); const evmMintInfo = await wh.getOriginalAssetSol( ctx.sol.conn, ctx.tokenBridgeSolana.toBase58(), mint.toBase58(), ); return new types.XTokenLocator({ address: ForeignAddrExt.fromBytes(evmMintInfo.assetAddress), chainId: evmMintInfo.chainId, }); } else { info( "Solana mint address " + mint.toBase58() + " is a native asset on Solana", ); return new types.XTokenLocator({ address: ForeignAddrExt.fromPubkey(mint), chainId: context.solanaChainId, }); } } // Assumes the token is either EVM or Solana native export async function getXTokenLocatorFromEVMMint( ctx: ClientContext, mint: string, ): Promise { if ( await wh.getIsWrappedAssetEth(ctx.tokenBridgeEvm, ctx.evm.signer, mint) ) { info("EVM mint address " + mint + " is a WH-wrapped asset on EVM"); const solanaMintInfo = await wh.getOriginalAssetEth( ctx.tokenBridgeEvm, ctx.evm.signer, mint, context.solanaChainId, ); return new types.XTokenLocator({ address: ForeignAddrExt.fromBytes(solanaMintInfo.assetAddress), chainId: solanaMintInfo.chainId, }); } else { info("EVM mint address " + mint + " is a native asset on EVM"); return new types.XTokenLocator({ address: ForeignAddrExt.fromHex(mint.slice(2)), chainId: ctx.evm.chainId, }); } } export async function ensureTokenAttested( ctx: context.Context, token: types.XTokenLocator, ) { switch (token.chainId) { case context.solanaChainId: return await ensureTokenAttestedEVM(ctx, token); case ctx.evm.chainId: return await ensureTokenAttestedSolana(ctx, token); default: warn( "Unsupported token source chain. Must be configured evm or solana", ); } } export async function ensureTokenAttestedEVM( ctx: context.Context, token: types.XTokenLocator, ) { const mintSolana = _undef( wh.tryUint8ArrayToNative( Buffer.from(token.address.inner), context.solanaChainId, ), ); if ((await getMintEVM(ctx.evm, token)) !== undefined) { info(mintSolana, "Token already attested SOL->EVM"); return; } utilities.debug_xTokenLocator(token, "Attesting new token from Solana"); await utilities.waitForAccountToExist( ctx.sol.conn, new PublicKey(mintSolana), ); let tx = await wh.attestFromSolana( ctx.sol.conn, ctx.coreBridgeSolana.toBase58(), ctx.tokenBridgeSolana.toBase58(), ctx.sol.payer.publicKey.toBase58(), mintSolana, ); tx.partialSign(ctx.sol.payer); tx.instructions.forEach((ix, i) => utilities.debug_ix(ix, i.toString())); const txResponse = await web3 .sendAndConfirmRawTransaction( ctx.sol.conn, tx.serialize(), ctx.sol.connectionOverrides, ) .then(ctx.sol.txLogs); const seq = dbg(wh.parseSequenceFromLogSolana(txResponse as web3.TransactionResponse), "seq"); const emitterAddress = dbg( await wh.getEmitterAddressSolana(ctx.tokenBridgeSolana.toBase58()), "Solana emitter address", ); const vaaBytes = await getSignedVaaWithBackoff( emitterAddress, seq, context.solanaChainId, ctx.wormholeRPC, ); dbg( await wh.createWrappedOnEth( ctx.tokenBridgeEvm, ctx.evm.signer, Buffer.from(vaaBytes), ), "result of createWrappedOnEth", ); info("Token attestation on ETH complete"); } // If the token has not been attested on solana yet, attest it export async function ensureTokenAttestedSolana( ctx: context.Context, token: types.XTokenLocator, ) { const chainId = token.chainId as wh.ChainId; const address = new Uint8Array(token.address.inner); if ( await wh.getForeignAssetSolana( ctx.sol.conn, ctx.tokenBridgeSolana.toBase58(), chainId, address, ) ) { return; } info(token, "Attesting new token from EVM"); const nativeEVMAddress = _undef( wh.tryUint8ArrayToNative(Buffer.from(token.address.inner), chainId), ); info(nativeEVMAddress, "Native EVM erc20 token address to attest"); const receipt = await wh.attestFromEth( ctx.tokenBridgeEvm, ctx.evm.signer, nativeEVMAddress, ); const seq = wh.parseSequenceFromLogEth(receipt, ctx.coreBridgeEvm); const emitterAddress = wh.getEmitterAddressEth(ctx.tokenBridgeEvm); const vaaBytes = await getSignedVaaWithBackoff( emitterAddress, seq, ctx.evm.chainId, ctx.wormholeRPC, ); info("Posting attestation VAA"); await wh.postVaaSolanaWithRetry( ctx.sol.conn, ctx.sol.signTx, ctx.coreBridgeSolana.toBase58(), ctx.sol.payer.publicKey.toBase58(), Buffer.from(vaaBytes), 5, ); info("Creating wrapped token mint on Solana"); await ctx.sol.sendIxsWithPayer( ( await wh.createWrappedOnSolana( ctx.sol.conn, ctx.coreBridgeSolana.toBase58(), ctx.tokenBridgeSolana.toBase58(), ctx.sol.payer.publicKey.toBase58(), vaaBytes, ) ).instructions, ); info("Token attestation complete"); } export async function transferEvmToSolanaNative( ctx: context.Context, mint: string, amountInDecimalSpace: number, ): Promise { const evmSigner = ctx.evm.signer; // find, ensure attested and load erc20 const inputERC20 = await loadERC20(mint, evmSigner); const inputDecimals: number = await inputERC20.decimals(); await inputERC20 .balanceOf(ctx.evm.evmWalletAddr) .then(bal => info(bal, "Balance of input token")); // approve proxy to use sender funds info(`Transferring ${amountInDecimalSpace} tokens (in decimal space)`); await inputERC20 .approve( ctx.tokenBridgeEvm, ethers.BigNumber.from(amountInDecimalSpace).mul(6).div(5), // fix "transfer amount exceeds allowance error" ) .then(x => x.wait()); const assetWrappedInfo = await wh.getOriginalAssetEth( ctx.tokenBridgeEvm, evmSigner, mint, context.solanaChainId, ); const solanaMintAddress = new web3.PublicKey(assetWrappedInfo.assetAddress); const ata = await spl.getOrCreateAssociatedTokenAccount( ctx.sol.conn, ctx.sol.payer, new web3.PublicKey(solanaMintAddress), ctx.sol.payer.publicKey, ); const evmTx = await wh.transferFromEth( ctx.tokenBridgeEvm, evmSigner, mint, amountInDecimalSpace, context.solanaChainId, wh.tryNativeToUint8Array(ata.address.toBase58(), context.solanaChainId), ); return wh.parseSequenceFromLogEth(evmTx, ctx.coreBridgeEvm); } export async function transferEvmNativeToSolana( ctx: context.Context, mint: string, amountIn: number, ): Promise { // We assume that an ERC20 contract already exists at the mint address // It does not have to be attested yet, taken care of by this line let xTokenLocator = new types.XTokenLocator({ address: ForeignAddrExt.fromHex(mint.slice(2)), chainId: ctx.evm.chainId, }); await ensureTokenAttestedSolana(ctx, xTokenLocator); const solanaMintAddress = await wh.getForeignAssetSolana( ctx.sol.conn, ctx.tokenBridgeSolana.toBase58(), ctx.evm.chainId, new Uint8Array(xTokenLocator.address.inner), ); info( "EVM native token with address " + mint + " attested on Solana. Solana mint address: " + solanaMintAddress, ); const evmSigner = ctx.evm.signer; info( "Setting allowance for sender's tokens to be spent by the EVM Token Bridge", ); const ERC20 = await loadERC20(mint, evmSigner); await ERC20.approve( ctx.tokenBridgeEvm, ethers.BigNumber.from(amountIn).mul(6).div(5), ).then(x => x.wait()); // When we initiate the token transfer, we assume the sender EVM address has a sufficient balance info( "Initiating token transfer from EVM address " + ctx.evm.evmWalletAddr + " to Solana address " + ctx.sol.payer.publicKey.toBase58(), ); const ata = await spl.getOrCreateAssociatedTokenAccount( ctx.sol.conn, ctx.sol.payer, new web3.PublicKey(solanaMintAddress || "Solana mint address is null"), ctx.sol.payer.publicKey, ); const tx = await wh.transferFromEth( ctx.tokenBridgeEvm, evmSigner, mint, ethers.BigNumber.from(amountIn), context.solanaChainId, wh.tryNativeToUint8Array(ata.address.toBase58(), context.solanaChainId), ); const seq = wh.parseSequenceFromLogEth(tx, ctx.coreBridgeEvm); info( "Token transfer from EVM to solana emitted (seq: " + seq + "). VAA awaiting submission on Solana.", ); return seq; } export async function redeemTransferOnSolana( ctx: context.Context, seq: string, ) { info("Redeeming transfer on Solana. Sequence: " + seq); const emitter = await wh.getEmitterAddressEth(ctx.tokenBridgeEvm); const vaa = await getSignedVaaWithBackoff( emitter, seq, ctx.evm.chainId, ctx.wormholeRPC, ); await wh.postVaaSolanaWithRetry( ctx.sol.conn, async tx => { tx.partialSign(ctx.sol.payer); return tx; }, ctx.coreBridgeSolana.toBase58(), ctx.sol.payer.publicKey.toBase58(), Buffer.from(vaa), 5, ); info("VAA posted on Solana"); const tx = await wh.redeemOnSolana( ctx.sol.conn, ctx.coreBridgeSolana.toBase58(), ctx.tokenBridgeSolana.toBase58(), ctx.sol.payer.publicKey.toBase58(), vaa, ); tx.partialSign(ctx.sol.payer); tx.instructions.forEach((ix, i) => utilities.debug_ix(ix, i.toString())); const txResponse = await web3 .sendAndConfirmRawTransaction(ctx.sol.conn, tx.serialize()) .then(utilities.txLogsFact(ctx.sol.conn)); } export async function redeemTransferOnEVM(ctx: context.Context, seq: string) { info(ctx.evm.evmWalletAddr, "evm sender"); const emitter = await wh.getEmitterAddressSolana( ctx.tokenBridgeSolana.toBase58(), ); const vaa = await getSignedVaaWithBackoff( emitter, seq, context.solanaChainId, ctx.wormholeRPC, ); info("Got signed VAA"); await wh.redeemOnEth(ctx.tokenBridgeEvm, ctx.evm.signer, vaa); info("Redemption on evm complete"); } export async function getSignedVaaWithBackoff( emitterAddress: string, sequence: string, chainID: wh.ChainId, whRPC: string, { backoff = 500, silent = false, retries = 50 }: utilities.RetryOptions = { backoff: 500, silent: false, retries: 50, }, ): Promise { const start = Date.now(); for (let i = 0; i < retries; i++) { try { const resp = await wh.getSignedVAA( whRPC, chainID, emitterAddress, sequence, { transport: grpcWebNodeHttpTransport.NodeHttpTransport() }, ); return resp.vaaBytes; } catch (e: any) { if (!silent) { if ( e?.metadata?.headersMap["grpc-message"] != "requested VAA not found in store" ) { dbg(e?.metadata?.headersMap["grpc-message"]); } console.warn( "VAA not yet available. Elapsed secs: ", (Date.now() - start) / 1000, ); } await utilities.sleep(backoff * i); } } return new Uint8Array(); } export async function parseTransferTokenWithPayload( baseVAA: wh.ParsedVaa, ): Promise { let transferPayloadRaw = wh.parseTransferPayload( Buffer.from(baseVAA["payload"]), ); return { ...transferPayloadRaw, originChain: transferPayloadRaw.originChain as wh.ChainId, targetChain: transferPayloadRaw.targetChain as wh.ChainId, originAddress: ForeignAddrExt.fromHex(transferPayloadRaw.originAddress), targetAddress: ForeignAddrExt.fromHex(transferPayloadRaw.targetAddress), fromAddress: new types.ForeignAddr({ inner: Array.from(baseVAA["payload"].slice(101, 133)), }), payload3: baseVAA["payload"].slice(133), }; } export async function tryParseTransferTokenWithPayload( baseVAA: wh.ParsedVaa, ): Promise { try { return parseTransferTokenWithPayload(baseVAA); } catch (e) { return undefined; } } export async function getAndParseEvmVaa( ctx: { chainId: wh.ChainId } & context.PIDS, seq: string, silent: boolean = false, ): Promise<{ header: Header; transfer?: TransferPayloadWithData; vaaBytes: Uint8Array; baseVAA: wh.ParsedVaa; }> { const emitterAddress = wh.getEmitterAddressEth( dbg( ctx.tokenBridgeEvm, "tokebBridgeEvm used in emiter address getting", ), ); if (!silent) { console.log("[Sequence]: ", seq); console.log( "[EmitterAddress]: ", wh.tryHexToNativeAssetString(emitterAddress, ctx.chainId), ); } const vaaBytes = await getSignedVaaWithBackoff( emitterAddress, seq, ctx.chainId, ctx.wormholeRPC, ); const baseVAA = wh.parseVaa(vaaBytes); let transfer: TransferPayloadWithData | undefined; let payload = baseVAA.payload; try { transfer = await parseTransferTokenWithPayload(baseVAA); payload = Buffer.from(transfer.payload3); } catch (e) {} const header = parseHeader(payload); return { transfer, vaaBytes, baseVAA, header }; } export function parseHeader( bytes: Uint8Array | Buffer, silent = true, ): types.Header { const header = types.Header.layout().decode(Buffer.from(bytes)); const parsed = types.Header.fromDecoded(header); if (!silent) { info(header.id.toString(), "xTx ID"); dbg(parsed, "Header.fromDecoded"); } return parsed; }