diff --git a/src/app/account.service.ts b/src/app/account.service.ts
index 0f95d29f..02545134 100644
--- a/src/app/account.service.ts
+++ b/src/app/account.service.ts
@@ -39,6 +39,29 @@ import { MetamaskService } from './metamask.service';
import { SigningService } from './signing.service';
export const ERROR_USER_NOT_FOUND = 'User not found';
+
+/**
+ * The key used to store the sub-account reverse lookup map in local storage.
+ * This map is used to look up the account number for a sub-account given the
+ * public key. Application developers provide the "owner" public key in certain
+ * scenarios (generating derived keys, for example), and we need to be able to
+ * look up the account number for that public key in order to generate the
+ * private key for signing. The structure of the map is:
+ *
+ * ```json
+ * {
+ * "subAccountPublicKey": {
+ * "lookupKey": "rootPublicKey",
+ * "accountNumber": 1
+ * }
+ * }
+ * ```
+ *
+ * For historical reasons, the "lookupKey" is the root public key, which is the
+ * sub-account generated for account number 0. This is the "root" account, and
+ * is used to store the common data for all accounts in a particular account
+ * group, including its mnemonic and all its sub-account account numbers.
+ */
const SUB_ACCOUNT_REVERSE_LOOKUP_KEY = 'subAccountReverseLookup';
export interface SubAccountReversLookupEntry {
@@ -62,13 +85,35 @@ export class AccountService {
private signingService: SigningService,
private metamaskService: MetamaskService
) {
+ /**
+ * We rebuild the sub-account reverse lookup map on every page load. This is
+ * to ensure there are no stale or missing entries in the map. The number of
+ * users in local storage is generally small, so this should not be a
+ * performance issue. If it does become a performance issue, we can consider
+ * a more sophisticated approach, but the number of users would need to be
+ * on the order of hundreds or thousands (very unlikely, and maybe literally
+ * impossible) before this would be a problem.
+ */
this.initializeSubAccountReverseLookup();
}
// Public Getters
- getPublicKeys(): any {
- return Object.keys(this.getRootLevelUsers());
+ getPublicKeys(): string[] {
+ const publicKeys: string[] = [];
+ const rootUsers = this.getRootLevelUsers();
+
+ Object.keys(rootUsers).forEach((publicKey) => {
+ publicKeys.push(publicKey);
+ const subAccounts = rootUsers[publicKey].subAccounts || [];
+ subAccounts.forEach((subAccount) => {
+ publicKeys.push(
+ this.getAccountPublicKeyBase58(publicKey, subAccount.accountNumber)
+ );
+ });
+ });
+
+ return publicKeys;
}
getAccountInfo(publicKey: string): PrivateUserInfo & SubAccountMetadata {
@@ -96,9 +141,16 @@ export class AccountService {
);
if (foundAccount) {
+ const keychain = this.cryptoService.getSubAccountKeychain(
+ rootUser.seedHex,
+ foundAccount.accountNumber
+ );
+ const subAccountSeedHex =
+ this.cryptoService.keychainToSeedHex(keychain);
info = {
...rootUser,
...foundAccount,
+ seedHex: subAccountSeedHex,
};
}
}
@@ -140,12 +192,13 @@ export class AccountService {
getEncryptedUsers(): { [key: string]: PublicUserInfo } {
const hostname = this.globalVars.hostname;
- const privateUsers = this.getRootLevelUsers();
+ const rootUsers = this.getRootLevelUsers();
const publicUsers: { [key: string]: PublicUserInfo } = {};
- for (const publicKey of Object.keys(privateUsers)) {
- const privateUser = privateUsers[publicKey];
- const accessLevel = this.getAccessLevel(publicKey, hostname);
+ for (const rootPublicKey of Object.keys(rootUsers)) {
+ const privateUser = rootUsers[rootPublicKey];
+
+ const accessLevel = this.getAccessLevel(rootPublicKey, hostname);
if (accessLevel === AccessLevel.None) {
continue;
}
@@ -166,19 +219,50 @@ export class AccountService {
privateUser.seedHex
);
- publicUsers[publicKey] = {
+ const commonFields = {
hasExtraText: privateUser.extraText?.length > 0,
btcDepositAddress: privateUser.btcDepositAddress,
ethDepositAddress: privateUser.ethDepositAddress,
version: privateUser.version,
- encryptedSeedHex,
network: privateUser.network,
loginMethod: privateUser.loginMethod || LoginMethod.DESO,
accessLevel,
+ };
+
+ publicUsers[rootPublicKey] = {
+ ...commonFields,
+ encryptedSeedHex,
accessLevelHmac,
derivedPublicKeyBase58Check: privateUser.derivedPublicKeyBase58Check,
encryptedMessagingKeyRandomness,
};
+
+ // To support sub-accounts for the legacy identity flow, we need to return
+ // a flat map of all users and their sub-accounts. Each sub-account has a
+ // unique seed hex that can be used for signing transactions, as well as a
+ // unique accessLevel hmac.
+ const subAccounts = privateUser.subAccounts || [];
+ subAccounts.forEach((subAccount) => {
+ const subAccountPublicKey = this.getAccountPublicKeyBase58(
+ rootPublicKey,
+ subAccount.accountNumber
+ );
+ const accountInfo = this.getAccountInfo(subAccountPublicKey);
+ const subAccountEncryptedSeedHex = this.cryptoService.encryptSeedHex(
+ accountInfo.seedHex,
+ hostname
+ );
+ const subAccountAccessLevelHmac = this.cryptoService.accessLevelHmac(
+ accessLevel,
+ accountInfo.seedHex
+ );
+
+ publicUsers[subAccountPublicKey] = {
+ ...commonFields,
+ encryptedSeedHex: subAccountEncryptedSeedHex,
+ accessLevelHmac: subAccountAccessLevelHmac,
+ };
+ });
}
return publicUsers;
@@ -244,12 +328,7 @@ export class AccountService {
.encode('array', true);
// Derived keys JWT with the same expiration as the derived key. This is needed for some backend endpoints.
- derivedJwt = this.signingService.signJWT(
- derivedSeedHex,
- 0, // NOTE: derived keys are always generated with account number 0.
- true,
- options
- );
+ derivedJwt = this.signingService.signJWT(derivedSeedHex, true, options);
} else {
// If the user has passed in a derived public key, use that instead.
// Don't define the derived seed hex (a private key presumably already exists).
@@ -261,12 +340,7 @@ export class AccountService {
}
// Compute the owner-signed JWT with the same expiration as the derived key. This is needed for some backend endpoints.
// In case of the metamask log-in, jwt will be signed by a derived key.
- jwt = this.signingService.signJWT(
- account.seedHex,
- account.accountNumber,
- isMetamask,
- options
- );
+ jwt = this.signingService.signJWT(account.seedHex, isMetamask, options);
// Generate new btc and eth deposit addresses for the derived key.
// const btcDepositAddress = this.cryptoService.keychainToBtcAddress(derivedKeychain, network);
@@ -362,11 +436,9 @@ export class AccountService {
);
}
} else {
- accessSignature = this.signingService.signHashes(
- account.seedHex,
- [accessHash],
- account.accountNumber
- )[0];
+ accessSignature = this.signingService.signHashes(account.seedHex, [
+ accessHash,
+ ])[0];
}
const {
messagingPublicKeyBase58Check,
@@ -504,25 +576,16 @@ export class AccountService {
mnemonic: string,
extraText: string,
network: Network,
- accountNumber: number,
- options: {
- google?: boolean;
+ {
+ lastLoginTimestamp,
+ loginMethod = LoginMethod.DESO,
+ }: {
+ lastLoginTimestamp?: number;
+ loginMethod?: LoginMethod;
} = {}
): string {
- // if the account number is provided, and it is greater than 0, this is a sub account.
- if (typeof accountNumber === 'number' && accountNumber > 0) {
- // We've already stored the sub account in the root user's subAccounts array,
- // so we can just return it's public key directly here.
- const seedHex = this.cryptoService.keychainToSeedHex(keychain);
- const keyPair = this.cryptoService.seedHexToKeyPair(
- seedHex,
- accountNumber
- );
- return this.cryptoService.publicKeyToDeSoPublicKey(keyPair, network);
- }
-
const seedHex = this.cryptoService.keychainToSeedHex(keychain);
- const keyPair = this.cryptoService.seedHexToKeyPair(seedHex, 0);
+ const keyPair = this.cryptoService.seedHexToKeyPair(seedHex);
const btcDepositAddress = this.cryptoService.keychainToBtcAddress(
// @ts-ignore TODO: add "identifier" to type definition
keychain.identifier,
@@ -530,11 +593,6 @@ export class AccountService {
);
const ethDepositAddress = this.cryptoService.publicKeyToEthAddress(keyPair);
- let loginMethod: LoginMethod = LoginMethod.DESO;
- if (options.google) {
- loginMethod = LoginMethod.GOOGLE;
- }
-
return this.addPrivateUser({
seedHex,
mnemonic,
@@ -544,12 +602,12 @@ export class AccountService {
network,
loginMethod,
version: PrivateUserVersion.V2,
- lastLoginTimestamp: Date.now(),
+ ...(lastLoginTimestamp && { lastLoginTimestamp }),
});
}
addUserWithSeedHex(seedHex: string, network: Network): string {
- const keyPair = this.cryptoService.seedHexToKeyPair(seedHex, 0);
+ const keyPair = this.cryptoService.seedHexToKeyPair(seedHex);
const helperKeychain = new HDKey();
helperKeychain.privateKey = Buffer.from(seedHex, 'hex');
// @ts-ignore TODO: add "identifier" to type definition
@@ -569,7 +627,6 @@ export class AccountService {
network,
loginMethod: LoginMethod.DESO,
version: PrivateUserVersion.V2,
- lastLoginTimestamp: Date.now(),
});
}
@@ -643,8 +700,7 @@ export class AccountService {
if (privateUser.version === PrivateUserVersion.V0) {
// Add ethDepositAddress field
const keyPair = this.cryptoService.seedHexToKeyPair(
- privateUser.seedHex,
- 0
+ privateUser.seedHex
);
privateUser.ethDepositAddress =
this.cryptoService.publicKeyToEthAddress(keyPair);
@@ -677,10 +733,7 @@ export class AccountService {
publicKey: string
): string {
const account = this.getAccountInfo(ownerPublicKeyBase58Check);
- const privateKey = this.cryptoService.seedHexToKeyPair(
- account.seedHex,
- account.accountNumber
- );
+ const privateKey = this.cryptoService.seedHexToKeyPair(account.seedHex);
const privateKeyBytes = privateKey.getPrivate().toBuffer(undefined, 32);
const publicKeyBytes = this.cryptoService.publicKeyToECBuffer(publicKey);
const sharedPx = ecies.derive(privateKeyBytes, publicKeyBytes);
@@ -725,11 +778,9 @@ export class AccountService {
let messagingKeySignature = '';
if (messagingKeyName === this.globalVars.defaultMessageKeyName) {
- messagingKeySignature = this.signingService.signHashes(
- account.seedHex,
- [messagingKeyHash],
- account.accountNumber
- )[0];
+ messagingKeySignature = this.signingService.signHashes(account.seedHex, [
+ messagingKeyHash,
+ ])[0];
}
return {
@@ -823,18 +874,9 @@ export class AccountService {
senderGroupKeyName: string,
recipientPublicKey: string,
message: string,
- options: {
- messagingKeyRandomness?: string;
- ownerPublicKeyBase58Check?: string;
- } = {}
+ messagingKeyRandomness?: string
): any {
- const { accountNumber = 0 } = options.ownerPublicKeyBase58Check
- ? this.getAccountInfo(options.ownerPublicKeyBase58Check)
- : {};
- const privateKey = this.cryptoService.seedHexToKeyPair(
- seedHex,
- accountNumber
- );
+ const privateKey = this.cryptoService.seedHexToKeyPair(seedHex);
const privateKeyBuffer = privateKey.getPrivate().toBuffer(undefined, 32);
const publicKeyBuffer =
@@ -846,7 +888,7 @@ export class AccountService {
privateEncryptionKey = this.getMessagingKeyForSeed(
seedHex,
senderGroupKeyName,
- options.messagingKeyRandomness
+ messagingKeyRandomness
);
}
@@ -865,16 +907,9 @@ export class AccountService {
// @param encryptedHexes : string[]
decryptMessagesLegacy(
seedHex: string,
- encryptedHexes: any,
- options: { ownerPublicKeyBase58Check?: string } = {}
+ encryptedHexes: any
): { [key: string]: any } {
- const { accountNumber = 0 } = options.ownerPublicKeyBase58Check
- ? this.getAccountInfo(options.ownerPublicKeyBase58Check)
- : {};
- const privateKey = this.cryptoService.seedHexToKeyPair(
- seedHex,
- accountNumber
- );
+ const privateKey = this.cryptoService.seedHexToKeyPair(seedHex);
const privateKeyBuffer = privateKey.getPrivate().toBuffer(undefined, 32);
const decryptedHexes: { [key: string]: any } = {};
@@ -897,21 +932,13 @@ export class AccountService {
seedHex: string,
encryptedMessages: EncryptedMessage[],
messagingGroups: MessagingGroup[],
- options: {
- messagingKeyRandomness?: string;
- ownerPublicKeyBase58Check?: string;
- } = {}
+ messagingKeyRandomness?: string,
+ ownerPublicKeyBase58Check?: string
): Promise<{ [key: string]: any }> {
- const { accountNumber = 0 } = options.ownerPublicKeyBase58Check
- ? this.getAccountInfo(options.ownerPublicKeyBase58Check)
- : {};
- const privateKey = this.cryptoService.seedHexToKeyPair(
- seedHex,
- accountNumber
- );
+ const privateKey = this.cryptoService.seedHexToKeyPair(seedHex);
const myPublicKey =
- options.ownerPublicKeyBase58Check ||
+ ownerPublicKeyBase58Check ||
this.cryptoService.privateKeyToDeSoPublicKey(
privateKey,
this.globalVars.network
@@ -1012,7 +1039,7 @@ export class AccountService {
this.getMessagingKeyForSeed(
seedHex,
myMessagingGroupMemberEntry.GroupMemberKeyName,
- options.messagingKeyRandomness
+ messagingKeyRandomness
);
privateEncryptionKey = this.signingService
.decryptGroupMessagingPrivateKeyToMember(
@@ -1035,7 +1062,7 @@ export class AccountService {
privateEncryptionKey = this.getMessagingKeyForSeed(
seedHex,
this.globalVars.defaultMessageKeyName,
- options.messagingKeyRandomness
+ messagingKeyRandomness
);
}
} catch (e: any) {
@@ -1062,7 +1089,7 @@ export class AccountService {
addPrivateUser(userInfo: PrivateUserInfo): string {
const privateUsers = this.getPrivateUsersRaw();
- const privateKey = this.cryptoService.seedHexToKeyPair(userInfo.seedHex, 0);
+ const privateKey = this.cryptoService.seedHexToKeyPair(userInfo.seedHex);
// Metamask login will be added with the master public key.
let publicKey = this.cryptoService.privateKeyToDeSoPublicKey(
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 5a11102f..2be3c923 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -73,10 +73,6 @@ export class AppComponent implements OnInit {
this.globalVars.authenticatedUsers = authenticatedUsers;
}
- if (params.get('subAccounts') === 'true') {
- this.globalVars.subAccounts = true;
- }
-
// Callback should only be used in mobile applications, where payload is passed through URL parameters.
const callback = params.get('callback') || stateParamsFromGoogle.callback;
if (callback) {
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index e59a2157..23933e92 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -1,6 +1,7 @@
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BrowserModule } from '@angular/platform-browser';
@@ -31,6 +32,10 @@ import { ErrorCallbackComponent } from './error-callback/error-callback.componen
import { FreeDeSoDisclaimerComponent } from './free-deso-message/free-deso-disclaimer/free-deso-disclaimer.component';
import { FreeDesoMessageComponent } from './free-deso-message/free-deso-message.component';
import { GetDesoComponent } from './get-deso/get-deso.component';
+import { BackupSeedDialogComponent } from './grouped-account-select/backup-seed-dialog/backup-seed-dialog.component';
+import { GroupedAccountSelectComponent } from './grouped-account-select/grouped-account-select.component';
+import { RecoverySecretComponent } from './grouped-account-select/recovery-secret/recovery-secret.component';
+import { RemoveAccountDialogComponent } from './grouped-account-select/remove-account-dialog/remove-account-dialog.component';
import { HomeComponent } from './home/home.component';
import { IconsModule } from './icons/icons.module';
import { IdentityService } from './identity.service';
@@ -98,6 +103,10 @@ import { TransactionSpendingLimitComponent } from './transaction-spending-limit/
TransactionSpendingLimitAssociationComponent,
TransactionSpendingLimitAccessGroupComponent,
TransactionSpendingLimitAccessGroupMemberComponent,
+ GroupedAccountSelectComponent,
+ RecoverySecretComponent,
+ BackupSeedDialogComponent,
+ RemoveAccountDialogComponent,
],
imports: [
BrowserModule,
@@ -114,6 +123,7 @@ import { TransactionSpendingLimitComponent } from './transaction-spending-limit/
}),
BuyDeSoComponentWrapper,
CookieModule.forRoot(),
+ MatDialogModule,
],
providers: [
IdentityService,
diff --git a/src/app/approve/approve.component.ts b/src/app/approve/approve.component.ts
index 9001d3d8..b8ed327d 100644
--- a/src/app/approve/approve.component.ts
+++ b/src/app/approve/approve.component.ts
@@ -105,8 +105,7 @@ export class ApproveComponent implements OnInit {
const signedTransactionHex = this.signingService.signTransaction(
account.seedHex,
this.transactionHex,
- isDerived,
- account.accountNumber
+ isDerived
);
this.finishFlow(signedTransactionHex);
}
diff --git a/src/app/auth/google/google.component.ts b/src/app/auth/google/google.component.ts
index 771dbd52..1d2eccdd 100644
--- a/src/app/auth/google/google.component.ts
+++ b/src/app/auth/google/google.component.ts
@@ -2,7 +2,7 @@ import { Component, NgZone, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subject } from 'rxjs';
import { environment } from '../../../environments/environment';
-import { GoogleAuthState } from '../../../types/identity';
+import { GoogleAuthState, LoginMethod } from '../../../types/identity';
import { AccountService } from '../../account.service';
import { RouteNames } from '../../app-routing.module';
import { BackendAPIService } from '../../backend-api.service';
@@ -93,9 +93,8 @@ export class GoogleComponent implements OnInit {
mnemonic,
extraText,
network,
- 0,
{
- google: true,
+ loginMethod: LoginMethod.GOOGLE,
}
);
} catch (err) {
@@ -149,9 +148,8 @@ export class GoogleComponent implements OnInit {
mnemonic,
extraText,
network,
- 0,
{
- google: true,
+ loginMethod: LoginMethod.GOOGLE,
}
);
this.loading = false;
diff --git a/src/app/backend-api.service.ts b/src/app/backend-api.service.ts
index 56fc4827..28ebd385 100644
--- a/src/app/backend-api.service.ts
+++ b/src/app/backend-api.service.ts
@@ -304,11 +304,7 @@ export class BackendAPIService {
}
const isDerived = this.accountService.isMetamaskAccount(account);
- const jwt = this.signingService.signJWT(
- account.seedHex,
- account.accountNumber,
- isDerived
- );
+ const jwt = this.signingService.signJWT(account.seedHex, isDerived);
return this.post(path, { ...body, ...{ JWT: jwt } });
}
@@ -349,7 +345,7 @@ export class BackendAPIService {
publicKeys: string[]
): Observable<{ [key: string]: UserProfile }> {
const userProfiles: { [key: string]: any } = {};
- const req = this.GetUsersStateless(publicKeys, true);
+ const req = this.GetUsersStateless(publicKeys, true, true);
if (publicKeys.length > 0) {
return req
.pipe(
@@ -358,6 +354,7 @@ export class BackendAPIService {
userProfiles[user.PublicKeyBase58Check] = {
username: user.ProfileEntryResponse?.Username,
profilePic: user.ProfileEntryResponse?.ProfilePic,
+ balanceNanos: user.BalanceNanos,
};
}
return userProfiles;
diff --git a/src/app/crypto.service.ts b/src/app/crypto.service.ts
index e1108ed2..07810bc0 100644
--- a/src/app/crypto.service.ts
+++ b/src/app/crypto.service.ts
@@ -142,49 +142,32 @@ export class CryptoService {
nonStandard?: boolean
): HDNode {
const seed = bip39.mnemonicToSeedSync(mnemonic, extraText);
- return deriveKeys(seed, 0, {
+ return generateSubAccountKeys(seed, 0, {
nonStandard,
});
}
getSubAccountKeychain(masterSeedHex: string, accountIndex: number): HDNode {
const seedBytes = Buffer.from(masterSeedHex, 'hex');
- return deriveKeys(seedBytes, accountIndex);
+ return generateSubAccountKeys(seedBytes, accountIndex);
}
keychainToSeedHex(keychain: HDNode): string {
return keychain.privateKey.toString('hex');
}
- /**
- * For a given parent seed hex and account number, return the corresponding private key. Public/private
- * key pairs are independent and unique based on a combination of the seed hex and account number.
- * @param parentSeedHex This is the seed hex used to generate multiple HD wallets/keys from a single seed.
- * @param accountNumber This is the account number used to generate unique keys from the parent seed.
- * @returns
- */
- seedHexToKeyPair(parentSeedHex: string, accountNumber: number): EC.KeyPair {
+ seedHexToKeyPair(seedHex: string): EC.KeyPair {
const ec = new EC('secp256k1');
- if (accountNumber === 0) {
- return ec.keyFromPrivate(parentSeedHex);
- }
-
- const hdKeys = this.getSubAccountKeychain(parentSeedHex, accountNumber);
- const seedHex = this.keychainToSeedHex(hdKeys);
-
return ec.keyFromPrivate(seedHex);
}
- encryptedSeedHexToPublicKey(
- encryptedSeedHex: string,
- accountNumber: number
- ): string {
+ encryptedSeedHexToPublicKey(encryptedSeedHex: string): string {
const seedHex = this.decryptSeedHex(
encryptedSeedHex,
this.globalVars.hostname
);
- const privateKey = this.seedHexToKeyPair(seedHex, accountNumber);
+ const privateKey = this.seedHexToKeyPair(seedHex);
return this.privateKeyToDeSoPublicKey(privateKey, this.globalVars.network);
}
@@ -309,7 +292,7 @@ export class CryptoService {
* m / purpose' / coin_type' / account' / change / address_index
* See for more details: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account
*/
-function deriveKeys(
+function generateSubAccountKeys(
seedBytes: Buffer,
accountIndex: number,
options?: { nonStandard?: boolean }
diff --git a/src/app/derive/derive.component.html b/src/app/derive/derive.component.html
index 44ec34cd..279a0bfa 100644
--- a/src/app/derive/derive.component.html
+++ b/src/app/derive/derive.component.html
@@ -21,14 +21,7 @@
}}
-
-
- or
-
-
+
0;
-
this.backendApi.GetAppState().subscribe((res) => {
this.blockHeight = res.BlockHeight;
});
@@ -69,6 +64,11 @@ export class DeriveComponent implements OnInit {
throw Error('invalid query parameter permutation');
}
if (params.publicKey) {
+ if (!this.publicKeyBase58Check) {
+ this.accountService.updateAccountInfo(params.publicKey, {
+ lastLoginTimestamp: Date.now(),
+ });
+ }
this.publicKeyBase58Check = params.publicKey;
this.isSingleAccount = true;
}
diff --git a/src/app/global-vars.service.ts b/src/app/global-vars.service.ts
index 050ff9de..e8fa4127 100644
--- a/src/app/global-vars.service.ts
+++ b/src/app/global-vars.service.ts
@@ -62,12 +62,6 @@ export class GlobalVarsService {
*/
showSkip: boolean = false;
- /**
- * Flag used to gate the new subAccounts functionality. After some sunset
- * period (TBD), we can remove this flag and make this the default behavior.
- */
- subAccounts: boolean = false;
-
/**
* Set of public keys that have been authenticated by the calling application.
* This is used as a hint to decide whether to show the derived key approval
@@ -180,4 +174,36 @@ export class GlobalVarsService {
formatTxCountLimit(count: number = 0): string {
return count >= 1e9 ? 'UNLIMITED' : count.toLocaleString();
}
+
+ abbreviateNumber(value: number) {
+ if (value === 0) {
+ return '0';
+ }
+
+ if (value < 0) {
+ return value.toString();
+ }
+ if (value < 0.01) {
+ return value.toFixed(5);
+ }
+ if (value < 0.1) {
+ return value.toFixed(4);
+ }
+
+ let shortValue;
+ const suffixes = ['', 'K', 'M', 'B', 'e12', 'e15', 'e18', 'e21'];
+ const suffixNum = Math.floor((('' + value.toFixed(0)).length - 1) / 3);
+ shortValue = value / Math.pow(1000, suffixNum);
+ if (
+ Math.floor(shortValue / 100) > 0 ||
+ shortValue / 1 === 0 ||
+ suffixNum > 3
+ ) {
+ return shortValue.toFixed(0) + suffixes[suffixNum];
+ }
+ if (Math.floor(shortValue / 10) > 0 || Math.floor(shortValue) > 0) {
+ return shortValue.toFixed(2) + suffixes[suffixNum];
+ }
+ return shortValue.toFixed(3) + suffixes[suffixNum];
+ }
}
diff --git a/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.html b/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.html
new file mode 100644
index 00000000..c5ddc562
--- /dev/null
+++ b/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.html
@@ -0,0 +1,94 @@
+
+
+
+
Backup DeSo Seed
+
+
+
+
+ Your seed phrase is the only way to recover your DeSo account. If you
+ lose your seed phrase, you will lose access to your DeSo account. Store
+ it in a safe and secure place.
+
+
+ DO NOT share your seed phrase with anyone! Support agents will never
+ request this.
+
+
+
+
+
+
+
Seed Phrase
+
+
+
+
Pass Phrase
+
+
+
+
Seed Hex
+
+ Provides an alternative means of logging in if you don't have a seed
+ phrase.
+
+
+
+
+
+ Disabling backup makes your account more secure by preventing anyone
+ from revealing your seed in the future, even if they've gained access to
+ your device.
+
+
+
+
+
+ Disabling backup means you will not be able to access your seed phrase
+ anymore.
+ Make sure that you've copied your seed phrase and stored it in a safe
+ place before you proceed.
+