diff --git a/client/examples/prepare-primary-network-txns/transfer-avax-from-c-chain-to-p-chain.ts b/client/examples/prepare-primary-network-txns/transfer-avax-from-c-chain-to-p-chain.ts index 9fff57ef..fed58dda 100644 --- a/client/examples/prepare-primary-network-txns/transfer-avax-from-c-chain-to-p-chain.ts +++ b/client/examples/prepare-primary-network-txns/transfer-avax-from-c-chain-to-p-chain.ts @@ -21,9 +21,9 @@ async function run() { // Creating a export transaction request from the C-chain to the P-chain const cChainExportTxnRequest = await walletClient.cChain.prepareExportTxn({ destinationChain: "P", - fromAddress: "0x76Dd3d7b2f635c2547B861e55aE8A374E587742D", + fromAddress: account.getEVMAddress(), // 0x76Dd3d7b2f635c2547B861e55aE8A374E587742D exportedOutput: { - addresses: ["P-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz"], + addresses: [account.getXPAddress("P", "fuji")], // P-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz amount: 0.0001, }, }); @@ -46,7 +46,7 @@ async function run() { const pChainImportTxnRequest = await walletClient.pChain.prepareImportTxn({ sourceChain: "C", importedOutput: { - addresses: ["P-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz"], + addresses: [account.getXPAddress("P", "fuji")], // P-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz }, }); diff --git a/client/examples/prepare-primary-network-txns/transfer-avax-from-c-chain-to-x-chain.ts b/client/examples/prepare-primary-network-txns/transfer-avax-from-c-chain-to-x-chain.ts index bc7ea7f6..29e435ea 100644 --- a/client/examples/prepare-primary-network-txns/transfer-avax-from-c-chain-to-x-chain.ts +++ b/client/examples/prepare-primary-network-txns/transfer-avax-from-c-chain-to-x-chain.ts @@ -21,9 +21,9 @@ async function run() { // Creating a export transaction request from the C-chain to the X-chain const cChainExportTxnRequest = await walletClient.cChain.prepareExportTxn({ destinationChain: "X", - fromAddress: "0x76Dd3d7b2f635c2547B861e55aE8A374E587742D", + fromAddress: account.getEVMAddress(), // 0x76Dd3d7b2f635c2547B861e55aE8A374E587742D exportedOutput: { - addresses: ["X-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz"], + addresses: [account.getXPAddress("X", "fuji")], // X-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz amount: 0.0011, }, }); @@ -46,7 +46,7 @@ async function run() { const xChainImportTxnRequest = await walletClient.xChain.prepareImportTxn({ sourceChain: "C", importedOutput: { - addresses: ["X-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz"], + addresses: [account.getXPAddress("X", "fuji")], // X-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz }, }); diff --git a/client/examples/prepare-primary-network-txns/transfer-avax-from-p-chain-to-c-chain.ts b/client/examples/prepare-primary-network-txns/transfer-avax-from-p-chain-to-c-chain.ts index b3d7433a..e3006d2f 100644 --- a/client/examples/prepare-primary-network-txns/transfer-avax-from-p-chain-to-c-chain.ts +++ b/client/examples/prepare-primary-network-txns/transfer-avax-from-p-chain-to-c-chain.ts @@ -24,7 +24,7 @@ async function run() { const pChainExportTxnRequest = await walletClient.pChain.prepareExportTxn({ exportedOutputs: [ { - addresses: ["P-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz"], + addresses: [account.getXPAddress("P", "fuji")], // P-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz amount: 0.0001, }, ], @@ -48,7 +48,7 @@ async function run() { // Creating a import transaction request in C-chain const cChainImportTxnRequest = await walletClient.cChain.prepareImportTxn({ sourceChain: "P", - toAddress: "0x76Dd3d7b2f635c2547B861e55aE8A374E587742D", + toAddress: account.getEVMAddress(), // 0x76Dd3d7b2f635c2547B861e55aE8A374E587742D }); // Signing and sending the import transaction request to the C-chain diff --git a/client/examples/prepare-primary-network-txns/transfer-avax-from-p-chain-to-x-chain.ts b/client/examples/prepare-primary-network-txns/transfer-avax-from-p-chain-to-x-chain.ts index 569a859e..b54a034f 100644 --- a/client/examples/prepare-primary-network-txns/transfer-avax-from-p-chain-to-x-chain.ts +++ b/client/examples/prepare-primary-network-txns/transfer-avax-from-p-chain-to-x-chain.ts @@ -24,7 +24,7 @@ async function run() { const pChainExportTxnRequest = await walletClient.pChain.prepareExportTxn({ exportedOutputs: [ { - addresses: ["P-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz"], + addresses: [account.getXPAddress("P", "fuji")], // P-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz amount: 0.0111, }, ], @@ -46,7 +46,7 @@ async function run() { const xChainImportTxnRequest = await walletClient.xChain.prepareImportTxn({ sourceChain: "P", importedOutput: { - addresses: ["X-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz"], + addresses: [account.getXPAddress("X", "fuji")], // X-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz }, }); diff --git a/client/examples/prepare-primary-network-txns/transfer-avax-from-x-chain-to-c-chain.ts b/client/examples/prepare-primary-network-txns/transfer-avax-from-x-chain-to-c-chain.ts index 6fd902ce..2d873bca 100644 --- a/client/examples/prepare-primary-network-txns/transfer-avax-from-x-chain-to-c-chain.ts +++ b/client/examples/prepare-primary-network-txns/transfer-avax-from-x-chain-to-c-chain.ts @@ -24,7 +24,7 @@ async function run() { const xChainExportTxnRequest = await walletClient.xChain.prepareExportTxn({ exportedOutputs: [ { - addresses: ["X-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz"], + addresses: [account.getXPAddress("X", "fuji")], // X-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz amount: 0.0001, }, ], @@ -45,7 +45,7 @@ async function run() { // Creating a import transaction request in C-chain const cChainImportTxnRequest = await walletClient.cChain.prepareImportTxn({ sourceChain: "X", - toAddress: "0x76Dd3d7b2f635c2547B861e55aE8A374E587742D", + toAddress: account.getEVMAddress(), // 0x76Dd3d7b2f635c2547B861e55aE8A374E587742D }); // Signing and sending the import transaction request to the C-chain diff --git a/client/examples/prepare-primary-network-txns/transfer-avax-from-x-chain-to-p-chain.ts b/client/examples/prepare-primary-network-txns/transfer-avax-from-x-chain-to-p-chain.ts index cff7d4f4..cecb8837 100644 --- a/client/examples/prepare-primary-network-txns/transfer-avax-from-x-chain-to-p-chain.ts +++ b/client/examples/prepare-primary-network-txns/transfer-avax-from-x-chain-to-p-chain.ts @@ -24,7 +24,7 @@ async function run() { const xChainExportTxnRequest = await walletClient.xChain.prepareExportTxn({ exportedOutputs: [ { - addresses: ["X-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz"], + addresses: [account.getXPAddress("X", "fuji")], // X-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz amount: 0.001, }, ], @@ -46,7 +46,7 @@ async function run() { const pChainImportTxnRequest = await walletClient.pChain.prepareImportTxn({ sourceChain: "X", importedOutput: { - addresses: ["P-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz"], + addresses: [account.getXPAddress("P", "fuji")], // P-fuji19fc97zn3mzmwr827j4d3n45refkksgms4y2yzz }, }); diff --git a/client/examples/sendAvax.ts b/client/examples/sendAvax.ts new file mode 100644 index 00000000..fe18e34c --- /dev/null +++ b/client/examples/sendAvax.ts @@ -0,0 +1,60 @@ +import { createAvalancheWalletClient } from "@avalanche-sdk/client"; +import { privateKeyToAvalancheAccount } from "@avalanche-sdk/client/accounts"; +import { avalancheFuji } from "@avalanche-sdk/client/chains"; + +async function run() { + // This is the private key of the account that will be used to export the avax from the X-chain to the P-chain + const account = privateKeyToAvalancheAccount( + "0x67d127b32d4c3dccba8a4493c9d6506e6e1c7e0f08fd45aace29c9973c7fc2ce" + ); + + // This is the wallet client that will be used to export the avax from the X-chain to the P-chain + const walletClient = createAvalancheWalletClient({ + chain: avalancheFuji, + transport: { + type: "http", + }, + account, + }); + + // 1. Send avax to another address on C-chain from C-chain + // Creates a transaction, signs, sends and waits for the transaction to + // be confirmed on C-chain + const sendC2CResponse = await walletClient.send({ + to: "0x909d71Ed4090ac6e57E3645dcF2042f8c6548664", + amount: 0.001, + }); + console.log("sendC2CResponse", sendC2CResponse); + + // 2. Send avax to another address on P-chain from C-chain + // Creates a export and import transaction, signs, sends and waits for + // the transaction to be confirmed on C-chain and P-chain + const sendC2PResponse = await walletClient.send({ + to: "P-fuji1apmh7wxg3js48fhacfv5y9md9065jxuf8rtns7", + amount: 0.001, + destinationChain: "P", + }); + console.log("sendC2PResponse", sendC2PResponse); + + // 3. Send avax to another address on C-chain from P-chain + // Creates a export and import transaction, signs, sends and waits for + // the transaction to be confirmed on C-chain and P-chain + const sendP2CResponse = await walletClient.send({ + to: "0x909d71Ed4090ac6e57E3645dcF2042f8c6548664", + amount: 0.001, + sourceChain: "P", + destinationChain: "C", + }); + console.log("sendP2CResponse", sendP2CResponse); + + // 4. Send avax to another address on P-chain from P-chain + const sendP2PResponse = await walletClient.send({ + to: "P-fuji1apmh7wxg3js48fhacfv5y9md9065jxuf8rtns7", + amount: 0.001, + sourceChain: "P", + destinationChain: "P", + }); + console.log("sendP2PResponse", sendP2PResponse); +} + +run(); diff --git a/client/src/clients/decorators/avalancheWallet.ts b/client/src/clients/decorators/avalancheWallet.ts index 5470c956..83ec8265 100644 --- a/client/src/clients/decorators/avalancheWallet.ts +++ b/client/src/clients/decorators/avalancheWallet.ts @@ -1,9 +1,14 @@ import { Chain, Transport } from "viem"; import { getAccountPubKey } from "../../methods/wallet/getAccountPubKey.js"; +import { send } from "../../methods/wallet/send.js"; import { sendXPTransaction } from "../../methods/wallet/sendXPTransaction.js"; import { signXPMessage } from "../../methods/wallet/signXPMessage.js"; import { signXPTransaction } from "../../methods/wallet/signXPTransaction.js"; import { GetAccountPubKeyReturnType } from "../../methods/wallet/types/getAccountPubKey.js"; +import { + SendParameters, + SendReturnType, +} from "../../methods/wallet/types/send.js"; import { SendXPTransactionParameters, SendXPTransactionReturnType, @@ -207,6 +212,29 @@ export type AvalancheWalletActions = { * ``` */ waitForTxn: (args: WaitForTxnParameters) => Promise; + + /** + * Sends tokens from the source chain to the destination chain. + * + * @param args - The parameters for the transaction. {@link SendParameters} + * @returns The hashes of the transactions. {@link SendReturnType} + * + * @example + * ```ts + * import { createAvalancheWalletClient } from '@avalanche-sdk/client' + * import { avalanche } from '@avalanche-sdk/client/chains' + * + * const walletClient = createAvalancheWalletClient({ + * chain: avalanche, + * transport: { type: "http" }, + * }) + * + * const result = await walletClient.send({ + * amount: 1, + * to: "0x0000000000000000000000000000000000000000", + * }); + */ + send: (args: SendParameters) => Promise; }; export function avalancheWalletActions< @@ -218,5 +246,6 @@ export function avalancheWalletActions< signXPTransaction: (args) => signXPTransaction(client, args), getAccountPubKey: () => getAccountPubKey(client), waitForTxn: (args) => waitForTxn(client, args), + send: (args) => send(client, args), }; } diff --git a/client/src/methods/wallet/cChain/prepareImportTxn.ts b/client/src/methods/wallet/cChain/prepareImportTxn.ts index 6417a72a..5ac9ddc7 100644 --- a/client/src/methods/wallet/cChain/prepareImportTxn.ts +++ b/client/src/methods/wallet/cChain/prepareImportTxn.ts @@ -6,6 +6,7 @@ import { C_CHAIN_ALIAS } from "../../consts.js"; import { baseFee as getBaseFee } from "../../public/index.js"; import { getContextFromURI } from "../getContextFromURI.js"; import { + addOrModifyXPAddressesAlias, bech32AddressToBytes, getBech32AddressFromAccountOrClient, getChainIdFromAlias, @@ -53,8 +54,9 @@ export async function prepareImportTxn( const context = params.context || (await getContextFromURI(client)); const baseFee = await getBaseFee(client); - const fromAddresses = params.fromAddresses || []; - + const fromAddresses = + addOrModifyXPAddressesAlias(params.fromAddresses, C_CHAIN_ALIAS) || []; + console.log("fromAddresses", fromAddresses); if (fromAddresses.length === 0) { const paramAc = parseAvalancheAccount(account); const address = await getBech32AddressFromAccountOrClient( diff --git a/client/src/methods/wallet/index.ts b/client/src/methods/wallet/index.ts index a9ba3612..712a6049 100644 --- a/client/src/methods/wallet/index.ts +++ b/client/src/methods/wallet/index.ts @@ -1,5 +1,6 @@ export type { AvalancheWalletRpcSchema } from "./avalancheWalletRPCSchema.js"; export { getAccountPubKey } from "./getAccountPubKey.js"; +export { send } from "./send.js"; export { sendXPTransaction } from "./sendXPTransaction.js"; export { signXPMessage } from "./signXPMessage.js"; export { signXPTransaction } from "./signXPTransaction.js"; @@ -14,6 +15,11 @@ export type { GetAccountPubKeyErrorType, GetAccountPubKeyReturnType, } from "./types/getAccountPubKey.js"; +export type { + SendErrorType, + SendParameters, + SendReturnType, +} from "./types/send.js"; export type { SendXPTransactionErrorType, SendXPTransactionParameters, @@ -33,4 +39,5 @@ export type { WaitForTxnErrorType, WaitForTxnParameters, } from "./types/waitForTxn.js"; +export { addOrModifyXPAddressesAlias as addOrModifyXPAddressAlias } from "./utils.js"; export { waitForTxn } from "./waitForTxn.js"; diff --git a/client/src/methods/wallet/send.ts b/client/src/methods/wallet/send.ts new file mode 100644 index 00000000..c10aa257 --- /dev/null +++ b/client/src/methods/wallet/send.ts @@ -0,0 +1,64 @@ +import { AvalancheWalletCoreClient } from "../../clients/createAvalancheWalletCoreClient.js"; +import { transferCtoCChain } from "./transferUtils/transferCtoCChain.js"; +import { transferCtoPChain } from "./transferUtils/transferCtoPChain.js"; +import { transferPtoCChain } from "./transferUtils/transferPtoCChain.js"; +import { transferPtoPChain } from "./transferUtils/transferPtoPChain.js"; +import { SendParameters, SendReturnType } from "./types/send.js"; + +/** + * @description Sends tokens from the source chain to the destination chain. + * @param client - The client to use for the transaction. {@link AvalancheWalletCoreClient} + * @param params - The parameters for the transaction. {@link SendParameters} + * @returns The hashes of the transactions. {@link SendReturnType} + * + * @example + * ```ts + * import { AvalancheWalletCoreClient } from "@avalabs/wallet-core-client"; + * import { send } from "@avalabs/wallet-core-client/methods/wallet/send"; + * + * const client = new AvalancheWalletCoreClient({ + * network: "mainnet", + * transport: { + * type: "custom", + * provider: window.avalanche, + * }, + * }); + * + * const txHashes = await send(client, { + * amount: 1, + * to: "0x0000000000000000000000000000000000000000", + * }); + */ +export async function send( + client: AvalancheWalletCoreClient, + params: SendParameters +): Promise { + const { sourceChain = "C", destinationChain = "C", token = "AVAX" } = params; + + if (token !== "AVAX") { + throw new Error(`Invalid token: ${token}, only AVAX is supported.`); + } + + switch (sourceChain) { + case "C": + switch (destinationChain) { + case "C": + return transferCtoCChain(client, params); + case "P": + return transferCtoPChain(client, params); + default: + throw new Error(`Invalid destination chain: ${destinationChain}`); + } + case "P": + switch (destinationChain) { + case "P": + return transferPtoPChain(client, params); + case "C": + return transferPtoCChain(client, params); + default: + throw new Error(`Invalid destination chain: ${destinationChain}`); + } + default: + throw new Error(`Invalid source chain: ${sourceChain}`); + } +} diff --git a/client/src/methods/wallet/transferUtils/transferCtoCChain.ts b/client/src/methods/wallet/transferUtils/transferCtoCChain.ts new file mode 100644 index 00000000..46b5b0cc --- /dev/null +++ b/client/src/methods/wallet/transferUtils/transferCtoCChain.ts @@ -0,0 +1,95 @@ +import { Address, formatEther, parseEther } from "viem"; +import { + estimateGas, + getBalance, + getGasPrice, + prepareTransactionRequest, + sendRawTransaction, + sendTransaction, + signTransaction, + waitForTransactionReceipt, +} from "viem/actions"; +import { AvalancheWalletCoreClient } from "../../../clients/createAvalancheWalletCoreClient.js"; +import { SendParameters, SendReturnType } from "../types/send.js"; +import { getEVMAddressFromAccountOrClient } from "../utils.js"; + +export type TransferCtoCChainReturnType = SendReturnType; + +export type TransferCtoCChainParameters = SendParameters; + +export async function transferCtoCChain( + client: AvalancheWalletCoreClient, + params: TransferCtoCChainParameters +): Promise { + const isAccountProvided = !!params.account || !!client.account; + console.log("isAccountProvided", isAccountProvided); + // Get the current account C chain address + const currentAccountEVMAddress = + params.from || + (await getEVMAddressFromAccountOrClient(client, params.account)); + + const [estimateGasResponse, gasPrice, balance] = await Promise.all([ + formatEther( + await estimateGas(client, { + to: params.to as Address, + value: parseEther(params.amount.toString()), + account: currentAccountEVMAddress as Address, + } as any) + ), + formatEther(await getGasPrice(client)), + formatEther( + await getBalance(client, { + address: currentAccountEVMAddress as Address, + }) + ), + ]); + + const estimatedFee = Number(estimateGasResponse) * Number(gasPrice); + + if ( + Number(balance) < Number(estimatedFee) || + Number(balance) < Number(params.amount) + ) { + throw new Error( + `Insufficient balance: ${estimatedFee} AVAX is required, but only ${balance} AVAX is available` + ); + } + + // If the account is not provided, we can use the sendTransaction action to send the + // transaction (this expects a injected wallet to sign the transaction) + // Otherwise, we need to prepare the transaction request, sign it, and send it using + // sendRawTransaction + let txnHash; + if (!isAccountProvided) { + txnHash = await sendTransaction(client, { + to: params.to as Address, + value: parseEther(params.amount.toString()), + account: currentAccountEVMAddress as Address, + } as any); + } else { + const request = await prepareTransactionRequest(client, { + to: params.to as Address, + value: parseEther(params.amount.toString()), + account: currentAccountEVMAddress as Address, + } as any); + + const account = params.account?.evmAccount || client.account; + + const serializedTransaction = await signTransaction(client, { + ...request, + account, + } as any); + txnHash = await sendRawTransaction(client, { serializedTransaction }); + } + + await waitForTransactionReceipt(client, { hash: txnHash }); + + return { + txHashes: [ + { + txHash: txnHash, + chainAlias: "C", + }, + ], + }; +} diff --git a/client/src/methods/wallet/transferUtils/transferCtoPChain.ts b/client/src/methods/wallet/transferUtils/transferCtoPChain.ts new file mode 100644 index 00000000..10ce5693 --- /dev/null +++ b/client/src/methods/wallet/transferUtils/transferCtoPChain.ts @@ -0,0 +1,148 @@ +import { evm, pvm, utils } from "@avalabs/avalanchejs"; +import { formatEther } from "viem"; +import { getBalance, getTransactionCount } from "viem/actions"; +import { AvalancheWalletCoreClient } from "../../../clients/createAvalancheWalletCoreClient.js"; +import { getFeeState } from "../../pChain/getFeeState.js"; +import { baseFee as getBaseFee } from "../../public/baseFee.js"; +import { prepareExportTxn as prepareExportTxnCChain } from "../cChain/prepareExportTxn.js"; +import { getContextFromURI } from "../getContextFromURI.js"; +import { prepareImportTxn as prepareImportTxnPChain } from "../pChain/prepareImportTxn.js"; +import { sendXPTransaction } from "../sendXPTransaction.js"; +import { SendParameters, SendReturnType } from "../types/send.js"; +import { + avaxToNanoAvax, + bech32AddressToBytes, + getBech32AddressFromAccountOrClient, + getChainIdFromAlias, + getEVMAddressFromAccountOrClient, + nanoAvaxToAvax, +} from "../utils.js"; +import { waitForTxn } from "../waitForTxn.js"; + +export type TransferCtoPChainParameters = SendParameters; + +export type TransferCtoPChainReturnType = SendReturnType; +export async function transferCtoPChain( + client: AvalancheWalletCoreClient, + params: TransferCtoPChainParameters +): Promise { + const context = params.context || (await getContextFromURI(client)); + + // Get current account evm address + const currentAccountEVMAddress = + params.from || + (await getEVMAddressFromAccountOrClient(client, params.account)); + + // Get the current account P chain address + const currentAccountPChainAddress = await getBech32AddressFromAccountOrClient( + client, + params.account, + "P", + context.networkID === 5 ? "fuji" : "avax" + ); + + // Validate the P chain address + if (!params.to.startsWith("P-")) { + throw new Error("Invalid P chain address, it should start with P-"); + } + + // Prepare the C chain export txn and P chain import txn and get the fee for each + const [ + cChainExportTxnRequest, + pChainImportTxnRequest, + pChainFeeState, + baseFee, + txCount, + balance, + ] = await Promise.all([ + prepareExportTxnCChain(client, { + destinationChain: "P", + fromAddress: currentAccountEVMAddress, + exportedOutput: { + addresses: [currentAccountPChainAddress], + amount: params.amount, + }, + context, + }), + prepareImportTxnPChain(client, { + sourceChain: "C", + importedOutput: { + addresses: [params.to], + }, + context, + }), + getFeeState(client.pChainClient), + getBaseFee(client), + getTransactionCount(client, { + address: `0x${utils.strip0x(currentAccountEVMAddress)}`, + }), + formatEther( + await getBalance(client, { + address: `0x${utils.strip0x(currentAccountEVMAddress)}`, + }) + ), + ]); + + // Calculate the fee for the P chain import txn + const pChainImportTxnFee = pvm.calculateFee( + pChainImportTxnRequest.tx.getTx(), + context.platformFeeConfig.weights, + pChainFeeState.price + ); + + const cChainExportTxnFee = evm.estimateExportCost( + context, + BigInt(baseFee), + avaxToNanoAvax(params.amount), + getChainIdFromAlias("P", context.networkID), + utils.hexToBuffer(currentAccountEVMAddress), + [bech32AddressToBytes(params.to)], + BigInt(txCount) + ); + + // Check if user has enough balance + if (Number(balance) < params.amount) { + throw new Error( + `Insufficient balance: ${params.amount} AVAX is required, but only ${balance} AVAX is available` + ); + } + + // Calculate the total fee + const totalFee = pChainImportTxnFee + cChainExportTxnFee; + if (totalFee > avaxToNanoAvax(params.amount)) { + throw new Error( + `Transfer amount is too low: ${nanoAvaxToAvax( + totalFee + )} AVAX Fee is required, but only ${ + params.amount + } AVAX is being transferred` + ); + } + + // Sign and send the C chain export txn and wait for it. + const sendCChainExportTxn = await sendXPTransaction( + client, + cChainExportTxnRequest + ); + await waitForTxn(client, sendCChainExportTxn); + + // Sign and send the P chain import txn and wait for it. + const sendPChainImportTxn = await sendXPTransaction( + client, + pChainImportTxnRequest + ); + await waitForTxn(client, sendPChainImportTxn); + + return { + txHashes: [ + { + txHash: sendCChainExportTxn.txHash, + chainAlias: "C", + }, + { + txHash: sendPChainImportTxn.txHash, + chainAlias: "P", + }, + ], + }; +} diff --git a/client/src/methods/wallet/transferUtils/transferPtoCChain.ts b/client/src/methods/wallet/transferUtils/transferPtoCChain.ts new file mode 100644 index 00000000..f9b88ddb --- /dev/null +++ b/client/src/methods/wallet/transferUtils/transferPtoCChain.ts @@ -0,0 +1,131 @@ +import { pvm, utils } from "@avalabs/avalanchejs"; +import { isAddress } from "viem"; +import { AvalancheWalletCoreClient } from "../../../clients/createAvalancheWalletCoreClient.js"; +import { getBalance } from "../../pChain/getBalance.js"; +import { getFeeState } from "../../pChain/getFeeState.js"; +import { baseFee as getBaseFee } from "../../public/baseFee.js"; +import { prepareImportTxn as prepareImportTxnCChain } from "../cChain/prepareImportTxn.js"; +import { getContextFromURI } from "../getContextFromURI.js"; +import { prepareExportTxn as prepareExportTxnPChain } from "../pChain/prepareExportTxn.js"; +import { sendXPTransaction } from "../sendXPTransaction.js"; +import { SendParameters, SendReturnType } from "../types/send.js"; +import { + avaxToNanoAvax, + getBech32AddressFromAccountOrClient, + nanoAvaxToAvax, +} from "../utils.js"; +import { waitForTxn } from "../waitForTxn.js"; + +export type TransferPtoCChainParameters = SendParameters; + +export type TransferPtoCChainReturnType = SendReturnType; + +export async function transferPtoCChain( + client: AvalancheWalletCoreClient, + params: TransferPtoCChainParameters +): Promise { + const context = params.context || (await getContextFromURI(client)); + const isTestnet = context.networkID === 5; + + // Get the P chain address + let currentAccountPChainAddress = params.from; + if (!currentAccountPChainAddress) { + currentAccountPChainAddress = await getBech32AddressFromAccountOrClient( + client, + params.account, + "P", + isTestnet ? "fuji" : "avax" + ); + } + + if (!isAddress(params.to)) { + throw new Error("Invalid `to` address"); + } + + // Prepare the P chain export txn and C chain import txn and get the fee for each + const [ + pChainExportTxnRequest, + cChainImportTxnRequest, + pChainFeeState, + baseFee, + balance, + ] = await Promise.all([ + await prepareExportTxnPChain(client, { + exportedOutputs: [ + { addresses: [currentAccountPChainAddress], amount: params.amount }, + ], + destinationChain: "C", + context, + }), + prepareImportTxnCChain(client, { + fromAddresses: [currentAccountPChainAddress], + sourceChain: "P", + toAddress: params.to, + context, + }), + getFeeState(client.pChainClient), + getBaseFee(client), + nanoAvaxToAvax( + ( + await getBalance(client.pChainClient, { + addresses: [currentAccountPChainAddress], + }) + ).balance + ), + ]); + + // Calculate the fee for the P chain import txn + const pChainExportTxnFee = pvm.calculateFee( + pChainExportTxnRequest.tx.getTx(), + context.platformFeeConfig.weights, + pChainFeeState.price + ); + const cChainImportTxnFee = + BigInt(baseFee) * BigInt(utils.costCorethTx(cChainImportTxnRequest.tx)); + + // Check if user has enough balance + if (Number(balance) < params.amount) { + throw new Error( + `Insufficient balance: ${params.amount} AVAX is required, but only ${balance} AVAX is available` + ); + } + + // Calculate the total fee + const totalFee = pChainExportTxnFee + cChainImportTxnFee; + if (totalFee > avaxToNanoAvax(params.amount)) { + throw new Error( + `Transfer amount is too low: ${nanoAvaxToAvax( + totalFee + )} AVAX Fee is required, but only ${ + params.amount + } AVAX is being transferred` + ); + } + + // Send the P chain export txn + const sendPChainExportTxn = await sendXPTransaction( + client, + pChainExportTxnRequest + ); + await waitForTxn(client, sendPChainExportTxn); + + // Send the C chain import txn + const sendCChainImportTxn = await sendXPTransaction( + client, + cChainImportTxnRequest + ); + await waitForTxn(client, sendCChainImportTxn); + + return { + txHashes: [ + { + txHash: sendPChainExportTxn.txHash, + chainAlias: "P", + }, + { + txHash: sendCChainImportTxn.txHash, + chainAlias: "C", + }, + ], + }; +} diff --git a/client/src/methods/wallet/transferUtils/transferPtoPChain.ts b/client/src/methods/wallet/transferUtils/transferPtoPChain.ts new file mode 100644 index 00000000..36bbe553 --- /dev/null +++ b/client/src/methods/wallet/transferUtils/transferPtoPChain.ts @@ -0,0 +1,94 @@ +import { pvm } from "@avalabs/avalanchejs"; +import { AvalancheWalletCoreClient } from "../../../clients/createAvalancheWalletCoreClient.js"; +import { getBalance } from "../../pChain/getBalance.js"; +import { getFeeState } from "../../pChain/getFeeState.js"; +import { getContextFromURI } from "../getContextFromURI.js"; +import { prepareBaseTxn } from "../pChain/prepareBaseTxn.js"; +import { sendXPTransaction } from "../sendXPTransaction.js"; +import { SendParameters, SendReturnType } from "../types/send.js"; +import { + getBech32AddressFromAccountOrClient, + nanoAvaxToAvax, +} from "../utils.js"; +import { waitForTxn } from "../waitForTxn.js"; + +export type TransferPtoPChainParameters = SendParameters; + +export type TransferPtoPChainReturnType = SendReturnType; + +export async function transferPtoPChain( + client: AvalancheWalletCoreClient, + params: TransferPtoPChainParameters +): Promise { + const context = params.context || (await getContextFromURI(client)); + const isTestnet = context.networkID === 5; + + // Get the current account P chain address + const currentAccountPChainAddress = await getBech32AddressFromAccountOrClient( + client, + params.account, + "P", + isTestnet ? "fuji" : "avax" + ); + + // Validate the destination address + if (!params.to.startsWith("P-")) { + throw new Error("Invalid P chain address, it should start with P-"); + } + + // Prepare the base transaction and get the fee state and balance + const [baseTxnRequest, pChainFeeState, balance] = await Promise.all([ + prepareBaseTxn(client, { + fromAddresses: [currentAccountPChainAddress], + outputs: [ + { + addresses: [params.to], + amount: params.amount, + }, + ], + context, + }), + getFeeState(client.pChainClient), + nanoAvaxToAvax( + ( + await getBalance(client.pChainClient, { + addresses: [currentAccountPChainAddress], + }) + ).balance + ), + ]); + + // Calculate the fee for the base transaction + const baseTxnFee = pvm.calculateFee( + baseTxnRequest.tx.getTx(), + context.platformFeeConfig.weights, + pChainFeeState.price + ); + + if (Number(balance) < params.amount) { + throw new Error( + `Insufficient balance: ${params.amount} AVAX is required, but only ${balance} AVAX is available` + ); + } + + if (nanoAvaxToAvax(baseTxnFee) > params.amount) { + throw new Error( + `Transfer amount is too low: ${nanoAvaxToAvax( + baseTxnFee + )} AVAX Fee is required, but only ${ + params.amount + } AVAX is being transferred` + ); + } + + const sendBaseTxn = await sendXPTransaction(client, baseTxnRequest); + await waitForTxn(client, sendBaseTxn); + return { + txHashes: [ + { + txHash: sendBaseTxn.txHash, + chainAlias: "P", + }, + ], + }; +} diff --git a/client/src/methods/wallet/types/send.ts b/client/src/methods/wallet/types/send.ts new file mode 100644 index 00000000..28be27c2 --- /dev/null +++ b/client/src/methods/wallet/types/send.ts @@ -0,0 +1,67 @@ +import { Context as ContextType } from "@avalabs/avalanchejs"; +import { Address } from "viem"; +import { RequestErrorType } from "viem/utils"; +import { + AvalancheAccount, + XPAddress, +} from "../../../accounts/avalancheAccount.js"; +/** + * @description The parameters for the send method. + */ +export type SendParameters = { + /** + * The account to send the transaction from. + */ + account?: AvalancheAccount; + /** + * The amount of tokens to send in AVAX. + */ + amount: number; + /** + * The address to send the tokens to. If the destination chain is P, this should be a P chain address. If the destination chain is C, this should be a C chain address. + */ + to: Address | XPAddress; + /** + * The address to send the tokens from. Default is the account address. + */ + from?: Address | XPAddress | undefined; + /** + * The chain to send the tokens from. Default is C. Only P and C are supported. + */ + sourceChain?: "P" | "C" | undefined; + /** + * The chain to send the tokens to. Default is C. Only P and C are supported. + */ + destinationChain?: "P" | "C" | undefined; + /** + * The token to send. Default is AVAX. Only AVAX is supported. + */ + token?: "AVAX" | undefined; + /** + * Optional. The context to use for the transaction. If not provided, the context will be fetched. + */ + context?: ContextType.Context; +}; + +export type SendErrorType = RequestErrorType; + +/** + * @description The details of a transaction. + */ +export type TransactionDetails = { + /** + * The hash of the transaction. + */ + txHash: string; + /** + * The chain alias of the transaction. + */ + chainAlias: "P" | "C"; +}; + +export type SendReturnType = { + /** + * The hashes of the transactions. + */ + txHashes: TransactionDetails[]; +}; diff --git a/client/src/methods/wallet/utils.ts b/client/src/methods/wallet/utils.ts index 57b9f513..983f63cb 100644 --- a/client/src/methods/wallet/utils.ts +++ b/client/src/methods/wallet/utils.ts @@ -13,7 +13,8 @@ import { UnsignedTx, utils, } from "@avalabs/avalanchejs"; -import { Address as AddressType } from "viem"; +import { Account, Address as AddressType } from "viem"; +import { getAddresses } from "viem/actions"; import { AvalancheAccount, parseAvalancheAccount, @@ -57,7 +58,7 @@ export function getBaseUrl(client: AvalancheWalletCoreClient): string { } export async function getBech32AddressFromAccountOrClient( - client: AvalancheWalletCoreClient, // TODO: use this to fetch the default account + client: AvalancheWalletCoreClient, account: AvalancheAccount | AddressType | undefined, chainAlias: | typeof P_CHAIN_ALIAS @@ -67,7 +68,6 @@ export async function getBech32AddressFromAccountOrClient( ): Promise { const xpAcc = parseAvalancheAccount(account)?.xpAccount || client.xpAccount; - // TODO: if no account provided or xpAccount is not provided, fetch from wallet the default account if (!xpAcc) { const { xp } = await getAccountPubKey(client); return `${chainAlias}-${publicKeyToXPAddress(xp, hrp)}`; @@ -76,6 +76,28 @@ export async function getBech32AddressFromAccountOrClient( return `${chainAlias}-${publicKeyToXPAddress(xpAcc.publicKey, hrp)}`; } +export async function getEVMAddressFromAccountOrClient( + client: AvalancheWalletCoreClient, + account: AvalancheAccount | undefined +): Promise { + let currentAccountEVMAddress = + account?.getEVMAddress() || (client.account as never as Account)?.address; + + if (!currentAccountEVMAddress) { + const getAddressesResponse = await getAddresses(client); + if (getAddressesResponse.length === 0) { + throw new Error("No EVM address found from wallet"); + } + if (getAddressesResponse.length > 1) { + throw new Error( + "Multiple EVM addresses found from wallet, pass the from address" + ); + } + currentAccountEVMAddress = getAddressesResponse[0] as `0x${string}`; + } + return currentAccountEVMAddress; +} + export function evmAddressToBytes(address: string) { let evmAddress = address; if (!evmAddress.startsWith("0x")) { @@ -396,3 +418,32 @@ export function toTransferableOutput( output: output.output as unknown as TransferOutput, }; } + +function addOrModifyXPAddressAliasUtil( + address: string | undefined, + alias: string +) { + if (!address) { + return undefined; + } + if (address.startsWith(alias)) { + return address; + } + + const strippedAddress = + address.split("-").length === 1 ? address : address.split("-")[1]; + + return `${alias}-${strippedAddress}`; +} + +export function addOrModifyXPAddressesAlias( + address: string[] | undefined, + alias: string +) { + if (!address) { + return undefined; + } + return address + .map((addr) => addOrModifyXPAddressAliasUtil(addr, alias)) + .filter((addr) => addr !== undefined); +} diff --git a/client/src/utils/index.ts b/client/src/utils/index.ts index f09b2ed7..5ccef133 100644 --- a/client/src/utils/index.ts +++ b/client/src/utils/index.ts @@ -1,7 +1,7 @@ export { bls, secp256k1, utils } from "@avalabs/avalanchejs"; export { utf8ToBytes } from "@noble/hashes/utils"; export { CB58ToHex, hexToCB58 } from "./common.js"; -export { getTxFromBytes } from "./getTxFromBytes.js"; +export { getTxFromBytes, getUnsignedTxFromBytes } from "./getTxFromBytes.js"; export { getUtxoFromBytes } from "./getUtxoFromBytes.js"; export { getUtxosForAddress } from "./getUtxosForAddress.js";