📄 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