Skip to content
This repository has been archived by the owner on Jul 15, 2022. It is now read-only.

Commit

Permalink
Merge pull request #266 from LedgerHQ/rework-account-ordering
Browse files Browse the repository at this point in the history
New ordering functions and split into many files
  • Loading branch information
gre authored Jun 7, 2019
2 parents ba867b1 + 574300d commit 8bfb521
Show file tree
Hide file tree
Showing 11 changed files with 885 additions and 695 deletions.
693 changes: 0 additions & 693 deletions src/account.js

This file was deleted.

57 changes: 57 additions & 0 deletions src/account/accountId.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// @flow
import invariant from "invariant";
import type { AccountIdParams, CryptoCurrency, DerivationMode } from "../types";
import { asDerivationMode } from "../derivation";

function ensureNoColon(value: string, ctx: string): string {
invariant(
!value.includes(":"),
"AccountId '%s' component must not use colon",
ctx
);
return value;
}

export function encodeAccountId({
type,
version,
currencyId,
xpubOrAddress,
derivationMode
}: AccountIdParams) {
return `${ensureNoColon(type, "type")}:${ensureNoColon(
version,
"version"
)}:${ensureNoColon(currencyId, "currencyId")}:${ensureNoColon(
xpubOrAddress,
"xpubOrAddress"
)}:${ensureNoColon(derivationMode, "derivationMode")}`;
}

export function decodeAccountId(accountId: string): AccountIdParams {
invariant(typeof accountId === "string", "accountId is not a string");
const splitted = accountId.split(":");
invariant(splitted.length === 5, "invalid size for accountId");
const [type, version, currencyId, xpubOrAddress, derivationMode] = splitted;
return {
type,
version,
currencyId,
xpubOrAddress,
derivationMode: asDerivationMode(derivationMode)
};
}

// you can pass account because type is shape of Account
// wallet name is a lib-core concept that usually identify a pool of accounts with the same (seed, cointype, derivation scheme) config.
export function getWalletName({
seedIdentifier,
derivationMode,
currency
}: {
seedIdentifier: string,
derivationMode: DerivationMode,
currency: CryptoCurrency
}) {
return `${seedIdentifier}_${currency.id}_${derivationMode}`;
}
27 changes: 27 additions & 0 deletions src/account/accountName.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// @flow
import type { Account, CryptoCurrency, DerivationMode } from "../types";
import { getTagDerivationMode } from "../derivation";

const MAX_ACCOUNT_NAME_SIZE = 50;

export const getAccountPlaceholderName = ({
currency,
index,
derivationMode
}: {
currency: CryptoCurrency,
index: number,
derivationMode: DerivationMode
}) => {
const tag = getTagDerivationMode(currency, derivationMode);
return `${currency.name} ${index + 1}${tag ? ` (${tag})` : ""}`;
};

export const getNewAccountPlaceholderName = getAccountPlaceholderName; // same naming

export const validateNameEdition = (account: Account, name: ?string): string =>
(
(name || account.name || "").replace(/\s+/g, " ").trim() ||
account.name ||
getAccountPlaceholderName(account)
).slice(0, MAX_ACCOUNT_NAME_SIZE);
199 changes: 199 additions & 0 deletions src/account/addAccounts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// @flow
import type { Account, CryptoCurrency, DerivationMode } from "../types";
import { getEnv } from "../env";
import { validateNameEdition } from "./accountName";
import { isAccountEmpty } from "./helpers";
import { decodeAccountId } from "./accountId";

export const shouldShowNewAccount = (
currency: CryptoCurrency,
derivationMode: DerivationMode
) =>
derivationMode === ""
? !!getEnv("SHOW_LEGACY_NEW_ACCOUNT") || !currency.supportsSegwit
: derivationMode === "segwit" || derivationMode === "native_segwit";

export function canBeMigrated(account: Account) {
const { type, currencyId } = decodeAccountId(account.id);
// at the moment migrations requires experimental libcore
if (!getEnv("EXPERIMENTAL_LIBCORE")) return false;
return type === "ethereumjs" && currencyId === "ethereum"; // TODO remove currencyId match
}

// attempt to find an account in scanned accounts that satisfy a migration
export function findAccountMigration(
account: Account,
scannedAccounts: Account[]
): ?Account {
if (!canBeMigrated(account)) return;
const { type } = decodeAccountId(account.id);
if (type === "ethereumjs") {
return scannedAccounts.find(a => a.freshAddress === account.freshAddress);
}
}

export type AddAccountsSection = {
id: string,
selectable: boolean,
defaultSelected: boolean,
data: Account[]
};

export type AddAccountsSectionResult = {
sections: AddAccountsSection[],
alreadyEmptyAccount: ?Account
};

/**
* logic that for the Add Accounts sectioned list
*/
export function groupAddAccounts(
existingAccounts: Account[],
scannedAccounts: Account[],
context: {
scanning: boolean
}
): AddAccountsSectionResult {
const importedAccounts = [];
const importableAccounts = [];
const creatableAccounts = [];
const migrateAccounts = [];
let alreadyEmptyAccount;

const scannedAccountsWithoutMigrate = [...scannedAccounts];
existingAccounts.forEach(existingAccount => {
const migrate = findAccountMigration(
existingAccount,
scannedAccountsWithoutMigrate
);
if (migrate) {
migrateAccounts.push({
...migrate,
name: existingAccount.name
});
const index = scannedAccountsWithoutMigrate.indexOf(migrate);
if (index !== -1) {
scannedAccountsWithoutMigrate[index] =
scannedAccountsWithoutMigrate[
scannedAccountsWithoutMigrate.length - 1
];
scannedAccountsWithoutMigrate.pop();
}
}
});

scannedAccountsWithoutMigrate.forEach(acc => {
const existingAccount = existingAccounts.find(a => a.id === acc.id);
const empty = isAccountEmpty(acc);
if (existingAccount) {
if (empty) {
alreadyEmptyAccount = existingAccount;
}
importedAccounts.push(existingAccount);
} else if (empty) {
creatableAccounts.push(acc);
} else {
importableAccounts.push(acc);
}
});

const sections = [];

if (importableAccounts.length) {
sections.push({
id: "importable",
selectable: true,
defaultSelected: true,
data: importableAccounts
});
}
if (migrateAccounts.length) {
sections.push({
id: "migrate",
selectable: true,
defaultSelected: true,
data: migrateAccounts
});
}
if (!context.scanning || creatableAccounts.length) {
// NB if data is empty, need to do custom placeholder that depends on alreadyEmptyAccount
sections.push({
id: "creatable",
selectable: true,
defaultSelected: false,
data: creatableAccounts
});
}

if (importedAccounts.length) {
sections.push({
id: "imported",
selectable: false,
defaultSelected: false,
data: importedAccounts
});
}

return {
sections,
alreadyEmptyAccount
};
}

export type AddAccountsProps = {
existingAccounts: Account[],
scannedAccounts: Account[],
selectedIds: string[],
renamings: { [_: string]: string }
};

export function addAccounts({
scannedAccounts,
existingAccounts,
selectedIds,
renamings
}: AddAccountsProps): Account[] {
const newAccounts = [];

// scanned accounts that was selected
const selected = scannedAccounts.filter(a => selectedIds.includes(a.id));

// we'll search for potential migration and append to newAccounts
existingAccounts.forEach(existing => {
const migration = findAccountMigration(existing, selected);
if (migration) {
if (!newAccounts.includes(migration)) {
newAccounts.push(migration);
const index = selected.indexOf(migration);
if (index !== -1) {
selected[index] = selected[selected.length - 1];
selected.pop();
}
}
} else {
// we'll try to find an updated version of the existing account as opportunity to refresh the operations
const update = selected.find(a => a.id === existing.id);
if (update) {
// preserve existing name
newAccounts.push({ ...update, name: existing.name });
} else {
newAccounts.push(existing);
}
}
});

// append the new accounts
selected.forEach(acc => {
const alreadyThere = newAccounts.find(a => a.id === acc.id);
if (!alreadyThere) {
newAccounts.push(acc);
}
});

// apply the renaming
return newAccounts.map(a => {
const name = validateNameEdition(a, renamings[a.id]);
if (name) return { ...a, name };
return a;
});
}
98 changes: 98 additions & 0 deletions src/account/groupOperations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// @flow
import type {
Account,
TokenAccount,
Operation,
DailyOperations
} from "../types";
import { flattenAccounts } from "./helpers";

function startOfDay(t) {
return new Date(t.getFullYear(), t.getMonth(), t.getDate());
}

const emptyDailyOperations = { sections: [], completed: true };

type GroupOpsByDayOpts = {
count: number,
withTokenAccounts?: boolean
};

/**
* @memberof account
*/
export function groupAccountsOperationsByDay(
inputAccounts: Account[] | TokenAccount[] | (Account | TokenAccount)[],
{ count, withTokenAccounts }: GroupOpsByDayOpts
): DailyOperations {
const accounts = withTokenAccounts
? flattenAccounts(inputAccounts)
: inputAccounts;
// Track indexes of account.operations[] for each account
const indexes: number[] = Array(accounts.length).fill(0);
// Track indexes of account.pendingOperations[] for each account
const indexesPending: number[] = Array(accounts.length).fill(0);
// Returns the most recent operation from the account with current indexes
function getNextOperation(): ?Operation {
let bestOp: ?Operation;
let bestOpInfo = { accountI: 0, fromPending: false };
for (let i = 0; i < accounts.length; i++) {
const account = accounts[i];
// look in operations
const op = account.operations[indexes[i]];
if (op && (!bestOp || op.date > bestOp.date)) {
bestOp = op;
bestOpInfo = { accountI: i, fromPending: false };
}
// look in pending operations
if (account.type === "Account") {
const opP = account.pendingOperations[indexesPending[i]];
if (opP && (!bestOp || opP.date > bestOp.date)) {
bestOp = opP;
bestOpInfo = { accountI: i, fromPending: true };
}
}
}
if (bestOp) {
if (bestOpInfo.fromPending) {
indexesPending[bestOpInfo.accountI]++;
} else {
indexes[bestOpInfo.accountI]++;
}
}
return bestOp;
}

let op = getNextOperation();
if (!op) return emptyDailyOperations;
const sections = [];
let day = startOfDay(op.date);
let data = [];
for (let i = 0; i < count && op; i++) {
if (op.date < day) {
sections.push({ day, data });
day = startOfDay(op.date);
data = [op];
} else {
data.push(op);
}
op = getNextOperation();
}
sections.push({ day, data });
return {
sections,
completed: !op
};
}

/**
* Return a list of `{count}` operations grouped by day.
* @memberof account
*/
export function groupAccountOperationsByDay(
account: Account | TokenAccount,
arg: GroupOpsByDayOpts
): DailyOperations {
const accounts: (Account | TokenAccount)[] = [account];
return groupAccountsOperationsByDay(accounts, arg);
}
Loading

0 comments on commit 8bfb521

Please sign in to comment.