Skip to content

Commit 52de9d0

Browse files
committed
feat(web-extension): add SignerManager
it is intended to be running in wallet's UI script and exposed via messaging to service worker, which calls signTransaction/signData, which then emits an object that has sign() and reject() methods on transactionWitnessRequest$/signDataRequest$
1 parent a3d3c17 commit 52de9d0

File tree

9 files changed

+425
-0
lines changed

9 files changed

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

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;

0 commit comments

Comments
 (0)