📄 src/web3/wallet/PrivateKeyConnector.ts
D-OPEN SOVEREIGN
1import { type EvmWalletConnector, type ConnectResult, type WalletType } from '../client/types';
2import { getPrivateKey, hasPrivateKey } from './credentials';
3import { getChain, toViemChain, DEFAULT_CHAIN_ID } from '../network/chains';
4import { privateKeyToAccount } from 'viem/accounts';
5import { createWalletClient, http, type TransactionRequest, type WalletClient } from 'viem';
6
7function normalizePrivateKeyHex(privateKey: string): `0x${string}` {
8 return (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`) as `0x${string}`;
9}
10
11// Storage-free counterparts to PrivateKeyConnector's storage-backed methods below —
12// for callers holding a key only decrypted-in-memory (e.g. an app with its own
13// encrypted vault) that want to sign without ever persisting the key via
14// credentials.ts's plaintext localStorage.
15export function addressFromPrivateKey(privateKey: string): string {
16 return privateKeyToAccount(normalizePrivateKeyHex(privateKey)).address;
17}
18
19export function createPrivateKeyWalletClient(chainId: number, privateKey: string): WalletClient {
20 const account = privateKeyToAccount(normalizePrivateKeyHex(privateKey));
21 const chainConfig = getChain(chainId);
22 return createWalletClient({
23 account,
24 chain: toViemChain(chainConfig),
25 transport: http(chainConfig.rpc),
26 });
27}
28
29export class PrivateKeyConnector implements EvmWalletConnector {
30 public readonly type: WalletType = 'private_key';
31 private activeChainId: number = 0;
32 private chainChangedCallbacks: Set<(chainId: number) => void> = new Set();
33
34 isAvailable(): boolean {
35 return true;
36 }
37
38 // chainId is optional on the WalletConnector interface (defaults to DEFAULT_CHAIN_ID) —
39 // callers that know the target chain (e.g. bootstrapWebSDK, UI chain selector) pass it
40 // explicitly rather than relying on whichever key happens to be stored first.
41 async connect(chainId: number = DEFAULT_CHAIN_ID): Promise<ConnectResult> {
42 if (!hasPrivateKey(chainId)) {
43 throw new Error(`No private key configured for chain ${chainId}`);
44 }
45 this.activeChainId = chainId;
46 const privateKey = getPrivateKey(chainId);
47 if (!privateKey) {
48 throw new Error(`No private key configured for chain ${chainId}`);
49 }
50 return {
51 address: addressFromPrivateKey(privateKey),
52 chainId,
53 };
54 }
55
56 async getAccounts(): Promise<string[]> {
57 if (!this.activeChainId) return [];
58 const privateKey = getPrivateKey(this.activeChainId);
59 if (!privateKey) return [];
60 return [addressFromPrivateKey(privateKey)];
61 }
62
63 async getChainId(): Promise<number> {
64 return this.activeChainId;
65 }
66
67 async requestSwitchChain(chainId: number): Promise<void> {
68 const privateKey = getPrivateKey(chainId);
69 if (!privateKey) {
70 throw new Error(`No private key configured for chain ${chainId}`);
71 }
72 this.activeChainId = chainId;
73 for (const cb of this.chainChangedCallbacks) {
74 cb(chainId);
75 }
76 }
77
78 async sendTransaction(tx: TransactionRequest, chainId: number): Promise<string> {
79 const privateKey = getPrivateKey(chainId);
80 if (!privateKey) {
81 throw new Error(`No private key found for chain ${chainId}`);
82 }
83 const walletClient = createPrivateKeyWalletClient(chainId, privateKey);
84 // TransactionRequest's optional `type` field doesn't discriminate cleanly
85 // against viem's SendTransactionParameters overloads — pre-existing gap.
86 return await walletClient.sendTransaction(tx as any);
87 }
88
89 async signMessage(message: string): Promise<string> {
90 if (!this.activeChainId) {
91 throw new Error('Wallet not connected');
92 }
93 const privateKey = getPrivateKey(this.activeChainId);
94 if (!privateKey) {
95 throw new Error('No private key found');
96 }
97 const account = privateKeyToAccount(normalizePrivateKeyHex(privateKey));
98 return await account.signMessage({ message });
99 }
100
101 async getWalletClient(chainId: number): Promise<WalletClient> {
102 const privateKey = getPrivateKey(chainId);
103 if (!privateKey) {
104 throw new Error(`No private key configured for chain ${chainId}`);
105 }
106 return createPrivateKeyWalletClient(chainId, privateKey);
107 }
108
109 onAccountChanged(cb: (address: string) => void): () => void {
110 return () => {};
111 }
112
113 onChainChanged(cb: (chainId: number) => void): () => void {
114 this.chainChangedCallbacks.add(cb);
115 return () => {
116 this.chainChangedCallbacks.delete(cb);
117 };
118 }
119
120 disconnect(): void {
121 this.activeChainId = 0;
122 }
123}
124export function createPrivateKeyConnector(): PrivateKeyConnector {
125 return new PrivateKeyConnector();
126}
127