@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
309 lines (277 loc) • 8.28 kB
text/typescript
/* eslint-disable no-console */
import { LiveConfig } from "@ledgerhq/live-config/LiveConfig";
import { CryptoCurrencyId } from "@ledgerhq/types-cryptoassets";
import { Account, AccountRaw } from "@ledgerhq/types-live";
import BigNumber from "bignumber.js";
import childProcess from "child_process";
import { existsSync, readFileSync, writeFileSync } from "fs";
import { promisify } from "util";
import { argv } from "yargs";
import { firstValueFrom, reduce } from "rxjs";
import { encodeAccountId, fromAccountRaw, toAccountRaw } from "../../account";
import { getAccountBridgeByFamily, getCurrencyBridge } from "../../bridge/impl";
import { liveConfig } from "../../config/sharedConfig";
import {
findCryptoCurrencyById,
getCryptoCurrencyById,
setSupportedCurrencies,
} from "../../currencies";
import { MigrationAddress, migrationAddresses as defaultAddresses } from "./addresses";
// mandatory to run the script
setSupportedCurrencies([
"bitcoin",
"ethereum",
"bsc",
"polkadot",
"ripple",
"litecoin",
"polygon",
"bitcoin_cash",
"stellar",
"dogecoin",
"cosmos",
"dash",
"tron",
"tezos",
"elrond", // NOTE: legacy 'multiversx' name, kept for compatibility
"ethereum_classic",
"zcash",
"decred",
"digibyte",
"algorand",
"avalanche_c_chain",
"qtum",
"bitcoin_gold",
"komodo",
"zencash",
"crypto_org",
"crypto_org_croeseid",
"celo",
"hedera",
"cardano",
"solana",
"osmosis",
"fantom",
"moonbeam",
"cronos",
"songbird",
"flare",
"near",
"optimism",
"arbitrum",
"rsk",
"bittorrent",
"energy_web",
"astar",
"metis",
"boba",
"moonriver",
"velas_evm",
"syscoin",
"axelar",
"stargaze",
"secret_network",
"umee",
"desmos",
"dydx",
"onomy",
"sei_network",
"persistence",
"quicksilver",
"vechain",
"internet_computer",
"klaytn",
"polygon_zk_evm",
"base",
"base_sepolia",
"stacks",
"telos_evm",
"sei_network_evm",
"berachain",
"hyperevm",
"coreum",
"injective",
"casper",
"neon_evm",
"lukso",
"filecoin",
"linea",
"ton",
"mina",
]);
LiveConfig.setConfig(liveConfig);
LiveConfig.setAppinfo({
environment: "ci",
platform: "headless-linux",
});
const args = argv;
type Args = {
/**
* comma seperated currencyIds
* eg: --currencies ethereum,polygon,bitcoin
*/
currencies?: CryptoCurrencyId;
/**
* absolute path for the output folder for the json file
* eg: --outputFolderPath ~/outputs/
*/
outputFolderPath?: string;
/**
* absolute path for the input json file
* must only contain an array of raw accounts
* eg: --inputFile ./rawAccounts/2d3fds.json
*/
inputFile?: string;
/**
* if set, no output file will be created
* eg: --noEmit
*/
noEmit?: boolean;
};
const { currencies, inputFile, noEmit, outputFolderPath } = args as Args;
const getMockAccount = (currencyId: string, address: string): Account => {
const currency = getCryptoCurrencyById(currencyId);
return {
type: "Account",
id: encodeAccountId({
type: "js",
version: "",
currencyId: currencyId,
xpubOrAddress: address,
derivationMode: "",
}),
freshAddress: address,
xpub: address,
derivationMode: "",
operations: [],
currency,
creationDate: new Date(0),
balance: new BigNumber(0),
spendableBalance: new BigNumber(0),
blockHeight: 0,
freshAddressPath: "",
seedIdentifier: "",
index: 0,
used: true,
operationsCount: 0,
pendingOperations: [],
lastSyncDate: new Date(0),
balanceHistoryCache: {
HOUR: { latestDate: 0, balances: [] },
DAY: { latestDate: 0, balances: [] },
WEEK: { latestDate: 0, balances: [] },
},
swapHistory: [],
};
};
const testSync = async (currencyId: string, xpubOrAddress: string) => {
console.log("starting sync on", currencyId, xpubOrAddress);
const mockAccount = getMockAccount(currencyId, xpubOrAddress);
const currency = getCryptoCurrencyById(currencyId);
const currencyBrige = getCurrencyBridge(currency);
const data = await currencyBrige.preload(currency);
currencyBrige.hydrate(data, currency);
const accountBrige = getAccountBridgeByFamily(mockAccount.currency!.family, mockAccount.id);
const syncedAccount = await firstValueFrom(
accountBrige
.sync(mockAccount, { paginationConfig: {}, blacklistedTokenIds: [] })
.pipe(reduce((acc, f: (arg0: Account) => Account) => f(acc), mockAccount)),
);
const accountRaw = toAccountRaw(syncedAccount);
console.log("finishing sync on", currencyId, xpubOrAddress);
return accountRaw;
};
const testSyncAccount = async (account: Account) => {
console.log("starting sync on", account.currency.id, account.xpub ?? account.freshAddress);
const currency = getCryptoCurrencyById(account.currency.id);
const currencyBrige = getCurrencyBridge(currency);
const data = await currencyBrige.preload(currency);
currencyBrige.hydrate(data, currency);
const accountBrige = getAccountBridgeByFamily(account.currency!.family, account.id);
const syncedAccount = await firstValueFrom(
accountBrige
.sync(account, { paginationConfig: {}, blacklistedTokenIds: [] })
.pipe(reduce((acc, f: (arg0: Account) => Account) => f(acc), account)),
);
const accountRaw = toAccountRaw(syncedAccount);
console.log("finishing sync on", account.currency.id, account.xpub ?? account.freshAddress);
return accountRaw;
};
(async () => {
// list of accounts from input file
const inputAccounts: AccountRaw[] = [];
if (inputFile) {
if (!existsSync(inputFile)) {
console.warn(
`Incorrect file path: ${inputFile} does not exist`,
"Will use default addresses instead",
);
} else {
// if there's a input file we parse it
inputAccounts.push(JSON.parse(readFileSync(inputFile, "utf8")));
}
}
const currencyIds = currencies?.split(",");
for (const currencyId of currencyIds || []) {
if (!findCryptoCurrencyById(currencyId)) {
continue;
}
}
// Basically the inputAccounts only exist after the second run
// So we first try to sync the default addresses
// And in the 2nd run we use the input file (which is the ouput of the first sync run)
const migrationAddresses = inputAccounts.length ? inputAccounts : defaultAddresses;
const filteredAddresses = (migrationAddresses as { currencyId: string }[]).filter(
({ currencyId }) => {
if (currencyIds) {
return currencyIds.includes(currencyId);
}
return true;
},
);
let syncedAccounts: PromiseSettledResult<AccountRaw>[] = [];
// if --inputFile we use the addresses from the input file otherwise from addresses.ts
if (inputAccounts.length) {
syncedAccounts = await Promise.allSettled(
(filteredAddresses as AccountRaw[]).map(rawAccount => {
const account = fromAccountRaw(rawAccount);
return testSyncAccount(account);
}),
);
} else {
syncedAccounts = await Promise.allSettled(
(filteredAddresses as MigrationAddress[]).map(migrationAddress => {
return testSync(
migrationAddress.currencyId,
migrationAddress.xpub ?? migrationAddress.address,
);
}),
);
}
const errors: string[] = [];
const response = syncedAccounts.map((account, i) => {
if (account.status === "fulfilled") {
return account.value;
} else {
// Promise.allSettled preserve order so it's safe to use filteredAddresses[i]
errors.push(`${filteredAddresses[i].currencyId} ${account.reason.stack}`);
}
});
if (errors.length) {
throw new Error(errors.map(err => `${err}`).join("\n\n"));
}
const exec = promisify(childProcess.exec);
const outputContent = JSON.stringify(response, null, 3);
if (!noEmit) {
// also used in account-migration.yml to know which input file to use
const { stdout } = await exec("git rev-parse --short HEAD");
const outputFilePath = outputFolderPath
? `${outputFolderPath}/${stdout.trim()}.json`
: `${stdout.trim()}.json`;
console.log("Writing output....");
writeFileSync(outputFilePath, outputContent);
console.log(`File created: ${outputFilePath}`);
}
console.log("Done");
return response;
})();