UNPKG

@ledgerhq/live-common

Version:
331 lines • 16 kB
/* eslint-disable no-console */ import { customWrapper } from "@ledgerhq/wallet-api-server"; import { deserializeTransaction } from "@ledgerhq/wallet-api-core"; import { getParentAccount, getMainAccount, makeEmptyTokenAccount, isTokenAccount, } from "@ledgerhq/coin-framework/account/index"; import { findTokenById, findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets"; import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies"; import { getAccountIdFromWalletAccountId, getWalletAPITransactionSignFlowInfos, } from "../converters"; import { getAccountBridge } from "../../bridge"; import { UserRefusedOnDevice } from "@ledgerhq/errors"; import { getEnv } from "@ledgerhq/live-env"; import BigNumber from "bignumber.js"; // Helper function to validate Ethereum address format function isValidEthereumAddress(address) { return /^0x[a-fA-F0-9]{40}$/.test(address); } // Helper function to validate all inputs before account creation function validateInputs(params) { const { ethereumAddress, tokenContractAddress, tokenTicker, meta } = params; // Validate Ethereum address format if (!ethereumAddress) { throw new Error("Ethereum address is required"); } if (!isValidEthereumAddress(ethereumAddress)) { throw new Error("Invalid Ethereum address format"); } // Validate that at least one token identifier is provided if (!tokenContractAddress && !tokenTicker) { throw new Error("Either tokenContractAddress or tokenTicker must be provided"); } return { ethereumAddress, tokenContractAddress, tokenTicker, meta }; } // Helper function to find acreToken by address or token id function findAcreToken(tokenContractAddress, tokenTicker) { let foundToken; // Try to find token by contract address first (if provided) if (tokenContractAddress) { foundToken = findTokenByAddressInCurrency(tokenContractAddress, "ethereum"); } else if (tokenTicker) { foundToken = findTokenById(tokenTicker.toLowerCase()); } if (!foundToken) { throw new Error(`Token not found. Tried contract address: ${tokenContractAddress || "not provided"}, ticker: ${tokenTicker || "not provided"}`); } return { token: foundToken, contractAddress: foundToken.contractAddress }; } // Helper function to generate unique account names with suffixes // This is clearly a hack as we do not have account name on Account type, we leverage on how many accounts have acreBTC as token sub account to define the name // This is made to help user to identify different ACRE account but not resilient to token account being wiped, emptied // (empty subAccount would not been included in the list therefore parent account not considered as an acre account anymore). function generateUniqueAccountName(existingAccounts, baseName, tokenAddress) { const existingAccountWithAcreToken = existingAccounts.flatMap(account => account.subAccounts?.filter(subAccount => subAccount.token.contractAddress.toLowerCase() === tokenAddress.toLowerCase()) || []); return existingAccountWithAcreToken.length > 0 ? `${baseName} ${existingAccountWithAcreToken.length}` : baseName; } // Helper function to create parent Ethereum account function createParentAccount(ethereumAddress, ethereumCurrency) { return { type: "Account", id: `js:2:ethereum:${ethereumAddress}:`, seedIdentifier: `04${ethereumAddress.slice(2)}`, derivationMode: "", index: 0, freshAddress: ethereumAddress, freshAddressPath: "44'/60'/0'/0/0", used: false, blockHeight: 0, creationDate: new Date(), balance: new BigNumber(0), spendableBalance: new BigNumber(0), operationsCount: 0, operations: [], pendingOperations: [], currency: ethereumCurrency, lastSyncDate: new Date(), swapHistory: [], balanceHistoryCache: { HOUR: { latestDate: Date.now(), balances: [] }, DAY: { latestDate: Date.now(), balances: [] }, WEEK: { latestDate: Date.now(), balances: [] }, }, syncHash: "0x00000000", subAccounts: [], nfts: [], }; } export const handlers = ({ accounts, tracking, manifest, uiHooks: { "custom.acre.messageSign": uiMessageSign, "custom.acre.transactionSign": uiTransactionSign, "custom.acre.transactionBroadcast": uiTransactionBroadcast, "custom.acre.registerAccount": uiRegisterAccount, }, }) => { function signTransaction({ accountId: walletAccountId, rawTransaction, options, tokenCurrency, }) { const transaction = deserializeTransaction(rawTransaction); tracking.signTransactionRequested(manifest); if (!transaction) { tracking.signTransactionFail(manifest); return Promise.reject(new Error("Transaction required")); } const accountId = getAccountIdFromWalletAccountId(walletAccountId); if (!accountId) { tracking.signTransactionFail(manifest); return Promise.reject(new Error(`accountId ${walletAccountId} unknown`)); } const account = accounts.find(account => account.id === accountId); if (!account) { tracking.signTransactionFail(manifest); return Promise.reject(new Error("Account required")); } const parentAccount = getParentAccount(account, accounts); const accountFamily = isTokenAccount(account) ? account.token.parentCurrency.family : account.currency.family; const mainAccount = getMainAccount(account, parentAccount); const currency = tokenCurrency ? findTokenById(tokenCurrency) : null; const signerAccount = currency ? makeEmptyTokenAccount(mainAccount, currency) : account; const { canEditFees, liveTx, hasFeesProvided } = getWalletAPITransactionSignFlowInfos({ walletApiTransaction: transaction, account, }); if (accountFamily !== liveTx.family) { return Promise.reject(new Error(`Account and transaction must be from the same family. Account family: ${accountFamily}, Transaction family: ${liveTx.family}`)); } const signFlowInfos = { canEditFees, liveTx, hasFeesProvided, }; return new Promise((resolve, reject) => { let done = false; return uiTransactionSign({ account: signerAccount, parentAccount, signFlowInfos, options, onSuccess: signedOperation => { if (done) return; done = true; tracking.signTransactionSuccess(manifest); resolve(signedOperation); }, onError: error => { if (done) return; done = true; tracking.signTransactionFail(manifest); reject(error); }, }); }); } return { "custom.acre.messageSign": customWrapper(async (params) => { if (!params) { tracking.signMessageNoParams(manifest); // Maybe return an error instead return { hexSignedMessage: "" }; } tracking.signMessageRequested(manifest); const { accountId: walletAccountId, derivationPath, message, options } = params; const accountId = getAccountIdFromWalletAccountId(walletAccountId); if (!accountId) { tracking.signMessageFail(manifest); return Promise.reject(new Error(`accountId ${walletAccountId} unknown`)); } const account = accounts.find(account => account.id === accountId); if (account === undefined) { tracking.signMessageFail(manifest); return Promise.reject(new Error("account not found")); } const path = fromRelativePath(getMainAccount(account).freshAddressPath, derivationPath); const formattedMessage = { ...message, path }; return new Promise((resolve, reject) => { let done = false; return uiMessageSign({ account, message: formattedMessage, options, onSuccess: signature => { if (done) return; done = true; tracking.signMessageSuccess(manifest); resolve({ hexSignedMessage: signature.replace("0x", ""), }); }, onCancel: () => { if (done) return; done = true; tracking.signMessageFail(manifest); reject(new UserRefusedOnDevice()); }, onError: error => { if (done) return; done = true; tracking.signMessageFail(manifest); reject(error); }, }); }); }), "custom.acre.transactionSign": customWrapper(async (params) => { if (!params) { tracking.signTransactionNoParams(manifest); // Maybe return an error instead return { signedTransactionHex: "" }; } const signedOperation = await signTransaction(params); return { signedTransactionHex: Buffer.from(signedOperation.signature).toString("hex"), }; }), "custom.acre.transactionSignAndBroadcast": customWrapper(async (params) => { if (!params) { tracking.signTransactionAndBroadcastNoParams(manifest); // Maybe return an error instead return { transactionHash: "" }; } const signedOperation = await signTransaction(params); if (!signedOperation) { tracking.broadcastFail(manifest); return Promise.reject(new Error("Transaction required")); } const { accountId: walletAccountId, tokenCurrency } = params; const accountId = getAccountIdFromWalletAccountId(walletAccountId); if (!accountId) { tracking.broadcastFail(manifest); return Promise.reject(new Error(`accountId ${walletAccountId} unknown`)); } const account = accounts.find(account => account.id === accountId); if (!account) { tracking.broadcastFail(manifest); return Promise.reject(new Error("Account required")); } const currency = tokenCurrency ? findTokenById(tokenCurrency) : null; const parentAccount = getParentAccount(account, accounts); const mainAccount = getMainAccount(account, parentAccount); const signerAccount = currency ? makeEmptyTokenAccount(mainAccount, currency) : account; const bridge = getAccountBridge(signerAccount, parentAccount); const broadcastAccount = getMainAccount(signerAccount, parentAccount); let optimisticOperation = signedOperation.operation; if (!getEnv("DISABLE_TRANSACTION_BROADCAST")) { try { optimisticOperation = await bridge.broadcast({ account: broadcastAccount, signedOperation, }); tracking.broadcastSuccess(manifest); } catch (error) { tracking.broadcastFail(manifest); throw error; } } uiTransactionBroadcast && uiTransactionBroadcast(account, parentAccount, mainAccount, optimisticOperation); return { transactionHash: optimisticOperation.hash, }; }), "custom.acre.registerYieldBearingEthereumAddress": customWrapper(async (params) => { if (!params) { return Promise.reject(new Error("Parameters required")); } const validatedInputs = validateInputs(params); const ethereumCurrency = getCryptoCurrencyById("ethereum"); const { token: existingToken, contractAddress: finalTokenContractAddress } = findAcreToken(validatedInputs.tokenContractAddress, validatedInputs.tokenTicker); const existingBearingAccount = accounts.find(account => "freshAddress" in account && account.freshAddress === validatedInputs.ethereumAddress); const baseName = "Yield-bearing BTC on ACRE"; // Account already added, skip registration. if (existingBearingAccount) { return { success: true, accountName: baseName, parentAccountId: existingBearingAccount.id, tokenAccountId: existingBearingAccount.id, ethereumAddress: validatedInputs.ethereumAddress, tokenContractAddress: finalTokenContractAddress, meta: validatedInputs.meta, }; } if (uiRegisterAccount) { // Create account & manage the case where an ACRE account has been already registered on another Ethereum address // Filter to have only root accounts const existingParentAccounts = accounts.filter((acc) => acc.type === "Account"); const accountName = generateUniqueAccountName(existingParentAccounts, baseName, finalTokenContractAddress); const parentAccount = createParentAccount(validatedInputs.ethereumAddress, ethereumCurrency); const tokenAccount = makeEmptyTokenAccount(parentAccount, existingToken); // Add token account as sub-account of parent account const parentAccountWithSubAccount = { ...parentAccount, subAccounts: [tokenAccount], }; return new Promise((resolve, reject) => { uiRegisterAccount({ parentAccount: parentAccountWithSubAccount, accountName, existingAccounts: existingParentAccounts, onSuccess: () => { resolve({ success: true, accountName, parentAccountId: parentAccountWithSubAccount.id, tokenAccountId: tokenAccount.id, ethereumAddress: validatedInputs.ethereumAddress, tokenContractAddress: finalTokenContractAddress, meta: validatedInputs.meta, }); }, onError: error => { reject(error); }, }); }); } else { throw new Error("No account registration UI hook available"); } }), }; }; function fromRelativePath(basePath, derivationPath) { if (!derivationPath) { return basePath; } const splitPath = basePath.split("'/"); splitPath[splitPath.length - 1] = derivationPath; return splitPath.join("'/"); } //# sourceMappingURL=server.js.map