UNPKG

@ledgerhq/live-common

Version:
351 lines • 17.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.handlers = void 0; /* eslint-disable no-console */ const wallet_api_server_1 = require("@ledgerhq/wallet-api-server"); const wallet_api_core_1 = require("@ledgerhq/wallet-api-core"); const index_1 = require("@ledgerhq/ledger-wallet-framework/account/index"); const state_1 = require("@ledgerhq/cryptoassets/state"); const currencies_1 = require("@ledgerhq/cryptoassets/currencies"); const converters_1 = require("../converters"); const bridge_1 = require("../../bridge"); const errors_1 = require("@ledgerhq/errors"); const live_env_1 = require("@ledgerhq/live-env"); const bignumber_js_1 = __importDefault(require("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 async function findAcreToken(tokenContractAddress, tokenTicker) { let foundToken; // Try to find token by contract address first (if provided) if (tokenContractAddress) { foundToken = await (0, state_1.getCryptoAssetsStore)().findTokenByAddressInCurrency(tokenContractAddress, "ethereum"); } else if (tokenTicker) { foundToken = await (0, state_1.getCryptoAssetsStore)().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_js_1.default(0), spendableBalance: new bignumber_js_1.default(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", // Use proper hash format subAccounts: [], // Add empty subAccounts array nfts: [], }; } const handlers = ({ accounts, tracking, manifest, uiHooks: { "custom.acre.messageSign": uiMessageSign, "custom.acre.transactionSign": uiTransactionSign, "custom.acre.transactionBroadcast": uiTransactionBroadcast, "custom.acre.registerAccount": uiRegisterAccount, }, }) => { async function signTransaction({ accountId: walletAccountId, rawTransaction, options, tokenCurrency, }) { const transaction = (0, wallet_api_core_1.deserializeTransaction)(rawTransaction); tracking.signTransactionRequested(manifest); if (!transaction) { tracking.signTransactionFail(manifest); throw new Error("Transaction required"); } const accountId = (0, converters_1.getAccountIdFromWalletAccountId)(walletAccountId); if (!accountId) { tracking.signTransactionFail(manifest); throw new Error(`accountId ${walletAccountId} unknown`); } const account = accounts.find(account => account.id === accountId); if (!account) { tracking.signTransactionFail(manifest); throw new Error("Account required"); } const parentAccount = (0, index_1.getParentAccount)(account, accounts); const accountFamily = (0, index_1.isTokenAccount)(account) ? account.token.parentCurrency.family : account.currency.family; const mainAccount = (0, index_1.getMainAccount)(account, parentAccount); const currency = tokenCurrency ? await (0, state_1.getCryptoAssetsStore)().findTokenById(tokenCurrency) : null; const signerAccount = currency ? (0, index_1.makeEmptyTokenAccount)(mainAccount, currency) : account; const { canEditFees, liveTx, hasFeesProvided } = (0, converters_1.getWalletAPITransactionSignFlowInfos)({ walletApiTransaction: transaction, account, }); if (accountFamily !== liveTx.family) { throw 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": (0, wallet_api_server_1.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 = (0, converters_1.getAccountIdFromWalletAccountId)(walletAccountId); if (!accountId) { tracking.signMessageFail(manifest); throw new Error(`accountId ${walletAccountId} unknown`); } const account = accounts.find(account => account.id === accountId); if (account === undefined) { tracking.signMessageFail(manifest); throw new Error("account not found"); } const path = fromRelativePath((0, index_1.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 errors_1.UserRefusedOnDevice()); }, onError: error => { if (done) return; done = true; tracking.signMessageFail(manifest); reject(error instanceof Error ? error : new Error(String(error))); }, }); }); }), "custom.acre.transactionSign": (0, wallet_api_server_1.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": (0, wallet_api_server_1.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 = (0, converters_1.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 ? await (0, state_1.getCryptoAssetsStore)().findTokenById(tokenCurrency) : null; const parentAccount = (0, index_1.getParentAccount)(account, accounts); const mainAccount = (0, index_1.getMainAccount)(account, parentAccount); const signerAccount = currency ? (0, index_1.makeEmptyTokenAccount)(mainAccount, currency) : account; const bridge = (0, bridge_1.getAccountBridge)(signerAccount, parentAccount); const broadcastAccount = (0, index_1.getMainAccount)(signerAccount, parentAccount); const networkId = signerAccount.type === "TokenAccount" ? signerAccount.token.parentCurrency.id : signerAccount.currency.id; const broadcastTrackingData = { sourceCurrency: signerAccount.type === "TokenAccount" ? signerAccount.token.name : signerAccount.currency.name, network: networkId, }; let optimisticOperation = signedOperation.operation; if (!(0, live_env_1.getEnv)("DISABLE_TRANSACTION_BROADCAST")) { try { optimisticOperation = await bridge.broadcast({ account: broadcastAccount, signedOperation, }); tracking.broadcastSuccess(manifest, broadcastTrackingData); } catch (error) { tracking.broadcastFail(manifest, broadcastTrackingData); throw error; } } uiTransactionBroadcast && uiTransactionBroadcast(account, parentAccount, mainAccount, optimisticOperation); return { transactionHash: optimisticOperation.hash, }; }), "custom.acre.registerYieldBearingEthereumAddress": (0, wallet_api_server_1.customWrapper)(async (params) => { if (!params) { return Promise.reject(new Error("Parameters required")); } const validatedInputs = validateInputs(params); const ethereumCurrency = (0, currencies_1.getCryptoCurrencyById)("ethereum"); const { token: existingToken, contractAddress: finalTokenContractAddress } = await 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 = (0, index_1.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"); } }), }; }; exports.handlers = handlers; function fromRelativePath(basePath, derivationPath) { if (!derivationPath) { return basePath; } const splitPath = basePath.split("'/"); splitPath[splitPath.length - 1] = derivationPath; return splitPath.join("'/"); } //# sourceMappingURL=server.js.map