Skip to content

Commit

Permalink
Merge pull request #1544 from input-output-hk/feat/async-wallet-repos…
Browse files Browse the repository at this point in the history
…itory-store-init

feat(web-extension)!: support async WalletRepository store initialization
  • Loading branch information
mkazlauskas authored Dec 6, 2024
2 parents 752012d + b64641a commit 3ec9198
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const storesFactory: StoresFactory = {

const walletRepository = new WalletRepository<Metadata, Metadata>({
logger,
store: new storage.InMemoryCollectionStore()
store$: of(new storage.InMemoryCollectionStore<AnyWallet<Metadata, Metadata>>())
});

const signingCoordinatorApi = consumeSigningCoordinatorApi({ logger, runtime });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import {
import { AnyWallet, ScriptWallet, WalletId, WalletType } from '../types';
import { KeyPurpose } from '@cardano-sdk/key-management';
import { Logger } from 'ts-log';
import { Observable, defer, firstValueFrom, map, shareReplay, switchMap, take } from 'rxjs';
import { NEVER, Observable, concat, defer, firstValueFrom, map, mergeMap, shareReplay, switchMap, take } from 'rxjs';
import { TrackerSubject, blockingWithLatestFrom } from '@cardano-sdk/util-rxjs';
import { WalletConflictError } from '../errors';
import { contextLogger } from '@cardano-sdk/util';
import { getWalletId } from '../util';
import { storage } from '@cardano-sdk/wallet';

export interface WalletRepositoryDependencies<WalletMetadata extends {}, AccountMetadata extends {}> {
store: storage.CollectionStore<AnyWallet<WalletMetadata, AccountMetadata>>;
store$: Observable<storage.CollectionStore<AnyWallet<WalletMetadata, AccountMetadata>>>;
logger: Logger;
}

Expand Down Expand Up @@ -56,13 +57,13 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends
implements WalletRepositoryApi<WalletMetadata, AccountMetadata>
{
readonly #logger: Logger;
readonly #store: WalletRepositoryDependencies<WalletMetadata, AccountMetadata>['store'];
readonly #store$: WalletRepositoryDependencies<WalletMetadata, AccountMetadata>['store$'];
readonly wallets$: Observable<AnyWallet<WalletMetadata, AccountMetadata>[]>;

constructor({ logger, store }: WalletRepositoryDependencies<WalletMetadata, AccountMetadata>) {
this.#store = store;
constructor({ logger, store$ }: WalletRepositoryDependencies<WalletMetadata, AccountMetadata>) {
this.#store$ = new TrackerSubject(concat(store$, NEVER));
this.#logger = contextLogger(logger, 'WalletRepository');
this.wallets$ = defer(() => store.observeAll()).pipe(shareReplay(1));
this.wallets$ = defer(() => this.#store$.pipe(mergeMap((store) => store.observeAll()))).pipe(shareReplay(1));
}

#getWallets() {
Expand All @@ -88,14 +89,15 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends

return firstValueFrom(
this.#getWallets().pipe(
switchMap((wallets) => {
blockingWithLatestFrom(this.#store$),
switchMap(([wallets, store]) => {
if (wallets.some((wallet) => wallet.walletId === walletId)) {
throw new WalletConflictError(`Wallet '${walletId}' already exists`);
}
if (props.type === WalletType.Script) {
this.#validateOwnSigners(wallets, props.ownSigners);
}
return this.#store.setAll([...wallets, { ...props, walletId }]);
return store.setAll([...wallets, { ...props, walletId }]);
}),
map(() => walletId)
)
Expand All @@ -109,7 +111,8 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends

return firstValueFrom(
this.#getWallets().pipe(
switchMap((wallets) => {
blockingWithLatestFrom(this.#store$),
switchMap(([wallets, store]) => {
const walletIndex = wallets.findIndex((w) => w.walletId === walletId);
if (walletIndex < 0) {
throw new WalletConflictError(`Wallet '${walletId}' does not exist`);
Expand All @@ -130,7 +133,7 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends
);
}

return this.#store
return store
.setAll(
cloneSplice(wallets, walletIndex, 1, {
...wallet,
Expand Down Expand Up @@ -159,11 +162,12 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends

return firstValueFrom(
this.#getWallets().pipe(
switchMap((wallets) => {
blockingWithLatestFrom(this.#store$),
switchMap(([wallets, store]) => {
const walletIndex = wallets.findIndex((wallet) => wallet.walletId === walletId);
if (walletIndex >= 0) {
// update any wallet
return this.#store.setAll(
return store.setAll(
cloneSplice(wallets, walletIndex, 1, {
...(wallets[walletIndex] as AnyWallet<WalletMetadata, AccountMetadata>),
metadata
Expand All @@ -187,13 +191,14 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends

return firstValueFrom(
this.#getWallets().pipe(
switchMap((wallets) => {
blockingWithLatestFrom(this.#store$),
switchMap(([wallets, store]) => {
// update account
const bip32Account = findAccount(wallets, walletId, accountIndex, purpose);
if (!bip32Account) {
throw new WalletConflictError(`Account not found: ${walletId}/${purpose}/${accountIndex}`);
}
return this.#store.setAll(
return store.setAll(
cloneSplice(wallets, bip32Account.walletIdx, 1, {
...bip32Account.wallet,
accounts: cloneSplice(bip32Account.wallet.accounts, bip32Account.accountIdx, 1, {
Expand All @@ -216,7 +221,8 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends
this.#logger.debug('removeAccount', walletId, accountIndex, purpose);
return firstValueFrom(
this.#getWallets().pipe(
switchMap((wallets) => {
blockingWithLatestFrom(this.#store$),
switchMap(([wallets, store]) => {
const bip32Account = findAccount(wallets, walletId, accountIndex, purpose);
if (!bip32Account) {
throw new WalletConflictError(`Account '${walletId}/${purpose}/${accountIndex}' does not exist`);
Expand All @@ -234,7 +240,7 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends
`Wallet '${dependentWallet.walletId}' depends on account '${walletId}/${purpose}/${accountIndex}'`
);
}
return this.#store.setAll(
return store.setAll(
cloneSplice(wallets, bip32Account.walletIdx, 1, {
...bip32Account.wallet,
accounts: cloneSplice(bip32Account.wallet.accounts, bip32Account.accountIdx, 1)
Expand All @@ -251,7 +257,8 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends
return firstValueFrom(
this.#getWallets().pipe(
take(1),
switchMap((wallets) => {
blockingWithLatestFrom(this.#store$),
switchMap(([wallets, store]) => {
const walletIndex = wallets.findIndex((w) => w.walletId === walletId);
if (walletIndex < 0) {
throw new WalletConflictError(`Wallet '${walletId}' does not exist`);
Expand All @@ -263,7 +270,7 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends
if (dependentWallet) {
throw new WalletConflictError(`Wallet '${dependentWallet.walletId}' depends on wallet '${walletId}'`);
}
return this.#store.setAll(cloneSplice(wallets, walletIndex, 1));
return store.setAll(cloneSplice(wallets, walletIndex, 1));
}),
map(() => walletId)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@
import { AccountMetadata, WalletMetadata, createAccount } from './util';
import {
AddWalletProps,
AnyWallet,
HardwareWallet,
UpdateAccountMetadataProps,
UpdateWalletMetadataProps,
WalletConflictError,
WalletId,
WalletRepository,
WalletRepositoryDependencies,
WalletType
} from '../../src';
import { BehaviorSubject, firstValueFrom, of } from 'rxjs';
import { Cardano, Serialization } from '@cardano-sdk/core';
import { Hash28ByteBase16 } from '@cardano-sdk/crypto';
import { KeyPurpose, KeyRole } from '@cardano-sdk/key-management';
import { logger } from '@cardano-sdk/util-dev';
import { storage } from '@cardano-sdk/wallet';
import pick from 'lodash/pick.js';

const storedLedgerWallet: HardwareWallet<WalletMetadata, AccountMetadata> = {
Expand Down Expand Up @@ -68,18 +69,18 @@ const storedScriptWallet = {
walletId: Serialization.Script.fromCore(createScriptWalletProps.paymentScript).hash().slice(32)
};

type WalletStore = storage.CollectionStore<AnyWallet<WalletMetadata, AccountMetadata>>;

describe('WalletRepository', () => {
let repository: WalletRepository<WalletMetadata, AccountMetadata>;
let store: jest.Mocked<WalletRepositoryDependencies<WalletMetadata, AccountMetadata>['store']>;
let store: jest.Mocked<WalletStore>;

beforeEach(() => {
store = {
observeAll: jest.fn() as jest.Mocked<
WalletRepositoryDependencies<WalletMetadata, AccountMetadata>['store']
>['observeAll'],
setAll: jest.fn() as jest.Mocked<WalletRepositoryDependencies<WalletMetadata, AccountMetadata>['store']>['setAll']
} as jest.Mocked<WalletRepositoryDependencies<WalletMetadata, AccountMetadata>['store']>;
repository = new WalletRepository({ logger, store });
observeAll: jest.fn() as jest.Mocked<WalletStore>['observeAll'],
setAll: jest.fn() as jest.Mocked<WalletStore>['setAll']
} as jest.Mocked<WalletStore>;
repository = new WalletRepository({ logger, store$: of(store) });

store.observeAll.mockReturnValue(of([storedLedgerWallet]));
store.setAll.mockReturnValue(of(void 0));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as Crypto from '@cardano-sdk/crypto';
import { AccountMetadata, WalletMetadata, createAccount, createPubKey } from './util';
import {
AddWalletProps,
AnyWallet,
WalletFactory,
WalletId,
WalletManager,
Expand All @@ -16,7 +17,7 @@ import { HexBlob, InvalidArgumentError, isNotNil } from '@cardano-sdk/util';
import { MinimalRuntime } from '../../src/messaging';
import { ObservableWallet, storage } from '@cardano-sdk/wallet';
import { Storage } from 'webextension-polyfill';
import { TimeoutError, filter, firstValueFrom, from, skip, timeout } from 'rxjs';
import { TimeoutError, filter, firstValueFrom, from, of, skip, timeout } from 'rxjs';
import { logger } from '@cardano-sdk/util-dev';
import pick from 'lodash/pick.js';

Expand Down Expand Up @@ -96,7 +97,7 @@ describe('WalletManager', () => {
const walletFactory: WalletFactory<WalletMetadata, AccountMetadata> = { create: walletFactoryCreate };
const walletRepository = new WalletRepository<WalletMetadata, AccountMetadata>({
logger,
store: new storage.InMemoryCollectionStore()
store$: of(new storage.InMemoryCollectionStore<AnyWallet<WalletMetadata, WalletMetadata>>())
});

const id = await walletRepository.addWallet(walletProps);
Expand Down

0 comments on commit 3ec9198

Please sign in to comment.