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.
+
class="section--seed__container margin-bottom--medium"
*ngIf="entropyService.temporaryEntropy.extraText.length > 0"
>
- Enter your passphrase:
+
+ Enter your passphrase:
+
0"
diff --git a/src/app/sign-up/sign-up.component.ts b/src/app/sign-up/sign-up.component.ts
index 74c18f96..f808c446 100644
--- a/src/app/sign-up/sign-up.component.ts
+++ b/src/app/sign-up/sign-up.component.ts
@@ -118,7 +118,9 @@ export class SignUpComponent implements OnInit, OnDestroy {
mnemonic,
extraText,
network,
- 0
+ {
+ lastLoginTimestamp: Date.now(),
+ }
);
this.accountService.setAccessLevel(
diff --git a/src/app/signing.service.ts b/src/app/signing.service.ts
index 1dfb28e3..b1ad64d3 100644
--- a/src/app/signing.service.ts
+++ b/src/app/signing.service.ts
@@ -20,24 +20,18 @@ export class SigningService {
signJWT(
seedHex: string,
- accountNumber: number,
isDerived: boolean,
{ expiration = 60 * 10 }: { expiration?: string | number } = {}
): string {
const keyEncoder = new KeyEncoder('secp256k1');
- // TODO: make sure the account number stuff works here...
- const acctNumber = isDerived ? 0 : accountNumber;
- const keys = this.cryptoService.seedHexToKeyPair(seedHex, acctNumber);
+ const keys = this.cryptoService.seedHexToKeyPair(seedHex);
const encodedPrivateKey = keyEncoder.encodePrivate(
keys.getPrivate('hex'),
'raw',
'pem'
);
if (isDerived) {
- const derivedPrivateKey = this.cryptoService.seedHexToKeyPair(
- seedHex,
- accountNumber
- );
+ const derivedPrivateKey = this.cryptoService.seedHexToKeyPair(seedHex);
const derivedPublicKeyBase58Check =
this.cryptoService.privateKeyToDeSoPublicKey(
derivedPrivateKey,
@@ -63,13 +57,9 @@ export class SigningService {
signTransaction(
seedHex: string,
transactionHex: string,
- isDerivedKey: boolean,
- accountNumber: number
+ isDerivedKey: boolean
): string {
- const privateKey = this.cryptoService.seedHexToKeyPair(
- seedHex,
- accountNumber
- );
+ const privateKey = this.cryptoService.seedHexToKeyPair(seedHex);
const transactionBytes = new Buffer(transactionHex, 'hex');
const [_, v1FieldsBuffer] = TransactionV0.fromBytes(transactionBytes) as [
@@ -98,15 +88,8 @@ export class SigningService {
]).toString('hex');
}
- signHashes(
- seedHex: string,
- unsignedHashes: string[],
- accountNumber: number
- ): string[] {
- const privateKey = this.cryptoService.seedHexToKeyPair(
- seedHex,
- accountNumber
- );
+ signHashes(seedHex: string, unsignedHashes: string[]): string[] {
+ const privateKey = this.cryptoService.seedHexToKeyPair(seedHex);
const signedHashes = [];
for (const unsignedHash of unsignedHashes) {
@@ -120,13 +103,9 @@ export class SigningService {
signHashesETH(
seedHex: string,
- unsignedHashes: string[],
- accountNumber: number
+ unsignedHashes: string[]
): { s: any; r: any; v: number | null }[] {
- const privateKey = this.cryptoService.seedHexToKeyPair(
- seedHex,
- accountNumber
- );
+ const privateKey = this.cryptoService.seedHexToKeyPair(seedHex);
const signedHashes = [];
for (const unsignedHash of unsignedHashes) {
diff --git a/src/lib/account-number.ts b/src/lib/account-number.ts
index c14bf0a4..777cade6 100644
--- a/src/lib/account-number.ts
+++ b/src/lib/account-number.ts
@@ -34,12 +34,18 @@ export function generateAccountNumber(accountNumbers: Set): number {
return candidate;
}
- // At most we look back 500 numbers. This is a bit arbitrary... but the
- // number of values could *technically* be 2^32 - 1, so we just limit the
+ // If we get here, it means a user must have manually entered the maximum
+ // allowed account number into the recover account field, and subsequently
+ // tried to add a new account. We'll get an error if we try to use the next
+ // incremented value (which is too big). We cannot use a static default
+ // fallback value because it could conflict with an account number we already
+ // have stored in local storage, so we look back for the first gap in the
+ // numbers. At most we look back 500 numbers. This is a bit arbitrary... but
+ // the number of values could *technically* be 2^32 - 1, so we just limit the
// number of iterations to some reasonable value. The reason we look back for
- // the highest available number instead of picking the lowest number is that
- // the lowest number is more likely to have been used in the past and we're
- // aiming to get a fresh wallet.
+ // the highest available number instead of picking the lowest available number
+ // is that the lowest number is more likely to have been used in the past and
+ // we're aiming to get a fresh/unused set of keys.
const maxLookBack = Math.max(sorted.length - 500, 0);
let nextExpectedValueInSequence = currentHighestAccountNumber - 1;
for (let i = sorted.length - 2; i >= maxLookBack; i--) {
diff --git a/src/styles.scss b/src/styles.scss
index 4082bdab..cd4eadb1 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -164,6 +164,7 @@ $colors: (
// SPACING UNITS
//-------------------------------
$spacing-units: (
+ 'auto': auto,
'none': 0,
'xsmall': 4px,
'small': 8px,
@@ -470,7 +471,7 @@ a,
@include color('blue', 'base', 'background-color');
@include color('neutral', 'white', 'color');
&:hover {
- @include color('blue', 'darker', 'background-color');
+ @include color('blue', 'dark', 'background-color');
}
}
.button--primary--outline {
@@ -480,6 +481,7 @@ a,
@include color('neutral', 'transparent', 'background-color');
@include color('text', 'lighter', 'color');
&:hover {
+ @include color('neutral', 'white', 'color');
@include color('blue', 'darker', 'background-color');
}
}
@@ -617,6 +619,17 @@ a,
flex-direction: row-reverse;
}
}
+
+//-------------------------------
+// POSITION
+//-------------------------------
+.relative {
+ position: relative;
+}
+.absolute {
+ position: absolute;
+}
+
//-------------------------------
// LAYOUT
//-------------------------------
@@ -790,6 +803,7 @@ a,
max-height: 245px;
overflow: scroll;
border: 1px solid;
+ scroll-behavior: smooth;
@include spacing('padding', 'medium');
@include spacing('padding-top', 'small');
@include color('blue', 'dark', 'border-color');
@@ -806,7 +820,6 @@ a,
@include spacing('padding-right', 'medium');
@include spacing('padding-top', 'small');
@include spacing('padding-bottom', 'small');
- @include spacing('margin-top', 'small');
@include color('text', 'lighter', 'color');
@include color('blue', 'darker', 'background-color');
@include color('blue', 'dark', 'border-color');
@@ -817,6 +830,11 @@ a,
@include color('blue', 'light', 'border-color');
cursor: pointer;
}
+
+ &--just-added {
+ @include color('blue', 'light', 'border-color');
+ @include color('blue', 'dark', 'background-color');
+ }
}
//-------------------------------
// SECTION = Create Seed Phrase
@@ -1162,3 +1180,47 @@ a,
background-color: rgba(0, 0, 0, 0.8) !important;
}
}
+
+.styleless-button {
+ border: none;
+ background: none;
+ padding: 0;
+ margin: 0;
+ cursor: pointer;
+}
+
+//-------------------------
+// Material Dialog
+//-------------------------
+.cdk-overlay-dark-backdrop {
+ background-color: rgba(0, 0, 0, 0.8) !important;
+}
+.dialog {
+ background-color: lighten(#040609, 10%);
+ border: 1px solid darkgray;
+ border-radius: 4px;
+ padding: 8px;
+}
+.dialog__header {
+ position: relative;
+ @include color('neutral', 'white', 'color');
+}
+.dialog__x {
+ border: none;
+ background: none;
+ position: absolute;
+ top: 0;
+ right: 0;
+ margin: 0;
+ padding: 0;
+ cursor: pointer;
+ @include color('neutral', 'white', 'color');
+}
+.dialog__title {
+ @include font-size('large');
+ @include color('neutral', 'white', 'color');
+ @include font-weight('bold');
+}
+.dialog__body {
+ padding: 12px 0;
+}
diff --git a/src/types/identity.ts b/src/types/identity.ts
index 06f71ba0..d39cd0a5 100644
--- a/src/types/identity.ts
+++ b/src/types/identity.ts
@@ -28,6 +28,13 @@ export interface PrivateUserInfo extends AccountMetadata {
*/
subAccounts?: SubAccountMetadata[];
+ /**
+ * Determines whether we display the "Back up your seed" button in the UI. We
+ * show it by default for all users, but we hide it for users who have
+ * explicitly disabled it.
+ */
+ exportDisabled?: boolean;
+
/** DEPRECATED in favor of loginMethod */
google?: boolean;
}