@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
351 lines • 17.2 kB
JavaScript
;
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