import { AccountLayout, Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token"; import { Connection, Keypair, PublicKey, SystemProgram, Transaction as SolanaTransaction, } from "@solana/web3.js"; import { MsgExecuteContract } from "@terra-money/terra.js"; import { Algodv2, bigIntToBytes, getApplicationAddress, makeApplicationCallTxnFromObject, makeAssetTransferTxnWithSuggestedParamsFromObject, makePaymentTxnWithSuggestedParamsFromObject, OnApplicationComplete, SuggestedParams, Transaction as AlgorandTransaction, } from "algosdk"; import { BigNumber, ethers, Overrides, PayableOverrides } from "ethers"; import { isNativeDenom } from ".."; import { assetOptinCheck, getMessageFee, optin, TransactionSignerPair, } from "../algorand"; import { getEmitterAddressAlgorand } from "../bridge"; import { Bridge__factory, TokenImplementation__factory, } from "../ethers-contracts"; import { getBridgeFeeIx, ixFromRust } from "../solana"; import { importTokenWasm } from "../solana/wasm"; import { ChainId, CHAIN_ID_SOLANA, createNonce, hexToUint8Array, textToUint8Array, WSOL_ADDRESS, } from "../utils"; import { safeBigIntToNumber } from "../utils/bigint"; export async function getAllowanceEth( tokenBridgeAddress: string, tokenAddress: string, signer: ethers.Signer ) { const token = TokenImplementation__factory.connect(tokenAddress, signer); const signerAddress = await signer.getAddress(); const allowance = await token.allowance(signerAddress, tokenBridgeAddress); return allowance; } export async function approveEth( tokenBridgeAddress: string, tokenAddress: string, signer: ethers.Signer, amount: ethers.BigNumberish, overrides: Overrides & { from?: string | Promise } = {} ) { const token = TokenImplementation__factory.connect(tokenAddress, signer); return await ( await token.approve(tokenBridgeAddress, amount, overrides) ).wait(); } export async function transferFromEth( tokenBridgeAddress: string, signer: ethers.Signer, tokenAddress: string, amount: ethers.BigNumberish, recipientChain: ChainId, recipientAddress: Uint8Array, relayerFee: ethers.BigNumberish = 0, overrides: PayableOverrides & { from?: string | Promise } = {} ) { const bridge = Bridge__factory.connect(tokenBridgeAddress, signer); const v = await bridge.transferTokens( tokenAddress, amount, recipientChain, recipientAddress, relayerFee, createNonce(), overrides ); const receipt = await v.wait(); return receipt; } export async function transferFromEthNative( tokenBridgeAddress: string, signer: ethers.Signer, amount: ethers.BigNumberish, recipientChain: ChainId, recipientAddress: Uint8Array, relayerFee: ethers.BigNumberish = 0, overrides: PayableOverrides & { from?: string | Promise } = {} ) { const bridge = Bridge__factory.connect(tokenBridgeAddress, signer); const v = await bridge.wrapAndTransferETH( recipientChain, recipientAddress, relayerFee, createNonce(), { ...overrides, value: amount, } ); const receipt = await v.wait(); return receipt; } export async function transferFromTerra( walletAddress: string, tokenBridgeAddress: string, tokenAddress: string, amount: string, recipientChain: ChainId, recipientAddress: Uint8Array, relayerFee: string = "0" ) { const nonce = Math.round(Math.random() * 100000); const isNativeAsset = isNativeDenom(tokenAddress); return isNativeAsset ? [ new MsgExecuteContract( walletAddress, tokenBridgeAddress, { deposit_tokens: {}, }, { [tokenAddress]: amount } ), new MsgExecuteContract( walletAddress, tokenBridgeAddress, { initiate_transfer: { asset: { amount, info: { native_token: { denom: tokenAddress, }, }, }, recipient_chain: recipientChain, recipient: Buffer.from(recipientAddress).toString("base64"), fee: relayerFee, nonce: nonce, }, }, {} ), ] : [ new MsgExecuteContract( walletAddress, tokenAddress, { increase_allowance: { spender: tokenBridgeAddress, amount: amount, expires: { never: {}, }, }, }, {} ), new MsgExecuteContract( walletAddress, tokenBridgeAddress, { initiate_transfer: { asset: { amount: amount, info: { token: { contract_addr: tokenAddress, }, }, }, recipient_chain: recipientChain, recipient: Buffer.from(recipientAddress).toString("base64"), fee: relayerFee, nonce: nonce, }, }, {} ), ]; } export async function transferNativeSol( connection: Connection, bridgeAddress: string, tokenBridgeAddress: string, payerAddress: string, amount: BigInt, targetAddress: Uint8Array, targetChain: ChainId, relayerFee: BigInt = BigInt(0) ) { //https://github.com/solana-labs/solana-program-library/blob/master/token/js/client/token.js const rentBalance = await Token.getMinBalanceRentForExemptAccount(connection); const mintPublicKey = new PublicKey(WSOL_ADDRESS); const payerPublicKey = new PublicKey(payerAddress); const ancillaryKeypair = Keypair.generate(); //This will create a temporary account where the wSOL will be created. const createAncillaryAccountIx = SystemProgram.createAccount({ fromPubkey: payerPublicKey, newAccountPubkey: ancillaryKeypair.publicKey, lamports: rentBalance, //spl token accounts need rent exemption space: AccountLayout.span, programId: TOKEN_PROGRAM_ID, }); //Send in the amount of SOL which we want converted to wSOL const initialBalanceTransferIx = SystemProgram.transfer({ fromPubkey: payerPublicKey, lamports: Number(amount), toPubkey: ancillaryKeypair.publicKey, }); //Initialize the account as a WSOL account, with the original payerAddress as owner const initAccountIx = await Token.createInitAccountInstruction( TOKEN_PROGRAM_ID, mintPublicKey, ancillaryKeypair.publicKey, payerPublicKey ); //Normal approve & transfer instructions, except that the wSOL is sent from the ancillary account. const { transfer_native_ix, approval_authority_address } = await importTokenWasm(); const nonce = createNonce().readUInt32LE(0); const transferIx = await getBridgeFeeIx( connection, bridgeAddress, payerAddress ); const approvalIx = Token.createApproveInstruction( TOKEN_PROGRAM_ID, ancillaryKeypair.publicKey, new PublicKey(approval_authority_address(tokenBridgeAddress)), payerPublicKey, //owner [], new u64(amount.toString(16), 16) ); let messageKey = Keypair.generate(); const ix = ixFromRust( transfer_native_ix( tokenBridgeAddress, bridgeAddress, payerAddress, messageKey.publicKey.toString(), ancillaryKeypair.publicKey.toString(), WSOL_ADDRESS, nonce, amount, relayerFee, targetAddress, targetChain ) ); //Close the ancillary account for cleanup. Payer address receives any remaining funds const closeAccountIx = Token.createCloseAccountInstruction( TOKEN_PROGRAM_ID, ancillaryKeypair.publicKey, //account to close payerPublicKey, //Remaining funds destination payerPublicKey, //authority [] ); const { blockhash } = await connection.getRecentBlockhash(); const transaction = new SolanaTransaction(); transaction.recentBlockhash = blockhash; transaction.feePayer = new PublicKey(payerAddress); transaction.add(createAncillaryAccountIx); transaction.add(initialBalanceTransferIx); transaction.add(initAccountIx); transaction.add(transferIx, approvalIx, ix); transaction.add(closeAccountIx); transaction.partialSign(messageKey); transaction.partialSign(ancillaryKeypair); return transaction; } export async function transferFromSolana( connection: Connection, bridgeAddress: string, tokenBridgeAddress: string, payerAddress: string, fromAddress: string, mintAddress: string, amount: BigInt, targetAddress: Uint8Array, targetChain: ChainId, originAddress?: Uint8Array, originChain?: ChainId, fromOwnerAddress?: string, relayerFee: BigInt = BigInt(0) ) { const nonce = createNonce().readUInt32LE(0); const transferIx = await getBridgeFeeIx( connection, bridgeAddress, payerAddress ); const { transfer_native_ix, transfer_wrapped_ix, approval_authority_address, } = await importTokenWasm(); const approvalIx = Token.createApproveInstruction( TOKEN_PROGRAM_ID, new PublicKey(fromAddress), new PublicKey(approval_authority_address(tokenBridgeAddress)), new PublicKey(fromOwnerAddress || payerAddress), [], new u64(amount.toString(16), 16) ); let messageKey = Keypair.generate(); const isSolanaNative = originChain === undefined || originChain === CHAIN_ID_SOLANA; if (!isSolanaNative && !originAddress) { throw new Error("originAddress is required when specifying originChain"); } const ix = ixFromRust( isSolanaNative ? transfer_native_ix( tokenBridgeAddress, bridgeAddress, payerAddress, messageKey.publicKey.toString(), fromAddress, mintAddress, nonce, amount, relayerFee, targetAddress, targetChain ) : transfer_wrapped_ix( tokenBridgeAddress, bridgeAddress, payerAddress, messageKey.publicKey.toString(), fromAddress, fromOwnerAddress || payerAddress, originChain as number, // checked by isSolanaNative originAddress as Uint8Array, // checked by throw nonce, amount, relayerFee, targetAddress, targetChain ) ); const transaction = new SolanaTransaction().add(transferIx, approvalIx, ix); const { blockhash } = await connection.getRecentBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = new PublicKey(payerAddress); transaction.partialSign(messageKey); return transaction; } /** * Transfers an asset from Algorand to a receiver on another chain * @param client AlgodV2 client * @param tokenBridgeId Application ID of the token bridge * @param bridgeId Application ID of the core bridge * @param sender Sending account * @param assetId Asset index * @param qty Quantity to transfer * @param receiver Receiving account * @param chain Reeiving chain * @param fee Transfer fee * @returns Sequence number of confirmation */ export async function transferFromAlgorand( client: Algodv2, tokenBridgeId: bigint, bridgeId: bigint, senderAddr: string, assetId: bigint, qty: bigint, receiver: string, chain: ChainId, fee: bigint ): Promise { const tokenAddr: string = getApplicationAddress(tokenBridgeId); const applAddr: string = getEmitterAddressAlgorand(tokenBridgeId); const txs: TransactionSignerPair[] = []; // "transferAsset" const { addr: emitterAddr, txs: emitterOptInTxs } = await optin( client, senderAddr, bridgeId, BigInt(0), applAddr ); txs.push(...emitterOptInTxs); let creator; let creatorAcctInfo: any; let wormhole: boolean = false; if (assetId !== BigInt(0)) { const assetInfo: Record = await client .getAssetByID(safeBigIntToNumber(assetId)) .do(); creator = assetInfo["params"]["creator"]; creatorAcctInfo = await client.accountInformation(creator).do(); const authAddr: string = creatorAcctInfo["auth-addr"]; if (authAddr === tokenAddr) { wormhole = true; } } const params: SuggestedParams = await client.getTransactionParams().do(); const msgFee: bigint = await getMessageFee(client, bridgeId); if (msgFee > 0) { const payTxn: AlgorandTransaction = makePaymentTxnWithSuggestedParamsFromObject({ from: senderAddr, suggestedParams: params, to: getApplicationAddress(tokenBridgeId), amount: msgFee, }); txs.push({ tx: payTxn, signer: null }); } if (!wormhole) { const bNat = Buffer.from("native", "binary").toString("hex"); // "creator" const result = await optin( client, senderAddr, tokenBridgeId, assetId, bNat ); creator = result.addr; txs.push(...result.txs); } if ( assetId !== BigInt(0) && !(await assetOptinCheck(client, assetId, creator)) ) { // Looks like we need to optin const payTxn: AlgorandTransaction = makePaymentTxnWithSuggestedParamsFromObject({ from: senderAddr, to: creator, amount: 100000, suggestedParams: params, }); txs.push({ tx: payTxn, signer: null }); // The tokenid app needs to do the optin since it has signature authority const bOptin: Uint8Array = textToUint8Array("optin"); let txn = makeApplicationCallTxnFromObject({ from: senderAddr, appIndex: safeBigIntToNumber(tokenBridgeId), onComplete: OnApplicationComplete.NoOpOC, appArgs: [bOptin, bigIntToBytes(assetId, 8)], foreignAssets: [safeBigIntToNumber(assetId)], accounts: [creator], suggestedParams: params, }); txn.fee *= 2; txs.push({ tx: txn, signer: null }); } const t = makeApplicationCallTxnFromObject({ from: senderAddr, appIndex: safeBigIntToNumber(tokenBridgeId), onComplete: OnApplicationComplete.NoOpOC, appArgs: [textToUint8Array("nop")], suggestedParams: params, }); txs.push({ tx: t, signer: null }); let accounts: string[] = []; if (assetId === BigInt(0)) { const t = makePaymentTxnWithSuggestedParamsFromObject({ from: senderAddr, to: creator, amount: qty, suggestedParams: params, }); txs.push({ tx: t, signer: null }); accounts = [emitterAddr, creator, creator]; } else { const t = makeAssetTransferTxnWithSuggestedParamsFromObject({ from: senderAddr, to: creator, suggestedParams: params, amount: qty, assetIndex: safeBigIntToNumber(assetId), }); txs.push({ tx: t, signer: null }); accounts = [emitterAddr, creator, creatorAcctInfo["address"]]; } let args = [ textToUint8Array("sendTransfer"), bigIntToBytes(assetId, 8), bigIntToBytes(qty, 8), hexToUint8Array(receiver), bigIntToBytes(chain, 8), bigIntToBytes(fee, 8), ]; let acTxn = makeApplicationCallTxnFromObject({ from: senderAddr, appIndex: safeBigIntToNumber(tokenBridgeId), onComplete: OnApplicationComplete.NoOpOC, appArgs: args, foreignApps: [safeBigIntToNumber(bridgeId)], foreignAssets: [safeBigIntToNumber(assetId)], accounts: accounts, suggestedParams: params, }); acTxn.fee *= 2; txs.push({ tx: acTxn, signer: null }); return txs; }