Skip to content

Commit 2b1e05c

Browse files
committed
feat(web-extension): wip signer manager
1 parent 7185fc8 commit 2b1e05c

File tree

9 files changed

+272
-0
lines changed

9 files changed

+272
-0
lines changed

packages/web-extension/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
"@cardano-sdk/crypto": "workspace:~",
5858
"@cardano-sdk/dapp-connector": "workspace:~",
5959
"@cardano-sdk/key-management": "workspace:~",
60+
"@cardano-sdk/hardware-ledger": "workspace:~",
61+
"@cardano-sdk/hardware-trezor": "workspace:~",
6062
"@cardano-sdk/tx-construction": "workspace:~",
6163
"@cardano-sdk/util": "workspace:~",
6264
"@cardano-sdk/util-rxjs": "workspace:~",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/* eslint-disable brace-style */
2+
import { Cardano, Serialization } from '@cardano-sdk/core';
3+
import { HardwareKeyAgentOptions, createHardwareKeyAgent } from './createHardwareKeyAgent';
4+
import { InMemoryKeyAgent, KeyAgent, KeyAgentDependencies, SignBlobResult } from '@cardano-sdk/key-management';
5+
import { InMemoryWallet, WalletType } from '../types';
6+
import {
7+
RequestBase,
8+
RequestContext,
9+
SignDataProps,
10+
SignDataRequest,
11+
SignRequest,
12+
SignTransactionProps,
13+
SignerManagerConfirmationApi,
14+
SignerManagerSignApi,
15+
TransactionWitnessRequest
16+
} from './types';
17+
import { Subject } from 'rxjs';
18+
19+
export type SignerManagerProps = {
20+
hwOptions: HardwareKeyAgentOptions;
21+
};
22+
23+
export class SignerManager<WalletMetadata extends {}>
24+
implements SignerManagerConfirmationApi<WalletMetadata>, SignerManagerSignApi<WalletMetadata>
25+
{
26+
readonly transactionWitnessRequest$ = new Subject<TransactionWitnessRequest<WalletMetadata>>();
27+
readonly signDataRequest$ = new Subject<SignDataRequest<WalletMetadata>>();
28+
readonly #hwOptions: HardwareKeyAgentOptions;
29+
readonly #keyAgentDependencies: KeyAgentDependencies;
30+
31+
constructor(props: SignerManagerProps, dependencies: KeyAgentDependencies) {
32+
this.#hwOptions = props.hwOptions;
33+
this.#keyAgentDependencies = dependencies;
34+
}
35+
36+
async signTransaction(
37+
{ tx, signContext, options }: SignTransactionProps,
38+
requestContext: RequestContext<WalletMetadata>
39+
): Promise<Cardano.Signatures> {
40+
const transaction = Serialization.Transaction.fromCbor(tx);
41+
return this.#signRequest(
42+
this.transactionWitnessRequest$,
43+
{
44+
requestContext,
45+
signContext,
46+
transaction,
47+
walletType: requestContext.wallet.type
48+
},
49+
(keyAgent) =>
50+
keyAgent.signTransaction(
51+
{
52+
body: transaction.body().toCore(),
53+
hash: transaction.getId()
54+
},
55+
signContext,
56+
options
57+
)
58+
);
59+
}
60+
61+
async signData(props: SignDataProps, requestContext: RequestContext<WalletMetadata>): Promise<SignBlobResult> {
62+
return this.#signRequest(
63+
this.signDataRequest$,
64+
{
65+
...props,
66+
requestContext,
67+
walletType: requestContext.wallet.type
68+
},
69+
(keyAgent) => keyAgent.signBlob(props.derivationPath, props.blob)
70+
);
71+
}
72+
73+
#signRequest<R, Req extends RequestBase<WalletMetadata> & SignRequest<R>>(
74+
emitter$: Subject<Req>,
75+
request: Omit<Req, 'reject' | 'sign'>,
76+
sign: (keyAgent: KeyAgent) => Promise<R>
77+
) {
78+
return new Promise<R>((resolve, reject) => {
79+
if (!emitter$.observed) {
80+
return reject(new Error('Internal error: signDataRequest$ not observed'));
81+
}
82+
const account = request.requestContext.wallet.accounts.find(
83+
({ accountIndex }) => accountIndex === request.requestContext.accountIndex
84+
);
85+
if (!account) {
86+
return reject(new Error(`Account not found: ${request.requestContext.accountIndex}`));
87+
}
88+
const bubbleResolveReject = async (action: () => Promise<R>): Promise<R> => {
89+
try {
90+
const result = action();
91+
resolve(result);
92+
return result;
93+
} catch (error) {
94+
reject(error);
95+
throw error;
96+
}
97+
};
98+
const commonRequestProps = {
99+
...request,
100+
reject: async (reason: string) => reject(new Error(reason))
101+
};
102+
emitter$.next(
103+
request.walletType === WalletType.InMemory
104+
? ({
105+
...commonRequestProps,
106+
sign: async (passphrase: Uint8Array) =>
107+
bubbleResolveReject(() => {
108+
const wallet = request.requestContext.wallet as InMemoryWallet<WalletMetadata>;
109+
return sign(
110+
new InMemoryKeyAgent(
111+
{
112+
accountIndex: account.accountIndex,
113+
chainId: request.requestContext.chainId,
114+
encryptedRootPrivateKeyBytes: [
115+
...Buffer.from(wallet.encryptedSecrets.rootPrivateKeyBytes, 'hex')
116+
],
117+
extendedAccountPublicKey: wallet.extendedAccountPublicKey,
118+
// TODO: this might be in memory for longer than needed
119+
getPassphrase: async () => passphrase
120+
},
121+
this.#keyAgentDependencies
122+
)
123+
);
124+
}),
125+
walletType: request.walletType
126+
} as Req)
127+
: ({
128+
...commonRequestProps,
129+
sign: async (): Promise<R> =>
130+
bubbleResolveReject(() =>
131+
sign(
132+
createHardwareKeyAgent(request.requestContext, account, this.#hwOptions, this.#keyAgentDependencies)
133+
)
134+
),
135+
walletType: request.walletType
136+
} as Req)
137+
);
138+
});
139+
}
140+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Bip32WalletAccount, WalletType } from '../types';
2+
import { KeyAgent, KeyAgentDependencies, TrezorConfig } from '@cardano-sdk/key-management';
3+
import { LedgerKeyAgent } from '@cardano-sdk/hardware-ledger';
4+
import { NotImplementedError } from '@cardano-sdk/core';
5+
import { RequestContext } from './types';
6+
import { TrezorKeyAgent } from '@cardano-sdk/hardware-trezor';
7+
8+
export type HardwareKeyAgentOptions = TrezorConfig;
9+
10+
/** Create and initialize key agent. Connects to hardware device. */
11+
export const createHardwareKeyAgent = <Metadata extends {}>(
12+
{ chainId, wallet }: Omit<RequestContext<Metadata>, 'origin'>,
13+
{ accountIndex }: Pick<Bip32WalletAccount<Metadata>, 'accountIndex'>,
14+
hwConfig: HardwareKeyAgentOptions,
15+
dependencies: KeyAgentDependencies
16+
): KeyAgent => {
17+
switch (wallet.type) {
18+
case WalletType.Ledger:
19+
return new LedgerKeyAgent(
20+
{
21+
accountIndex,
22+
chainId,
23+
communicationType: hwConfig.communicationType,
24+
extendedAccountPublicKey: wallet.extendedAccountPublicKey
25+
},
26+
dependencies
27+
);
28+
case WalletType.Trezor:
29+
return new TrezorKeyAgent(
30+
{
31+
accountIndex,
32+
chainId,
33+
extendedAccountPublicKey: wallet.extendedAccountPublicKey,
34+
trezorConfig: hwConfig
35+
},
36+
dependencies
37+
);
38+
default:
39+
throw new NotImplementedError(`Unknown hardware KeyAgent type: ${wallet.type}`);
40+
}
41+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './types';
2+
export * from './SignerManager';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {
2+
AccountKeyDerivationPath,
3+
SignBlobResult,
4+
SignTransactionContext,
5+
SignTransactionOptions
6+
} from '@cardano-sdk/key-management';
7+
import { AnyBip32Wallet, WalletType } from '../types';
8+
import { Cardano, Serialization, TxCBOR } from '@cardano-sdk/core';
9+
import { HexBlob } from '@cardano-sdk/util';
10+
import { Observable } from 'rxjs';
11+
12+
export type RequestContext<WalletMetadata extends {}> = {
13+
wallet: AnyBip32Wallet<WalletMetadata>;
14+
accountIndex: number;
15+
chainId: Cardano.ChainId;
16+
};
17+
18+
export type RequestBase<WalletMetadata extends {}> = {
19+
requestContext: RequestContext<WalletMetadata>;
20+
reject(reason: string): Promise<void>;
21+
};
22+
23+
type SignRequestInMemory<R> = {
24+
walletType: WalletType.InMemory;
25+
sign(passphrase: Uint8Array): Promise<R>;
26+
};
27+
28+
type SignRequestHardware<R> = {
29+
walletType: WalletType.Trezor | WalletType.Ledger;
30+
/** Must be called from user gesture when running in web environments */
31+
sign(): Promise<R>;
32+
};
33+
34+
export type SignRequest<R> = SignRequestHardware<R> | SignRequestInMemory<R>;
35+
36+
export type TransactionWitnessRequest<WalletMetadata extends {}> = RequestBase<WalletMetadata> & {
37+
transaction: Serialization.Transaction;
38+
signContext: SignTransactionContext;
39+
} & SignRequest<Cardano.Signatures>;
40+
41+
export type SignDataContext = { origin?: string };
42+
43+
export type SignDataProps = {
44+
derivationPath: AccountKeyDerivationPath;
45+
blob: HexBlob;
46+
signContext: SignDataContext;
47+
};
48+
49+
export type SignDataRequest<WalletMetadata extends {}> = RequestBase<WalletMetadata> &
50+
SignDataProps &
51+
SignRequest<SignBlobResult>;
52+
53+
export type SignTransactionProps = {
54+
tx: TxCBOR;
55+
signContext: SignTransactionContext;
56+
options?: SignTransactionOptions;
57+
};
58+
59+
export interface SignerManagerConfirmationApi<WalletMetadata extends {}> {
60+
transactionWitnessRequest$: Observable<TransactionWitnessRequest<WalletMetadata>>;
61+
signDataRequest$: Observable<SignDataRequest<WalletMetadata>>;
62+
}
63+
64+
export interface SignerManagerSignApi<WalletMetadata extends {}> {
65+
signTransaction(
66+
props: SignTransactionProps,
67+
requestContext: RequestContext<WalletMetadata>
68+
): Promise<Cardano.Signatures>;
69+
signData(props: SignDataProps, requestContext: RequestContext<WalletMetadata>): Promise<SignBlobResult>;
70+
}

packages/web-extension/src/walletManager/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export * from './util';
33
export * from './walletManagerUi';
44
export * from './walletManagerWorker';
55
export * from './WalletRepository';
6+
export * from './SignerManager';
67
export * from './errors';
78
export * from './types';

packages/web-extension/src/walletManager/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type InMemoryWallet<Metadata extends {}> = Bip32Wallet<Metadata> & {
3636
};
3737
};
3838

39+
export type AnyBip32Wallet<WalletMetadata extends {}> = HardwareWallet<WalletMetadata> | InMemoryWallet<WalletMetadata>;
40+
3941
export type OwnSignerAccount = {
4042
walletId: WalletId;
4143
accountIndex: number;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
describe('SignerManager', () => {
2+
// let signerManager: SignerManager<{}>;
3+
4+
beforeEach(() => {
5+
// signerManager = new SignerManager();
6+
// TODO
7+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
8+
// signerManager;
9+
});
10+
11+
it.todo('todo');
12+
});

yarn.lock

+2
Original file line numberDiff line numberDiff line change
@@ -3771,6 +3771,8 @@ __metadata:
37713771
"@cardano-sdk/core": "workspace:~"
37723772
"@cardano-sdk/crypto": "workspace:~"
37733773
"@cardano-sdk/dapp-connector": "workspace:~"
3774+
"@cardano-sdk/hardware-ledger": "workspace:~"
3775+
"@cardano-sdk/hardware-trezor": "workspace:~"
37743776
"@cardano-sdk/key-management": "workspace:~"
37753777
"@cardano-sdk/tx-construction": "workspace:~"
37763778
"@cardano-sdk/util": "workspace:~"

0 commit comments

Comments
 (0)