UNPKG

@ledgerhq/coin-filecoin

Version:
132 lines (118 loc) 4.39 kB
import type { CraftedTransaction, FeeEstimation, TransactionIntent, } from "@ledgerhq/coin-module-framework/api/index"; import { Message } from "iso-filecoin/message"; import { abiEncodeTransferParams, encodeTxnParams } from "../../erc20/tokenAccounts"; import { convertAddressFilToEth, validateAddress } from "../../network/addresses"; import { BroadcastBlockIncl } from "../../types"; import { fetchEstimatedFees } from "../../network/api"; import { getNextSequence } from "../account/getNextSequence"; // Inline method numbers from Filecoin spec (mirrors src/bridge/utils.ts Methods enum). // logic/ must not import from bridge/, so we inline the relevant constants here. const METHOD_TRANSFER = 0; const METHOD_INVOKE_EVM = 3844450837; // Serialisation format (symmetric with combine.ts): // Output `transaction` field = JSON.stringify({ cbor, message }) where // - cbor: base64-encoded CBOR of the Filecoin Message (what the signer signs) // - message: the message fields in Lotus shape (what combine uses to rebuild // the BroadcastTransactionRequest without re-deserialising CBOR — iso-filecoin // v4 exposes Message.serialize() but no static deserialize equivalent). export async function craftTransaction( intent: TransactionIntent, customFees?: FeeEstimation, ): Promise<CraftedTransaction> { const senderValidation = validateAddress(intent.sender); if (!senderValidation.isValid) { throw new Error(`Invalid sender address: ${intent.sender}`); } const from = senderValidation.parsedAddress.toString(); const nonce = await getNextSequence(from); let to: string; let value: string; let method: number; let params: string | undefined; const assetType = intent.asset.type; if (assetType === "native") { const recipientValidation = validateAddress(intent.recipient ?? ""); if (!recipientValidation.isValid) { throw new Error(`Invalid recipient address: ${intent.recipient}`); } to = recipientValidation.parsedAddress.toString(); value = intent.amount.toString(); method = METHOD_TRANSFER; } else if (assetType === "erc20") { const asset = intent.asset as { type: "erc20"; assetReference: string }; const contractAddr = asset.assetReference.toLowerCase(); const contractValidation = validateAddress(contractAddr); if (!contractValidation.isValid) { throw new Error(`Invalid ERC-20 contract address: ${contractAddr}`); } to = contractValidation.parsedAddress.toString(); value = "0"; method = METHOD_INVOKE_EVM; const recipientEth = intent.recipient?.startsWith("0x") ? intent.recipient : (() => { try { return convertAddressFilToEth(intent.recipient ?? ""); } catch { throw new Error(`Invalid token recipient: ${intent.recipient}`); } })(); const abiEncoded = abiEncodeTransferParams(recipientEth, intent.amount.toString()); params = encodeTxnParams(abiEncoded); } else { throw new Error(`Unsupported asset type: ${String(assetType)}`); } // Fetch gas estimates (or use custom fees if provided) let gasFeeCap: string; let gasLimit: number; let gasPremium: string; if (customFees?.parameters) { gasFeeCap = String(customFees.parameters["gasFeeCap"] ?? "0"); gasLimit = Number(customFees.parameters["gasLimit"] ?? 0); gasPremium = String(customFees.parameters["gasPremium"] ?? "0"); } else { const fees = await fetchEstimatedFees({ from, to, methodNum: method, blockIncl: BroadcastBlockIncl, value, ...(params ? { params } : {}), }); gasFeeCap = fees.gas_fee_cap; gasLimit = fees.gas_limit; gasPremium = fees.gas_premium; } const message = new Message({ to, from, nonce: Number(nonce), value, gasFeeCap, gasLimit, gasPremium, method, params, version: 0, }); const cbor = Buffer.from(message.serialize()).toString("base64"); const messageJson = { version: message.version ?? 0, to: message.to, from: message.from, nonce: message.nonce, value: message.value, gasLimit: message.gasLimit, gasFeeCap: message.gasFeeCap, gasPremium: message.gasPremium, method: message.method, params: message.params ?? "", }; return { transaction: JSON.stringify({ cbor, message: messageJson }), }; }