Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ scripts/private

# nix
/result

ledger-penumbra
4 changes: 2 additions & 2 deletions apps/extension/src/ctx/full-viewing-key.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FullViewingKey } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';
import { Wallet } from '@repo/wallet';
import { Code, ConnectError } from '@connectrpc/connect';
import { localExtStorage } from '@repo/storage-chrome/local';

Expand All @@ -8,5 +8,5 @@ export const getFullViewingKey = async () => {
throw new ConnectError('No wallet available', Code.FailedPrecondition);
}

return FullViewingKey.fromJsonString(wallet0.fullViewingKey);
return Wallet.fromJson(wallet0).fullViewingKey;
};
4 changes: 2 additions & 2 deletions apps/extension/src/ctx/wallet-id.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WalletId } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';
import { Wallet } from '@repo/wallet';
import { Code, ConnectError } from '@connectrpc/connect';
import { localExtStorage } from '@repo/storage-chrome/local';

Expand All @@ -8,5 +8,5 @@ export const getWalletId = async () => {
throw new ConnectError('No wallet available', Code.FailedPrecondition);
}

return WalletId.fromJsonString(wallet0.id);
return Wallet.fromJson(wallet0).id;
};
2 changes: 1 addition & 1 deletion apps/extension/src/state/persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const customPersistImpl: Persist = f => (set, get, store) => {
set(
produce((state: AllSlices) => {
state.network.chainId = stored
? AppParameters.fromJsonString(stored).chainId
? AppParameters.fromJson(stored).chainId

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: this caught my eye. v3 schema suggests that param is stored as a string, not a parsed object. let's revert this back to AppParameters.fromJsonString(stored).

: state.network.chainId;
}),
);
Expand Down
9 changes: 5 additions & 4 deletions apps/extension/src/state/wallets.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Key } from '@repo/encryption/key';
import { Box } from '@repo/encryption/box';
import { Wallet, type WalletJson } from '@repo/wallet';
import { generateSpendKey, getFullViewingKey, getWalletId } from '@penumbra-zone/wasm/keys';
import { assertWalletType, Wallet, type WalletJson } from '@repo/wallet';
import { generateSpendKey, getFullViewingKey } from '@penumbra-zone/wasm/keys';
import type { ExtensionStorage } from '@repo/storage-chrome/base';
import type { LocalStorageState } from '@repo/storage-chrome/local';
import type { SessionStorageState } from '@repo/storage-chrome/session';
import { AllSlices, SliceCreator } from '.';

export interface WalletsSlice {
/** new custody types aren't used yet */
all: WalletJson<'encryptedSeedPhrase'>[];
all: WalletJson[];
addWallet: (toAdd: { label: string; seedPhrase: string[] }) => Promise<void>;
getSeedPhrase: () => Promise<string[]>;
}
Expand All @@ -33,7 +33,7 @@ export const createWalletsSlice =
}

const key = await Key.fromJson(passwordKey);
const newWallet = new Wallet(label, getWalletId(fullViewingKey), fullViewingKey, {
const newWallet = new Wallet(label, fullViewingKey, {
encryptedSeedPhrase: await key.seal(seedPhraseStr),
});

Expand All @@ -58,6 +58,7 @@ export const createWalletsSlice =
throw new Error('no wallet set');
}

assertWalletType('encryptedSeedPhrase', activeWallet);
const phraseBox = Box.fromJson(activeWallet.toJson().custody.encryptedSeedPhrase);

const phrase = await key.unseal(phraseBox);
Expand Down
10 changes: 5 additions & 5 deletions apps/extension/src/wallet-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import { AppParameters } from '@penumbra-zone/protobuf/penumbra/core/app/v1/app_
import { AppService } from '@penumbra-zone/protobuf';
import { createGrpcWebTransport } from '@connectrpc/connect-web';
import { createClient } from '@connectrpc/connect';
import { FullViewingKey, WalletId } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';
import { localExtStorage } from '@repo/storage-chrome/local';
import { Wallet } from '@repo/wallet';
import { localExtStorage } from '@repo/storage-chrome';
import { onboardGrpcEndpoint, onboardWallet } from '@repo/storage-chrome/onboard';
import { Services } from '@repo/context';
import { WalletServices } from '@penumbra-zone/types/services';
import { AssetId } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { SENTINEL_U64_MAX } from './utils/sentinel';

export const startWalletServices = async () => {
const wallet = await onboardWallet();
const wallet = Wallet.fromJson(await onboardWallet());
const grpcEndpoint = await onboardGrpcEndpoint();
const numeraires = await localExtStorage.get('numeraires');
const chainId = await getChainId(grpcEndpoint);
Expand All @@ -21,8 +21,8 @@ export const startWalletServices = async () => {
const services = new Services({
grpcEndpoint,
chainId,
walletId: WalletId.fromJsonString(wallet.id),
fullViewingKey: FullViewingKey.fromJsonString(wallet.fullViewingKey),
walletId: wallet.id,
fullViewingKey: wallet.fullViewingKey,
numeraires: numeraires.map(n => AssetId.fromJsonString(n)),
walletCreationBlockHeight,
compactFrontierBlockHeight,
Expand Down
1 change: 1 addition & 0 deletions apps/extension/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export default ({
alias: {
'@ui': path.resolve(__dirname, '../../packages/ui'),
},
fallback: { buffer: false },
},
plugins: [
new webpack.CleanPlugin(),
Expand Down
79 changes: 64 additions & 15 deletions packages/mock-chrome/src/mocks/storage-area.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/* eslint-disable @typescript-eslint/consistent-indexed-object-style -- meets external mapped types */
export class MockStorageArea implements chrome.storage.StorageArea {
public mock = new Map<string, unknown>();
public mock = new MapWithUpdates<string, unknown>();

public listeners = new Set<(changes: { [key: string]: chrome.storage.StorageChange }) => void>();

get(cb: (result: Record<string, unknown>) => void): void;
get(keys?: string | string[] | Record<string, unknown> | null): Promise<Record<string, unknown>>;
Expand Down Expand Up @@ -59,6 +62,13 @@ export class MockStorageArea implements chrome.storage.StorageArea {
} else {
throw new TypeError('Invalid query');
}

const updates = this.mock.flushUpdates();
const callListeners = Promise.all(
Array.from(this.listeners).map(listener => Promise.resolve(updates).then(listener)),
);
void callListeners;

return Promise.resolve();
}

Expand All @@ -72,25 +82,22 @@ export class MockStorageArea implements chrome.storage.StorageArea {
this.mock.set(key, value);
}
}

const updates = this.mock.flushUpdates();
const callListeners = Promise.all(
Array.from(this.listeners).map(listener => Promise.resolve(updates).then(listener)),
);
void callListeners;

return Promise.resolve();
}

get onChanged(): chrome.storage.StorageAreaChangedEvent {
return {
addListener: listener => {
console.debug('MockStorageArea.onChanged.addListener', listener);
},
removeListener: listener => {
console.debug('MockStorageArea.onChanged.removeListener', listener);
},
hasListener: listener => {
console.debug('MockStorageArea.onChanged.hasListener', listener);
throw new Error('Not implemented');
},
hasListeners: () => {
console.debug('MockStorageArea.onChanged.hasListeners');
throw new Error('Not implemented');
},
addListener: listener => this.listeners.add(listener),
removeListener: listener => this.listeners.delete(listener),
hasListener: listener => this.listeners.has(listener),
hasListeners: () => this.listeners.size > 0,
getRules: undefined as never,
addRules: undefined as never,
removeRules: undefined as never,
Expand All @@ -100,6 +107,13 @@ export class MockStorageArea implements chrome.storage.StorageArea {
async clear() {
console.debug('MockStorageArea.clear');
this.mock.clear();

const updates = this.mock.flushUpdates();
const callListeners = Promise.all(
Array.from(this.listeners).map(listener => Promise.resolve(updates).then(listener)),
);
void callListeners;

return Promise.resolve();
}

Expand All @@ -108,3 +122,38 @@ export class MockStorageArea implements chrome.storage.StorageArea {
return Promise.reject(new Error('Not implemented'));
}
}

class MapWithUpdates<K extends string, V> extends Map<K, V> {
public previous = new Map<K, V | undefined>();

override set(key: K, value: V) {
this.previous.set(key, super.get(key));
return super.set(key, value);
}

override delete(key: K): boolean {
if (super.has(key)) {
this.previous.set(key, super.get(key));
}
return super.delete(key);
}

override clear() {
this.previous = new Map(super.entries());
super.clear();
}

flushUpdates() {
const updates = new Map<K, chrome.storage.StorageChange>();

for (const [key, oldValue] of this.previous.entries()) {
updates.set(key, { oldValue, newValue: super.get(key) });
}

this.previous.clear();

return Object.fromEntries(updates.entries()) as {
[key: string]: chrome.storage.StorageChange;
};
}
}
4 changes: 2 additions & 2 deletions packages/storage-chrome/src/local.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ExtensionStorage, ExtensionStorageDefaults } from './base';
import { VERSION as LocalStorageVersion, LOCAL as LocalStorageState } from './versions/v2';
import { VERSION as LocalStorageVersion, LOCAL as LocalStorageState } from './versions/v3';

const localDefaults: ExtensionStorageDefaults<LocalStorageState> = {
wallets: [],
Expand All @@ -10,7 +10,7 @@ const localDefaults: ExtensionStorageDefaults<LocalStorageState> = {
export const localExtStorage = new ExtensionStorage<LocalStorageState, LocalStorageVersion>(
chrome.storage.local,
localDefaults,
2,
3,
);

export type { LocalStorageState, LocalStorageVersion };
Expand Down
5 changes: 4 additions & 1 deletion packages/storage-chrome/src/migrations/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { ExtensionStorageMigrations } from '../base';

import local_v0_v1 from './local-v0-v1';
import local_v1_v2 from './local-v1-v2';
import local_v2_v3 from './local-v2-v3';

import type { LocalStorageVersion } from '../local';

export const localMigrations: ExtensionStorageMigrations<LocalStorageVersion, 0 | 1> = {
export const localMigrations: ExtensionStorageMigrations<LocalStorageVersion, 0 | 1 | 2> = {
0: local_v0_v1,
1: local_v1_v2,
2: local_v2_v3,
} as const;
5 changes: 5 additions & 0 deletions packages/storage-chrome/src/migrations/local-v0-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export default {
knownSites: old.knownSites?.value ?? [],
params: old.params?.value,
numeraires: old.numeraires?.value ?? [],

// absent values now explicitly required
backupReminderSeen: undefined,
compactFrontierBlockHeight: undefined,
walletCreationBlockHeight: undefined,
Comment on lines +27 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: why the explicit requirement here?

}),
} satisfies MIGRATION;

Expand Down
10 changes: 10 additions & 0 deletions packages/storage-chrome/src/migrations/local-v1-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ export default {
passwordKeyPrint: isVestigialItem(old.passwordKeyPrint)
? old.passwordKeyPrint.value
: old.passwordKeyPrint,

// lol
backupReminderSeen:
typeof old.backupReminderSeen === 'boolean' ? old.backupReminderSeen : undefined,
compactFrontierBlockHeight:
typeof old.compactFrontierBlockHeight === 'number'
? old.compactFrontierBlockHeight
: undefined,
walletCreationBlockHeight:
typeof old.walletCreationBlockHeight === 'number' ? old.walletCreationBlockHeight : undefined,
Comment on lines +27 to +35

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: can't we do without this?

}),
} satisfies MIGRATION;

Expand Down
36 changes: 36 additions & 0 deletions packages/storage-chrome/src/migrations/local-v2-v3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type * as FROM from '../versions/v2';
import type * as TO from '../versions/v3';
import { expectVersion, type Migration } from './util';

type MIGRATION = Migration<FROM.VERSION, FROM.LOCAL, TO.VERSION, TO.LOCAL>;

export default {
version: v => expectVersion(v, 2, 3),
transform: ({
knownSites,
numeraires,
wallets,

backupReminderSeen,
compactFrontierBlockHeight,
frontendUrl,
fullSyncHeight,
grpcEndpoint,
params,
passwordKeyPrint,
walletCreationBlockHeight,
}) => ({
knownSites: knownSites ?? [],
numeraires: numeraires ?? [],
wallets: wallets ?? [],

backupReminderSeen,
compactFrontierBlockHeight,
frontendUrl,
fullSyncHeight,
grpcEndpoint,
params,
passwordKeyPrint,
walletCreationBlockHeight,
}),
} satisfies MIGRATION;
6 changes: 5 additions & 1 deletion packages/storage-chrome/src/migrations/type.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
export type Explicit<T> = {
[K in keyof Required<T>]: T[K];
};

export interface Migration<
FromV extends number,
FromState extends Record<string, unknown> = Record<string, unknown>,
ToV extends number = number,
ToState extends Record<string, unknown> = Record<string, unknown>,
> {
version(iv: FromV): ToV;
transform(fs: Partial<FromState>): ToState | Promise<ToState>;
transform(fs: Partial<FromState>): Explicit<ToState> | Promise<Explicit<ToState>>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: explicit flag here requires older migrations set optional keys to undefined to satisfy the type. don't know what the implication of that is, but i'd rather not change this. let's revert and remove absent values that are now explicitly required.

}
40 changes: 40 additions & 0 deletions packages/storage-chrome/src/versions/v3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export { type LOCAL, type SYNC, type VERSION };

type VERSION = 3;

type SYNC = void;

type LOCAL = {
// required values
knownSites: { choice: 'Approved' | 'Denied' | 'Ignored'; date: number; origin: string }[];
/** Stringified AssetId */
numeraires: string[];
wallets: {
custody:
| { encryptedSeedPhrase: { cipherText: string; nonce: string } }
| { encryptedSpendKey: { cipherText: string; nonce: string } }
| { ledgerUsb: { cipherText: string; nonce: string } };
/** Stringified FullViewingKey */
fullViewingKey: string;
/** Stringified WalletId */
id: string;
label: string;
}[];

// optional values
backupReminderSeen?: boolean;
/** integer */
compactFrontierBlockHeight?: number;
/** url string */
frontendUrl?: string;
/** integer */
fullSyncHeight?: number;
/** url string */
grpcEndpoint?: string;
/** Stringified AppParameters */
params?: string;
/** KeyPrintJson */
passwordKeyPrint?: { hash: string; salt: string };
/** integer */
walletCreationBlockHeight?: number;
};
2 changes: 2 additions & 0 deletions packages/wallet/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
snapshots-tmp
ledger-penumbra
Loading