cruzo-web3 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +242 -0
  2. package/lib/components/icons/copy-icon.component.ts +14 -0
  3. package/lib/components/web3-signer/web3-signer.component.module.css +106 -0
  4. package/lib/components/web3-signer/web3-signer.component.ts +320 -0
  5. package/lib/components/web3-signing/web3-signing.component.module.css +36 -0
  6. package/lib/components/web3-signing/web3-signing.component.ts +239 -0
  7. package/lib/components/web3-wallet-picker/web3-wallet-picker.bucket.ts +22 -0
  8. package/lib/components/web3-wallet-picker/web3-wallet-picker.component.module.css +62 -0
  9. package/lib/components/web3-wallet-picker/web3-wallet-picker.component.ts +171 -0
  10. package/lib/crypto/account-signature.ts +175 -0
  11. package/lib/crypto/decode-bytes.ts +93 -0
  12. package/lib/crypto/ecdsa-signature.ts +45 -0
  13. package/lib/crypto/keccak256.ts +117 -0
  14. package/lib/crypto/secp256k1-verify.ts +232 -0
  15. package/lib/crypto/sha256.ts +8 -0
  16. package/lib/crypto/verify-signature.ts +54 -0
  17. package/lib/env.d.ts +4 -0
  18. package/lib/errors/web3-error.ts +49 -0
  19. package/lib/index.ts +20 -0
  20. package/lib/providers/eip1193.provider.ts +152 -0
  21. package/lib/providers/injected.ts +71 -0
  22. package/lib/providers/message-bytes.ts +13 -0
  23. package/lib/providers/solana.provider.ts +116 -0
  24. package/lib/providers/tonconnect.provider.ts +161 -0
  25. package/lib/providers/tron.provider.ts +142 -0
  26. package/lib/providers/wallet-transport.ts +1 -0
  27. package/lib/providers/wallet.ts +92 -0
  28. package/lib/providers/walletconnect-ethereum.provider.ts +49 -0
  29. package/lib/pub-key.ts +144 -0
  30. package/lib/signing-url.ts +334 -0
  31. package/lib/types/signer-state.ts +10 -0
  32. package/lib/types/web3-types.ts +27 -0
  33. package/lib/utils/format-pub-key.ts +18 -0
  34. package/lib/web3-wallet.ts +31 -0
  35. package/lib/web3.service.ts +419 -0
  36. package/package.json +58 -0
@@ -0,0 +1,54 @@
1
+ import { toBytes } from "./decode-bytes";
2
+ import { hashEvmPersonalMessage, keccak256 } from "./keccak256";
3
+ import { verifySecp256k1 } from "./secp256k1-verify";
4
+
5
+ async function verifyEd25519(
6
+ publicKey: Uint8Array,
7
+ signature: Uint8Array,
8
+ message: Uint8Array
9
+ ) {
10
+ if (!globalThis.crypto?.subtle) return false;
11
+
12
+ try {
13
+ const key = await crypto.subtle.importKey(
14
+ "raw",
15
+ publicKey as BufferSource,
16
+ { name: "Ed25519" },
17
+ false,
18
+ ["verify"]
19
+ );
20
+
21
+ return crypto.subtle.verify(
22
+ { name: "Ed25519" },
23
+ key,
24
+ signature as BufferSource,
25
+ message as BufferSource
26
+ );
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ export async function verifySignedBytes(
33
+ content: Uint8Array,
34
+ signature: Uint8Array,
35
+ publicKey: Uint8Array,
36
+ algorithm: "secp256k1" | "Ed25519" | "sr25519",
37
+ options?: { evmPersonalSign?: boolean }
38
+ ) {
39
+ if (algorithm === "Ed25519") {
40
+ return verifyEd25519(publicKey, signature, content);
41
+ }
42
+
43
+ const messageHash = options?.evmPersonalSign
44
+ ? hashEvmPersonalMessage(content)
45
+ : content.length === 32
46
+ ? content
47
+ : keccak256(content);
48
+
49
+ return verifySecp256k1(signature, messageHash, publicKey);
50
+ }
51
+
52
+ export function normalizeContent(content: string | Uint8Array) {
53
+ return toBytes(content);
54
+ }
package/lib/env.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module "*.module.css" {
2
+ const classes: Record<string, string>;
3
+ export default classes;
4
+ }
@@ -0,0 +1,49 @@
1
+ export type Web3ErrorCode =
2
+ | "NO_PROVIDER"
3
+ | "NO_WALLET"
4
+ | "INVALID_MESSAGE"
5
+ | "INVALID_PUB_KEY"
6
+ | "INVALID_SIGNATURE"
7
+ | "UNSUPPORTED_ALGORITHM";
8
+
9
+ export class Web3Error extends Error {
10
+ readonly name = "Web3Error";
11
+
12
+ constructor(
13
+ readonly code: Web3ErrorCode,
14
+ message: string,
15
+ ) {
16
+ super(message);
17
+ }
18
+
19
+ static noProvider() {
20
+ return new Web3Error("NO_PROVIDER", "Web3 provider is not set");
21
+ }
22
+
23
+ static noWallet() {
24
+ return new Web3Error(
25
+ "NO_WALLET",
26
+ "No injected wallet found (install MetaMask or another EIP-1193 wallet)",
27
+ );
28
+ }
29
+
30
+ static invalidMessage(detail: string) {
31
+ return new Web3Error("INVALID_MESSAGE", `Message must be a non-empty string or Uint8Array (${detail})`);
32
+ }
33
+
34
+ static invalidPubKey(message: string) {
35
+ return new Web3Error("INVALID_PUB_KEY", message);
36
+ }
37
+
38
+ static invalidSignature(message: string) {
39
+ return new Web3Error("INVALID_SIGNATURE", message);
40
+ }
41
+
42
+ static unsupportedAlgorithm(message: string) {
43
+ return new Web3Error("UNSUPPORTED_ALGORITHM", message);
44
+ }
45
+
46
+ static customProviderNotFound(providerId: string) {
47
+ return new Web3Error("NO_PROVIDER", `Custom Web3 provider "${providerId}" is not configured`);
48
+ }
49
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ export {
2
+ web3Service,
3
+ ALL_BUILTIN_WALLET_SLOTS,
4
+ } from "./web3.service";
5
+ export type {
6
+ Web3Config,
7
+ Web3CustomProviderConfig,
8
+ Web3CustomProviderFactory,
9
+ Web3CustomProviderOption,
10
+ } from "./web3.service";
11
+ export type {
12
+ Web3BuiltinWallet,
13
+ Web3CustomWallet,
14
+ Web3WalletSlot,
15
+ Web3WalletTarget,
16
+ } from "./web3-wallet";
17
+ export { isBuiltinWallet, isCustomWallet } from "./web3-wallet";
18
+
19
+ export { Web3SignerComponent } from "./components/web3-signer/web3-signer.component";
20
+ export { Web3SigningComponent } from "./components/web3-signing/web3-signing.component";
@@ -0,0 +1,152 @@
1
+ import type { PubKey, Web3Provider } from "../types/web3-types";
2
+
3
+ export interface Eip1193Like {
4
+ request(args: { method: string; params?: unknown[] }): Promise<unknown>;
5
+ on?(event: string, listener: (...args: unknown[]) => void): void;
6
+ removeListener?(event: string, listener: (...args: unknown[]) => void): void;
7
+ }
8
+
9
+ declare global {
10
+ interface Window {
11
+ ethereum?: Eip1193Like;
12
+ }
13
+ }
14
+
15
+ function toHexMessage(message: string | Uint8Array) {
16
+ const bytes = message instanceof Uint8Array ? message : new TextEncoder().encode(message);
17
+ let hex = "0x";
18
+
19
+ for (const byte of bytes) {
20
+ hex += byte.toString(16).padStart(2, "0");
21
+ }
22
+
23
+ return hex;
24
+ }
25
+
26
+ function normalizeAddress(address: string) {
27
+ return address.toLowerCase();
28
+ }
29
+
30
+ export function getInjectedEthereum() {
31
+ return globalThis.window?.ethereum ?? null;
32
+ }
33
+
34
+ export class Eip1193Provider implements Web3Provider {
35
+ readonly id = "eip1193";
36
+
37
+ private account: string | null = null;
38
+ private onAccountsChanged: ((accounts: unknown) => void) | null = null;
39
+
40
+ constructor(
41
+ private ethereum: Eip1193Like,
42
+ private onAccountChange?: (pubKey: PubKey) => void
43
+ ) {}
44
+
45
+ static fromInjected(onAccountChange?: (pubKey: PubKey) => void) {
46
+ const ethereum = getInjectedEthereum();
47
+
48
+ if (!ethereum) {
49
+ throw new Error("No injected EIP-1193 wallet found (MetaMask and others)");
50
+ }
51
+
52
+ return new Eip1193Provider(ethereum, onAccountChange);
53
+ }
54
+
55
+ static fromEthereum(ethereum: Eip1193Like, onAccountChange?: (pubKey: PubKey) => void) {
56
+ return new Eip1193Provider(ethereum, onAccountChange);
57
+ }
58
+
59
+ async connect() {
60
+ const accounts = await this.requestAccounts();
61
+ const account = accounts[0];
62
+
63
+ if (!account) {
64
+ throw new Error("Wallet returned no accounts");
65
+ }
66
+
67
+ this.account = normalizeAddress(account);
68
+ this.watchAccounts();
69
+
70
+ return this.toPubKey(this.account);
71
+ }
72
+
73
+ async disconnect() {
74
+ this.unwatchAccounts();
75
+ this.account = null;
76
+ }
77
+
78
+ async signMessage(message: string | Uint8Array) {
79
+ const account = await this.getAccount();
80
+
81
+ const signature = await this.ethereum.request({
82
+ method: "personal_sign",
83
+ params: [toHexMessage(message), account],
84
+ });
85
+
86
+ if (typeof signature !== "string" || !signature.length) {
87
+ throw new Error("Wallet returned an invalid signature");
88
+ }
89
+
90
+ return signature.startsWith("0x") ? signature : `0x${signature}`;
91
+ }
92
+
93
+ getAccount() {
94
+ if (this.account) return Promise.resolve(this.account);
95
+
96
+ return this.requestAccounts().then((accounts) => {
97
+ const account = accounts[0];
98
+
99
+ if (!account) {
100
+ throw new Error("Wallet is not connected");
101
+ }
102
+
103
+ this.account = normalizeAddress(account);
104
+ this.watchAccounts();
105
+
106
+ return this.account;
107
+ });
108
+ }
109
+
110
+ private async requestAccounts() {
111
+ const accounts = await this.ethereum.request({
112
+ method: "eth_requestAccounts",
113
+ });
114
+
115
+ if (!Array.isArray(accounts)) {
116
+ throw new Error("Wallet returned an invalid accounts list");
117
+ }
118
+
119
+ return accounts.filter((account): account is string => typeof account === "string");
120
+ }
121
+
122
+ private toPubKey(address: string): PubKey {
123
+ return {
124
+ type: "secp256k1",
125
+ value: address,
126
+ encoding: "hex",
127
+ };
128
+ }
129
+
130
+ private watchAccounts() {
131
+ if (!this.ethereum.on || this.onAccountsChanged) return;
132
+
133
+ this.onAccountsChanged = (accounts: unknown) => {
134
+ if (!Array.isArray(accounts) || typeof accounts[0] !== "string") {
135
+ this.account = null;
136
+ return;
137
+ }
138
+
139
+ this.account = normalizeAddress(accounts[0]);
140
+ this.onAccountChange?.(this.toPubKey(this.account));
141
+ };
142
+
143
+ this.ethereum.on("accountsChanged", this.onAccountsChanged);
144
+ }
145
+
146
+ private unwatchAccounts() {
147
+ if (!this.onAccountsChanged || !this.ethereum.removeListener) return;
148
+
149
+ this.ethereum.removeListener("accountsChanged", this.onAccountsChanged);
150
+ this.onAccountsChanged = null;
151
+ }
152
+ }
@@ -0,0 +1,71 @@
1
+ import type { PubKey, Web3Provider } from "../types/web3-types";
2
+ import { Eip1193Provider, getInjectedEthereum } from "./eip1193.provider";
3
+ import { SolanaProvider, getInjectedSolana } from "./solana.provider";
4
+ import { TonConnectProvider, getInjectedTonBridgeKey, hasInjectedTonWallet } from "./tonconnect.provider";
5
+ import { TronProvider, getInjectedTronLink } from "./tron.provider";
6
+
7
+ export type InjectedWalletKind = "ethereum" | "solana" | "tron" | "ton";
8
+
9
+ export type InjectedProviderOptions = {
10
+ tonManifestUrl?: string;
11
+ };
12
+
13
+ const WALLET_LABELS: Record<InjectedWalletKind, string> = {
14
+ ethereum: "Ethereum (MetaMask and EIP-1193 wallets)",
15
+ solana: "Solana (Phantom and other Solana wallets)",
16
+ tron: "Tron (TronLink)",
17
+ ton: "TON (Tonkeeper and other TON wallets)",
18
+ };
19
+
20
+ export function getInjectedWalletLabel(kind: InjectedWalletKind) {
21
+ return WALLET_LABELS[kind];
22
+ }
23
+
24
+ export function hasInjectedWallet(kind: InjectedWalletKind) {
25
+ switch (kind) {
26
+ case "ethereum":
27
+ return !!getInjectedEthereum();
28
+ case "solana":
29
+ return !!getInjectedSolana();
30
+ case "tron":
31
+ return !!getInjectedTronLink();
32
+ case "ton":
33
+ return hasInjectedTonWallet();
34
+ }
35
+ }
36
+
37
+ export function detectInjectedWallets() {
38
+ const kinds: InjectedWalletKind[] = ["ethereum", "solana", "tron", "ton"];
39
+ return kinds.filter(hasInjectedWallet);
40
+ }
41
+
42
+ export function createInjectedProvider(
43
+ kind: InjectedWalletKind,
44
+ onAccountChange?: (pubKey: PubKey) => void,
45
+ options: InjectedProviderOptions = {},
46
+ ) {
47
+ switch (kind) {
48
+ case "ethereum":
49
+ return Eip1193Provider.fromInjected(onAccountChange);
50
+ case "solana":
51
+ return SolanaProvider.fromInjected(onAccountChange);
52
+ case "tron":
53
+ return TronProvider.fromInjected(onAccountChange);
54
+ case "ton": {
55
+ const manifestUrl = options.tonManifestUrl;
56
+
57
+ if (!manifestUrl) {
58
+ throw new Error("TON wallet requires tonManifestUrl (Ton Connect manifest)");
59
+ }
60
+
61
+ return TonConnectProvider.create(
62
+ { manifestUrl, transport: "extension", jsBridgeKey: getInjectedTonBridgeKey() ?? undefined },
63
+ onAccountChange,
64
+ );
65
+ }
66
+ }
67
+ }
68
+
69
+ export function asWeb3Provider(provider: Web3Provider) {
70
+ return provider;
71
+ }
@@ -0,0 +1,13 @@
1
+ export function toMessageBytes(message: string | Uint8Array) {
2
+ return message instanceof Uint8Array ? message : new TextEncoder().encode(message);
3
+ }
4
+
5
+ export function bytesToBase64(bytes: Uint8Array) {
6
+ let binary = "";
7
+
8
+ for (const byte of bytes) {
9
+ binary += String.fromCharCode(byte);
10
+ }
11
+
12
+ return btoa(binary);
13
+ }
@@ -0,0 +1,116 @@
1
+ import type { PubKey, Web3Provider } from "../types/web3-types";
2
+ import { bytesToBase64, toMessageBytes } from "./message-bytes";
3
+
4
+ export interface SolanaPublicKey {
5
+ toBase58(): string;
6
+ }
7
+
8
+ export interface SolanaWallet {
9
+ isPhantom?: boolean;
10
+ isConnected?: boolean;
11
+ publicKey?: SolanaPublicKey | null;
12
+ connect(): Promise<{ publicKey: SolanaPublicKey }>;
13
+ disconnect(): Promise<void>;
14
+ signMessage(
15
+ message: Uint8Array,
16
+ display?: "utf8" | "hex",
17
+ ): Promise<{ signature: Uint8Array }>;
18
+ on?(event: "accountChanged", listener: (publicKey: SolanaPublicKey | null) => void): void;
19
+ removeListener?(
20
+ event: "accountChanged",
21
+ listener: (publicKey: SolanaPublicKey | null) => void,
22
+ ): void;
23
+ }
24
+
25
+ declare global {
26
+ interface Window {
27
+ solana?: SolanaWallet;
28
+ phantom?: { solana?: SolanaWallet };
29
+ }
30
+ }
31
+
32
+ export function getInjectedSolana() {
33
+ return globalThis.window?.phantom?.solana ?? globalThis.window?.solana ?? null;
34
+ }
35
+
36
+ export class SolanaProvider implements Web3Provider {
37
+ readonly id = "solana";
38
+
39
+ private accountListener: ((publicKey: SolanaPublicKey | null) => void) | null = null;
40
+
41
+ constructor(
42
+ private wallet: SolanaWallet,
43
+ private onAccountChange?: (pubKey: PubKey) => void,
44
+ ) {}
45
+
46
+ static fromInjected(onAccountChange?: (pubKey: PubKey) => void) {
47
+ const wallet = getInjectedSolana();
48
+
49
+ if (!wallet) {
50
+ throw new Error("No injected Solana wallet found (install Phantom or another Solana wallet)");
51
+ }
52
+
53
+ return new SolanaProvider(wallet, onAccountChange);
54
+ }
55
+
56
+ async connect() {
57
+ const { publicKey } = await this.wallet.connect();
58
+ this.watchAccount();
59
+ return this.toPubKey(publicKey);
60
+ }
61
+
62
+ async disconnect() {
63
+ this.unwatchAccount();
64
+ await this.wallet.disconnect();
65
+ }
66
+
67
+ async signMessage(message: string | Uint8Array) {
68
+ const publicKey = await this.requirePublicKey();
69
+ const bytes = toMessageBytes(message);
70
+
71
+ const { signature } = await this.wallet.signMessage(bytes, "utf8");
72
+
73
+ if (!(signature instanceof Uint8Array) || !signature.length) {
74
+ throw new Error("Wallet returned an invalid signature");
75
+ }
76
+
77
+ if (publicKey.toBase58() !== (this.wallet.publicKey?.toBase58() ?? "")) {
78
+ throw new Error("Connected Solana wallet account changed before signing");
79
+ }
80
+
81
+ return bytesToBase64(signature);
82
+ }
83
+
84
+ private async requirePublicKey() {
85
+ if (this.wallet.publicKey) return this.wallet.publicKey;
86
+
87
+ const { publicKey } = await this.wallet.connect();
88
+ return publicKey;
89
+ }
90
+
91
+ private toPubKey(publicKey: SolanaPublicKey): PubKey {
92
+ return {
93
+ type: "Ed25519",
94
+ value: publicKey.toBase58(),
95
+ encoding: "base58",
96
+ };
97
+ }
98
+
99
+ private watchAccount() {
100
+ if (!this.wallet.on || this.accountListener) return;
101
+
102
+ this.accountListener = (publicKey) => {
103
+ if (!publicKey) return;
104
+ this.onAccountChange?.(this.toPubKey(publicKey));
105
+ };
106
+
107
+ this.wallet.on("accountChanged", this.accountListener);
108
+ }
109
+
110
+ private unwatchAccount() {
111
+ if (!this.accountListener || !this.wallet.removeListener) return;
112
+
113
+ this.wallet.removeListener("accountChanged", this.accountListener);
114
+ this.accountListener = null;
115
+ }
116
+ }
@@ -0,0 +1,161 @@
1
+ import { TonConnect } from "@tonconnect/sdk";
2
+ import { TonConnectUI } from "@tonconnect/ui";
3
+ import type { Account } from "@tonconnect/sdk";
4
+
5
+ import type { PubKey, Web3Provider } from "../types/web3-types";
6
+ import { toMessageBytes } from "./message-bytes";
7
+ import type { WalletTransport } from "./wallet-transport";
8
+
9
+ export type TonConnectProviderConfig = {
10
+ manifestUrl: string;
11
+ transport?: WalletTransport;
12
+ jsBridgeKey?: string;
13
+ };
14
+
15
+ const INJECTED_TON_WALLETS = ["tonkeeper", "mytonwallet", "tonhub"] as const;
16
+
17
+ export function getInjectedTonBridgeKey() {
18
+ for (const key of INJECTED_TON_WALLETS) {
19
+ if (TonConnect.isWalletInjected(key)) return key;
20
+ }
21
+
22
+ return null;
23
+ }
24
+
25
+ export function hasInjectedTonWallet() {
26
+ return !!getInjectedTonBridgeKey();
27
+ }
28
+
29
+ export class TonConnectProvider implements Web3Provider {
30
+ readonly id = "tonconnect";
31
+
32
+ private ui: TonConnectUI;
33
+ private unsubscribeStatus: (() => void) | null = null;
34
+
35
+ constructor(
36
+ private config: TonConnectProviderConfig,
37
+ private onAccountChange?: (pubKey: PubKey) => void,
38
+ ) {
39
+ this.ui = new TonConnectUI({ manifestUrl: config.manifestUrl });
40
+ this.watchStatus();
41
+ }
42
+
43
+ static create(
44
+ config: TonConnectProviderConfig,
45
+ onAccountChange?: (pubKey: PubKey) => void,
46
+ ) {
47
+ return new TonConnectProvider(config, onAccountChange);
48
+ }
49
+
50
+ async connect() {
51
+ await this.ui.connectionRestored;
52
+
53
+ if (this.ui.account) {
54
+ const pubKey = this.toPubKey(this.ui.account);
55
+ this.onAccountChange?.(pubKey);
56
+ return pubKey;
57
+ }
58
+
59
+ const transport = this.resolveTransport();
60
+
61
+ if (transport === "extension") {
62
+ const jsBridgeKey = this.config.jsBridgeKey ?? getInjectedTonBridgeKey();
63
+
64
+ if (!jsBridgeKey) {
65
+ throw new Error("No injected TON wallet found (install Tonkeeper or another TON wallet)");
66
+ }
67
+
68
+ return this.waitForAccount(() => {
69
+ void this.ui.connector.connect({ jsBridgeKey });
70
+ });
71
+ }
72
+
73
+ return this.waitForAccount(() => {
74
+ void this.ui.openModal();
75
+ });
76
+ }
77
+
78
+ async disconnect() {
79
+ await this.ui.disconnect();
80
+ }
81
+
82
+ async signMessage(message: string | Uint8Array) {
83
+ const account = this.requireAccount();
84
+ const text = new TextDecoder().decode(toMessageBytes(message));
85
+
86
+ const response = await this.ui.signData({
87
+ type: "text",
88
+ text,
89
+ from: account.address,
90
+ });
91
+
92
+ if (!response.signature) {
93
+ throw new Error("Wallet returned an invalid signature");
94
+ }
95
+
96
+ return response.signature;
97
+ }
98
+
99
+ private resolveTransport(): WalletTransport {
100
+ const transport = this.config.transport ?? "auto";
101
+
102
+ if (transport !== "auto") return transport;
103
+
104
+ return getInjectedTonBridgeKey() ? "extension" : "app";
105
+ }
106
+
107
+ private requireAccount() {
108
+ const account = this.ui.account;
109
+
110
+ if (!account) {
111
+ throw new Error("TON wallet is not connected");
112
+ }
113
+
114
+ return account;
115
+ }
116
+
117
+ private waitForAccount(start: () => void) {
118
+ return new Promise<PubKey>((resolve, reject) => {
119
+ const unsubscribe = this.ui.onStatusChange(
120
+ (wallet) => {
121
+ if (!wallet?.account) return;
122
+
123
+ unsubscribe();
124
+ const pubKey = this.toPubKey(wallet.account);
125
+ this.onAccountChange?.(pubKey);
126
+ resolve(pubKey);
127
+ },
128
+ (error) => {
129
+ unsubscribe();
130
+ reject(error);
131
+ },
132
+ );
133
+
134
+ try {
135
+ start();
136
+ } catch (error) {
137
+ unsubscribe();
138
+ reject(error);
139
+ }
140
+ });
141
+ }
142
+
143
+ private toPubKey(account: Account): PubKey {
144
+ if (!account.publicKey) {
145
+ throw new Error("TON wallet did not return a public key");
146
+ }
147
+
148
+ return {
149
+ type: "Ed25519",
150
+ value: account.publicKey,
151
+ encoding: "hex",
152
+ };
153
+ }
154
+
155
+ private watchStatus() {
156
+ this.unsubscribeStatus = this.ui.onStatusChange((wallet) => {
157
+ if (!wallet?.account) return;
158
+ this.onAccountChange?.(this.toPubKey(wallet.account));
159
+ });
160
+ }
161
+ }