Skip to content

Commit

Permalink
feature: new component for sub account UI
Browse files Browse the repository at this point in the history
  • Loading branch information
jackson-dean committed Sep 20, 2023
1 parent 67c2635 commit c33b8df
Show file tree
Hide file tree
Showing 6 changed files with 517 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ 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 { GroupedAccountSelectComponent } from './grouped-account-select/grouped-account-select.component';
import { HomeComponent } from './home/home.component';
import { IconsModule } from './icons/icons.module';
import { IdentityService } from './identity.service';
Expand Down Expand Up @@ -98,6 +99,7 @@ import { TransactionSpendingLimitComponent } from './transaction-spending-limit/
TransactionSpendingLimitAssociationComponent,
TransactionSpendingLimitAccessGroupComponent,
TransactionSpendingLimitAccessGroupMemberComponent,
GroupedAccountSelectComponent,
],
imports: [
BrowserModule,
Expand Down
51 changes: 51 additions & 0 deletions src/app/grouped-account-select/account-number.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#extended-keys
* The max value that should be used for an hd key account number is 2^31 -1. In
* practice, we would never reach this via pure incremental account number
* generation, but we allow entering arbitrary account numbers in the UI, so we
* need to explicitly validate them. For whatever reason, the hdkey library
* we use throws for anything over 2^30.
*/
const MAX_UNSIGNED_INT_VALUE = 1073741824; // 2^30

export function isValid32BitUnsignedInt(value: number) {
return Number.isInteger(value) && value >= 0 && value <= MAX_UNSIGNED_INT_VALUE;
}

/**
* - The lowest possible sub-account number is 1 (0 is reserved for the "root" account).
* - If the set is empty, we just return 1.
* - Otherwise, we look for the highest number in the set and increment it by 1.
* - If the next incremented number is too big, we look back for the first gap in the numbers and use that instead.
* - If no gap is found (very unlikely), we throw an error.
*/
export function generateAccountNumber(accountNumbers: Set<number>): number {
if (accountNumbers.size === 0) {
return 1;
}

const sorted = Array.from(accountNumbers).sort((a, b) => a - b);
const currentHighestAccountNumber = sorted[sorted.length - 1];
const candidate = currentHighestAccountNumber + 1;

if (candidate <= MAX_UNSIGNED_INT_VALUE) {
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
// 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.
const maxLookBack = Math.max(sorted.length - 500, 0);
let nextExpectedValueInSequence = currentHighestAccountNumber - 1;
for (let i = sorted.length - 2; i >= maxLookBack; i--) {
if (nextExpectedValueInSequence !== sorted[i]) {
return nextExpectedValueInSequence;
}
nextExpectedValueInSequence--;
}

throw new Error('Cannot generate account number.');
}
138 changes: 138 additions & 0 deletions src/app/grouped-account-select/grouped-account-select.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<div class="section--accounts" *ngIf="hasVisibleAccounts">
<div class="section--accounts__header margin-bottom--medium">
Select an account
</div>

<ng-container
*ngFor="let group of accountGroups | keyvalue : keyValueSort; let i = index"
>
<div *ngIf="group.value.accounts.length" class="margin-bottom--large">
<ul
class="section--accounts__list container--scrollbar margin-bottom--small"
>
<li
*ngFor="let account of group.value.accounts; let j = index"
data-control-name="account-select-item"
>
<div class="display--flex items--center margin-top--small">
<button
class="section--accounts__item margin-right--small"
(click)="selectAccount(account.publicKey)"
role="button"
>
<div class="display--flex items--center">
<img
class="avatar--rounded avatar--large margin-right--small"
[appAvatar]="account.publicKey"
/>
<div>
<div>
<span [title]="getAccountDisplayName(account)">{{
getAccountDisplayName(account) | truncateAddressOrUsername
}}</span>
<span
*ngIf="account.lastUsed && !this.globalVars.isMobile()"
class="font-size--xsmall text--green-lighter margin-left--small"
>Last used</span
>
</div>
<div *ngIf="this.globalVars.isMobile()" class="display--flex">
<span class="font-size--xsmall"
>Account: {{ account.accountNumber }}</span
>
<span
*ngIf="account.lastUsed"
class="font-size--xsmall text--green-lighter margin-left--auto"
>Last used</span
>
</div>
</div>
</div>
<div class="display--flex items--center">
<div
*ngIf="!this.globalVars.isMobile()"
class="display--flex flex--column items--end"
>
<span class="font-size--xsmall margin-right--small"
>Account: {{ account.accountNumber }}</span
>
</div>
<img
[src]="
getLoginMethodIcon(
accountService.getLoginMethodWithPublicKeyBase58Check(
group.key
)
)
"
class="section--accounts__icon"
/>
</div>
</button>
<button
*ngIf="!isMetaMaskAccountGroup(group.key)"
class="styleless-button"
(click)="hideAccount(group.key, account)"
>
<i-feather name="power" class="text--blue-base"></i-feather>
</button>
</div>
</li>
</ul>
<div *ngIf="!isMetaMaskAccountGroup(group.key)">
<div class="display--flex">
<button
class="button--small button--primary display--flex items--center margin-right--small"
style="width: auto; white-space: nowrap"
aria-label="Add account"
(click)="addSubAccount(group.key)"
>
<i-feather
name="user-plus"
class="margin-right--xsmall"
style="height: 16px; width: 16px"
/>
Account
</button>
<button
class="button--small button--secondary display--flex items--center"
style="width: auto; white-space: nowrap"
(click)="toggleRecoverSubAccountForm(group.key)"
>
<i-feather
name="power"
class="margin-right--xsmall"
style="height: 16px; width: 16px"
/>
Recover
</button>
</div>
<form
*ngIf="group.value.showRecoverSubAccountInput"
(submit)="recoverSubAccount($event, group.key)"
class="margin-top--small margin-left--small"
>
<label for="account-number" class="display--block font-size--small"
>Enter account number:</label
>
<input
[(ngModel)]="accountNumberToRecover"
name="account-number"
id="account-number"
type="number"
step="1"
/>
<button
type="submit"
class="button--primary button--small margin-left--small"
style="width: auto; white-space: nowrap"
>
Submit
</button>
</form>
</div>
</div>
</ng-container>
</div>
<div *ngIf="hasVisibleAccounts" class="text--divider margin-top--medium margin-bottom--medium">or</div>
<app-log-in-options></app-log-in-options>
Empty file.
Loading

0 comments on commit c33b8df

Please sign in to comment.