Skip to content

Commit c29a7f8

Browse files
feat!: wallet manager now takes an WalletManagerActivateProps object rather than just a wallet id
BREAKING CHANGES:
1 parent 89f8a31 commit c29a7f8

25 files changed

+682
-235
lines changed

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,8 @@
8888
"typedoc-plugin-missing-exports": "^1.0.0",
8989
"typescript": "^4.7.4"
9090
},
91-
"packageManager": "[email protected]"
91+
"packageManager": "[email protected]",
92+
"dependencies": {
93+
"chromedriver": "^120.0.0"
94+
}
9295
}

packages/e2e/src/factories.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
CommunicationType,
3131
InMemoryKeyAgent,
3232
KeyAgentDependencies,
33+
Witnesser,
3334
util
3435
} from '@cardano-sdk/key-management';
3536
import {
@@ -282,6 +283,7 @@ export type GetWalletProps = {
282283
stores?: storage.WalletStores;
283284
customKeyParams?: KeyAgentFactoryProps;
284285
keyAgent?: AsyncKeyAgent;
286+
witnesser?: Witnesser;
285287
};
286288

287289
/** Delays initializing tx when nearing the epoch boundary. Relies on system clock being accurate. */
@@ -307,7 +309,7 @@ const patchInitializeTxToRespectEpochBoundary = <T extends ObservableWallet>(
307309
* @returns an object containing the wallet and providers passed to it
308310
*/
309311
export const getWallet = async (props: GetWalletProps) => {
310-
const { env, idx, logger, name, polling, stores, customKeyParams, keyAgent } = props;
312+
const { env, idx, logger, name, polling, stores, customKeyParams, keyAgent, witnesser } = props;
311313
const providers = {
312314
addressDiscovery: await addressDiscoveryFactory.create(
313315
env.ADDRESS_DISCOVERY,
@@ -365,7 +367,7 @@ export const getWallet = async (props: GetWalletProps) => {
365367
bip32Account,
366368
logger,
367369
stores,
368-
witnesser: util.createBip32Ed25519Witnesser(asyncKeyAgent)
370+
witnesser: witnesser || util.createBip32Ed25519Witnesser(asyncKeyAgent)
369371
}
370372
);
371373

packages/e2e/test/web-extension/extension/background/walletManager.ts

+32-19
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,25 @@ import {
33
WalletFactory,
44
WalletManagerActivateProps,
55
WalletManagerWorker,
6+
WalletRepository,
67
exposeApi,
8+
repositoryChannel,
79
walletManagerChannel,
8-
walletManagerProperties
10+
walletManagerProperties,
11+
walletRepositoryProperties
912
} from '@cardano-sdk/web-extension';
1013

11-
import { AsyncKeyAgent } from '@cardano-sdk/key-management';
1214
import { storage as WebExtensionStorage, runtime } from 'webextension-polyfill';
13-
import { env, logger } from '../util';
15+
import { Witnesser } from '@cardano-sdk/key-management';
16+
import { env, logger, Metadata } from "../util";
1417
import { from, merge, of } from 'rxjs';
1518
import { getWallet } from '../../../../src';
1619
import { storage } from '@cardano-sdk/wallet';
1720
import { toEmpty } from '@cardano-sdk/util-rxjs';
1821
import { walletName } from '../const';
1922

2023
export interface WalletFactoryDependencies {
21-
keyAgent: AsyncKeyAgent;
24+
witnesser: Witnesser;
2225
stores: storage.WalletStores;
2326
}
2427

@@ -28,30 +31,40 @@ export interface WalletFactoryDependencies {
2831
* Please check its documentation for examples.
2932
*/
3033
const walletFactory: WalletFactory = {
31-
create: async (props: WalletManagerActivateProps, { keyAgent, stores }: WalletFactoryDependencies) =>
34+
create: async (props: WalletManagerActivateProps, { witnesser, stores }: WalletFactoryDependencies) =>
3235
(
3336
await getWallet({
3437
env,
35-
keyAgent,
3638
logger,
3739
name: props.observableWalletName,
38-
stores
40+
stores,
41+
witnesser
3942
})
4043
).wallet
4144
};
4245

4346
const storesFactory: StoresFactory = {
44-
create: ({ walletId }) => storage.createPouchDbWalletStores(walletId, { logger })
47+
create: ({ name }) => storage.createPouchDbWalletStores(name, { logger })
4548
};
4649

47-
export const wallet$ = (() => {
48-
const walletManager = new WalletManagerWorker(
49-
{ walletName },
50-
{ logger, managerStorage: WebExtensionStorage.local, runtime, storesFactory, walletFactory }
51-
);
52-
exposeApi(
53-
{ api$: of(walletManager), baseChannel: walletManagerChannel(walletName), properties: walletManagerProperties },
54-
{ logger, runtime }
55-
);
56-
return merge(walletManager.activeWallet$, from(walletManager.initialize()).pipe(toEmpty));
57-
})();
50+
const walletRepository = new WalletRepository<Metadata>({
51+
logger,
52+
store: new storage.InMemoryCollectionStore()
53+
});
54+
55+
const walletManager = new WalletManagerWorker<Metadata>(
56+
{ walletName },
57+
{ logger, managerStorage: WebExtensionStorage.local, runtime, storesFactory, walletFactory, walletRepository }
58+
);
59+
60+
exposeApi(
61+
{ api$: of(walletRepository), baseChannel: repositoryChannel(walletName), properties: walletRepositoryProperties },
62+
{ logger, runtime }
63+
);
64+
65+
exposeApi(
66+
{ api$: of(walletManager), baseChannel: walletManagerChannel(walletName), properties: walletManagerProperties },
67+
{ logger, runtime }
68+
);
69+
70+
export const wallet$ = (() => merge(walletManager.activeWallet$, from(walletManager.initialize()).pipe(toEmpty)))();

packages/e2e/test/web-extension/extension/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"unlimitedStorage"
1010
],
1111
"content_security_policy": {
12-
"extension_pages": "default-src 'self' http://localhost:3000; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src data: http://localhost:8080 https://backend.live-preprod.eks.lw.iog.io https://api.coingecko.com http://167.235.156.245:4000 http://localhost:3000 ws://localhost:3000 wss://localhost:3000 http://localhost:4000 ws://localhost:4000 wss://localhost:4000 http://testnet-dev-backend.dev.lw.iog.io:80 http://localhost:4567 https://testnet-dev-backend.dev.lw.iog.io:443 https://preprod-api.v2.prod.lw.iog.io; style-src * 'unsafe-inline'; img-src * data:; font-src https://fonts.gstatic.com;"
12+
"extension_pages": "default-src 'self' http://localhost:3000; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src data: http://localhost:8080 https://backend.live-preprod.eks.lw.iog.io https://api.coingecko.com http://167.235.156.245:4000 http://localhost:3000 ws://localhost:3000 wss://localhost:3000 http://localhost:4011 ws://localhost:4011 wss://localhost:4011 http://localhost:4000 ws://localhost:4000 wss://localhost:4000 http://testnet-dev-backend.dev.lw.iog.io:80 http://localhost:4567 https://testnet-dev-backend.dev.lw.iog.io:443 https://preprod-api.v2.prod.lw.iog.io; style-src * 'unsafe-inline'; img-src * data:; font-src https://fonts.gstatic.com;"
1313
},
1414
"web_accessible_resources": [
1515
{

packages/e2e/test/web-extension/extension/ui.ts

+119-19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable no-use-before-define */
22
import {
33
BackgroundServices,
4+
Metadata,
45
UserPromptService,
56
adaPriceProperties,
67
disconnectPortTestObjProperties,
@@ -9,10 +10,14 @@ import {
910
} from './util';
1011
import {
1112
RemoteApiPropertyType,
13+
SignerManager,
1214
WalletManagerUi,
15+
WalletType,
1316
consumeRemoteApi,
1417
consumeSupplyDistributionTracker,
15-
exposeApi
18+
createKeyAgentFactory,
19+
exposeApi,
20+
exposeSignerManagerApi
1621
} from '@cardano-sdk/web-extension';
1722
import {
1823
adaPriceServiceChannel,
@@ -21,12 +26,20 @@ import {
2126
userPromptServiceChannel,
2227
walletName
2328
} from './const';
24-
import { keyManagementFactory } from '../../../src';
2529

30+
import * as Crypto from '@cardano-sdk/crypto';
31+
import { Buffer } from 'buffer';
2632
import { Cardano } from '@cardano-sdk/core';
33+
import {
34+
CommunicationType,
35+
InMemoryKeyAgent,
36+
SerializableInMemoryKeyAgentData,
37+
emip3encrypt,
38+
util
39+
} from '@cardano-sdk/key-management';
2740
import { HexBlob } from '@cardano-sdk/util';
2841
import { SodiumBip32Ed25519 } from '@cardano-sdk/crypto';
29-
import { combineLatest, firstValueFrom, of } from 'rxjs';
42+
import { combineLatest, firstValueFrom, merge, of } from 'rxjs';
3043
import { runtime } from 'webextension-polyfill';
3144

3245
const delegationConfig = {
@@ -198,7 +211,48 @@ const cleanupMultidelegationInfo = (multiDelegationDiv: Element) => {
198211
}
199212
};
200213

201-
const walletManager = new WalletManagerUi({ walletName }, { logger, runtime });
214+
const signerManager = new SignerManager(
215+
{
216+
hwOptions: {
217+
communicationType: CommunicationType.Web,
218+
manifest: {
219+
appUrl: 'https://web-extension.app',
220+
221+
}
222+
}
223+
},
224+
{
225+
keyAgentFactory: createKeyAgentFactory({
226+
bip32Ed25519: new Crypto.SodiumBip32Ed25519(),
227+
logger
228+
})
229+
}
230+
);
231+
232+
const passphraseByteArray = Uint8Array.from(
233+
env.KEY_MANAGEMENT_PARAMS.passphrase.split('').map((letter) => letter.charCodeAt(0))
234+
);
235+
merge(signerManager.signDataRequest$, signerManager.transactionWitnessRequest$).subscribe((req) => {
236+
logger.info('Sign request', req);
237+
if (req.walletType === WalletType.InMemory) {
238+
req.sign(new Uint8Array(passphraseByteArray));
239+
} else {
240+
req.sign();
241+
}
242+
logger.info('Signed', req);
243+
});
244+
245+
// Expose signer manager.
246+
exposeSignerManagerApi(
247+
{
248+
signerManager
249+
},
250+
{ logger, runtime }
251+
);
252+
253+
// TODO: toSerializableObj does not support serializing empty obj {}
254+
const walletManager = new WalletManagerUi<Metadata>({ walletName }, { logger, runtime });
255+
202256
// Wallet object does not change when wallets are activated/deactivated.
203257
// Instead, it's observable properties emit from the currently active wallet.
204258
const wallet = walletManager.wallet;
@@ -220,30 +274,72 @@ wallet.delegation.distribution$.subscribe((delegationDistrib) => {
220274
}
221275
});
222276

223-
const createWallet = async (accountIndex: number) => {
224-
clearWalletValues();
225-
226-
const keyAgent = await (
227-
await keyManagementFactory.create(
228-
env.KEY_MANAGEMENT_PROVIDER,
277+
const createWalletIfNotExistsAndActivate = async (accountIndex: number) => {
278+
const wallets = await firstValueFrom(walletManager.repository.wallets$);
279+
let walletId = wallets.find(
280+
(w) => w.type !== WalletType.Script && w.accounts.some((a) => a.accountIndex === accountIndex)
281+
)?.walletId;
282+
if (!walletId) {
283+
logger.log('creating wallet');
284+
clearWalletValues();
285+
const bip32Ed25519 = new SodiumBip32Ed25519();
286+
const mnemonicWords = env.KEY_MANAGEMENT_PARAMS.mnemonic.split(' ');
287+
const passphrase = new Uint8Array(passphraseByteArray);
288+
const keyAgent = await InMemoryKeyAgent.fromBip39MnemonicWords(
229289
{
230-
...env.KEY_MANAGEMENT_PARAMS,
231-
accountIndex
290+
accountIndex,
291+
chainId: env.KEY_MANAGEMENT_PARAMS.chainId,
292+
getPassphrase: async () => passphrase,
293+
mnemonicWords
232294
},
233-
logger
234-
)
235-
)({ bip32Ed25519: new SodiumBip32Ed25519(), logger });
295+
{ bip32Ed25519, logger }
296+
);
297+
const encryptedRootPrivateKey = (keyAgent.serializableData as SerializableInMemoryKeyAgentData)
298+
.encryptedRootPrivateKeyBytes;
299+
300+
const entropy = Buffer.from(util.mnemonicWordsToEntropy(mnemonicWords), 'hex');
301+
const encryptedEntropy = await emip3encrypt(entropy, passphraseByteArray);
302+
303+
logger.log('adding to repository wallet');
304+
// Add wallet to the repository.
305+
walletId = await walletManager.repository.addWallet({
306+
encryptedSecrets: {
307+
entropy: HexBlob.fromBytes(encryptedEntropy),
308+
rootPrivateKeyBytes: HexBlob.fromBytes(new Uint8Array(encryptedRootPrivateKey))
309+
},
310+
extendedAccountPublicKey: keyAgent.serializableData.extendedAccountPublicKey,
311+
type: WalletType.InMemory
312+
});
313+
await walletManager.repository.addAccount({
314+
accountIndex,
315+
metadata: { name: `Wallet #${accountIndex + 1}` },
316+
walletId
317+
});
236318

237-
await walletManager.destroy();
238-
await walletManager.activate({ keyAgent, observableWalletName: getObservableWalletName(accountIndex) });
319+
logger.log(`Wallet added: ${walletId}`);
320+
} else {
321+
logger.info(`Wallet with accountIndex ${accountIndex} already exists`);
322+
}
323+
324+
// await walletManager.destroy();
325+
await walletManager.activate({
326+
accountIndex,
327+
chainId: env.KEY_MANAGEMENT_PARAMS.chainId,
328+
observableWalletName: getObservableWalletName(accountIndex),
329+
walletId
330+
});
239331

240332
// Same wallet object will return different names, based on which wallet is active
241333
// Calling this method before any wallet is active, will resolve only once a wallet becomes active
242334
setName(await wallet.getName());
243335
};
244336

245-
document.querySelector(selectors.btnActivateWallet1)!.addEventListener('click', async () => await createWallet(0));
246-
document.querySelector(selectors.btnActivateWallet2)!.addEventListener('click', async () => await createWallet(1));
337+
document
338+
.querySelector(selectors.btnActivateWallet1)!
339+
.addEventListener('click', async () => await createWalletIfNotExistsAndActivate(0));
340+
document
341+
.querySelector(selectors.btnActivateWallet2)!
342+
.addEventListener('click', async () => await createWalletIfNotExistsAndActivate(1));
247343
document.querySelector(selectors.deactivateWallet)!.addEventListener('click', async () => await deactivateWallet());
248344
document.querySelector(selectors.destroyWallet)!.addEventListener('click', async () => await destroyWallet());
249345
document.querySelector(selectors.btnDelegate)!.addEventListener('click', async () => {
@@ -253,7 +349,10 @@ document.querySelector(selectors.btnDelegate)!.addEventListener('click', async (
253349
});
254350

255351
document.querySelector(selectors.btnSignAndBuildTx)!.addEventListener('click', async () => {
352+
logger.info('Building transaction');
256353
const [{ address: ownAddress }] = await firstValueFrom(wallet.addresses$);
354+
logger.info(`Address: ${ownAddress}`);
355+
257356
const builtTx = wallet
258357
.createTxBuilder()
259358
.addOutput({
@@ -264,6 +363,7 @@ document.querySelector(selectors.btnSignAndBuildTx)!.addEventListener('click', a
264363
const { body } = await builtTx.inspect();
265364
logger.info('Built tx', body.outputs.length);
266365
const { tx: signedTx } = await builtTx.sign();
366+
logger.error(signedTx);
267367
setSignature(signedTx.witness.signatures.values().next().value);
268368
});
269369

packages/e2e/test/web-extension/extension/util.ts

+2
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ export const disconnectPortTestObjProperties: RemoteApiProperties<DisconnectPort
3535
export const logger = console;
3636

3737
export const env = getEnv(walletVariables);
38+
39+
export type Metadata = { name: string };

packages/key-management/src/InMemoryKeyAgent.ts

+1
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export class InMemoryKeyAgent extends KeyAgentBase implements KeyAgent {
161161
async #decryptRootPrivateKey(noCache?: true) {
162162
const passphrase = await getPassphraseRethrowTypedError(() => this.#getPassphrase(noCache));
163163
let decryptedRootKeyBytes: Uint8Array;
164+
164165
try {
165166
decryptedRootKeyBytes = await emip3decrypt(
166167
new Uint8Array((this.serializableData as SerializableInMemoryKeyAgentData).encryptedRootPrivateKeyBytes),

packages/key-management/src/types.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as Crypto from '@cardano-sdk/crypto';
2-
import { Cardano } from '@cardano-sdk/core';
2+
import { Cardano, Serialization } from '@cardano-sdk/core';
33
import { HexBlob, OpaqueString, Shutdown } from '@cardano-sdk/util';
44
import { Logger } from 'ts-log';
55
import type { Runtime } from 'webextension-polyfill';
@@ -222,12 +222,13 @@ export interface Witnesser {
222222
/**
223223
* Generates the witness data for a given transaction.
224224
*
225-
* @param txInternals The transaction body along with its hash for which the witness data is to be generated.
225+
* @param transaction The transaction along with its hash for which the witness data is to be generated.
226+
* @param context The witness sign transaction context
226227
* @param options Optional additional parameters that may influence how the witness data is generated.
227228
* @returns A promise that resolves to the generated witness data for the transaction.
228229
*/
229230
witness(
230-
txInternals: Cardano.TxBodyWithHash,
231+
transaction: Serialization.Transaction,
231232
context: SignTransactionContext,
232233
options?: WitnessOptions
233234
): Promise<Cardano.Witness>;

0 commit comments

Comments
 (0)