@horus-wallet/sdk-react 0.1.0-beta.2 → 0.3.0-beta.1

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.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * `useConnectWallet()` — connect a user's external wallet (MetaMask
3
+ * or any EIP-1193-compatible injected provider).
4
+ *
5
+ * import { useConnectWallet } from '@horus-wallet/sdk-react/connect';
6
+ *
7
+ * const { connect, disconnect, wallet, available } = useConnectWallet();
8
+ *
9
+ * // In a click handler:
10
+ * const w = await connect();
11
+ * // → { address: '0xabc…', chainId: 1, provider: <EIP-1193> }
12
+ *
13
+ * Separate from the embedded-wallet hooks: this surface NEVER touches
14
+ * Horus's backend. The user's private key stays in their wallet
15
+ * extension; Horus never sees it. The hook just gives partners a
16
+ * unified way to surface "Connect" / "Switch" / "Sign with MetaMask"
17
+ * alongside their embedded-wallet flow.
18
+ *
19
+ * Lives in the `/connect` sub-entrypoint so partners who only use
20
+ * embedded wallets don't pay the bundle cost for EIP-1193 plumbing.
21
+ *
22
+ * v1 scope: injected providers only (`window.ethereum`). v2 will add
23
+ * WalletConnect via its own peer dep (large enough to warrant its own
24
+ * opt-in).
25
+ */
26
+ /**
27
+ * EIP-1193 provider — the minimal surface we depend on. Defining it
28
+ * locally keeps the SDK free of an `ethers` / `viem` peer-dep just for
29
+ * type signatures. Partners can cast `provider` to ethers' or viem's
30
+ * provider type on the way out.
31
+ */
32
+ interface Eip1193Provider {
33
+ request: (args: {
34
+ method: string;
35
+ params?: unknown[] | object;
36
+ }) => Promise<unknown>;
37
+ on?: (eventName: string, handler: (...args: any[]) => void) => void;
38
+ removeListener?: (eventName: string, handler: (...args: any[]) => void) => void;
39
+ isMetaMask?: boolean;
40
+ isCoinbaseWallet?: boolean;
41
+ isRabby?: boolean;
42
+ isBraveWallet?: boolean;
43
+ }
44
+ interface ConnectedExternalWallet {
45
+ /** EVM address (`0x…`, 40 hex chars). */
46
+ address: string;
47
+ /** Chain ID as a base-10 number (e.g., 1 for Ethereum mainnet). */
48
+ chainId: number;
49
+ /** Best-effort wallet brand name based on injected provider flags. */
50
+ walletName: string;
51
+ /** The raw EIP-1193 provider, passable to ethers / viem signers. */
52
+ provider: Eip1193Provider;
53
+ }
54
+ interface UseConnectWalletResult {
55
+ /**
56
+ * Currently-connected wallet, or `null` if the user hasn't connected
57
+ * (or has disconnected via their wallet UI).
58
+ */
59
+ wallet: ConnectedExternalWallet | null;
60
+ /** True while a `connect()` call is in flight. */
61
+ pending: boolean;
62
+ /** Last error from a connect/disconnect attempt. */
63
+ error: Error | undefined;
64
+ /** True if an EIP-1193 provider is detected on the page. */
65
+ available: boolean;
66
+ /** Prompt the user to connect their injected wallet. */
67
+ connect: () => Promise<ConnectedExternalWallet>;
68
+ /** Forget the current connection (does NOT log the user out of their wallet extension). */
69
+ disconnect: () => void;
70
+ }
71
+ declare function useConnectWallet(): UseConnectWalletResult;
72
+
73
+ /**
74
+ * `useExternalSignMessage()` / `useExternalSignTypedData()` /
75
+ * `useExternalSendTransaction()` — signing helpers for external
76
+ * (EIP-1193) wallets connected via `useConnectWallet`.
77
+ *
78
+ * Separate from the embedded-wallet hooks because the dispatch model
79
+ * is fundamentally different: external wallets sign in the browser
80
+ * (no backend round-trip, no Bearer auth), while embedded wallets are
81
+ * signed by the Horus backend.
82
+ *
83
+ * Pattern:
84
+ *
85
+ * import {
86
+ * useConnectWallet,
87
+ * useExternalSignMessage,
88
+ * } from '@horus-wallet/sdk-react/connect';
89
+ *
90
+ * const { wallet, connect } = useConnectWallet();
91
+ * const { signMessage } = useExternalSignMessage();
92
+ *
93
+ * const sig = await signMessage(wallet ?? await connect(), 'hello');
94
+ *
95
+ * Why a `wallet` parameter on every call instead of context state:
96
+ * partners often need to sign with a SPECIFIC wallet (the user might
97
+ * have multiple injected providers, e.g. MetaMask + Coinbase Wallet in
98
+ * the same browser). Forcing the wallet through the call site keeps
99
+ * intent explicit and avoids the "active wallet leaked into the
100
+ * wrong sign" footgun.
101
+ */
102
+
103
+ interface UseExternalSignMessageResult {
104
+ signMessage: (wallet: ConnectedExternalWallet, message: string) => Promise<string>;
105
+ pending: boolean;
106
+ error: Error | undefined;
107
+ }
108
+ declare function useExternalSignMessage(): UseExternalSignMessageResult;
109
+ /**
110
+ * EIP-712 typed-data signing via the external wallet. The wallet pops
111
+ * its own confirmation UI showing the typed-data fields; we don't
112
+ * render anything in the partner page.
113
+ */
114
+ interface UseExternalSignTypedDataResult {
115
+ signTypedData: (wallet: ConnectedExternalWallet, typedData: {
116
+ domain: Record<string, unknown>;
117
+ types: Record<string, Array<{
118
+ name: string;
119
+ type: string;
120
+ }>>;
121
+ primaryType?: string;
122
+ message: Record<string, unknown>;
123
+ }) => Promise<string>;
124
+ pending: boolean;
125
+ error: Error | undefined;
126
+ }
127
+ declare function useExternalSignTypedData(): UseExternalSignTypedDataResult;
128
+ /**
129
+ * Send a transaction from the external wallet. The wallet broadcasts
130
+ * via its own RPC and returns the tx hash. Confirmation polling is
131
+ * the partner's job — same as `useSendTransaction` for embedded.
132
+ */
133
+ interface ExternalTx {
134
+ to: string;
135
+ value?: string;
136
+ data?: string;
137
+ gas?: string;
138
+ /**
139
+ * Optional — if the external wallet is on the wrong chain, the
140
+ * partner can pass the target chainId here and the hook will request
141
+ * a switchEthereumChain before sending. The hook does NOT auto-add
142
+ * unknown chains; if the wallet rejects the switch, the call throws.
143
+ */
144
+ chainId?: number;
145
+ }
146
+ interface UseExternalSendTransactionResult {
147
+ sendTransaction: (wallet: ConnectedExternalWallet, tx: ExternalTx) => Promise<{
148
+ hash: string;
149
+ }>;
150
+ pending: boolean;
151
+ error: Error | undefined;
152
+ }
153
+ declare function useExternalSendTransaction(): UseExternalSendTransactionResult;
154
+
155
+ export { type ConnectedExternalWallet, type Eip1193Provider, useConnectWallet, useExternalSendTransaction, useExternalSignMessage, useExternalSignTypedData };
@@ -0,0 +1,155 @@
1
+ /**
2
+ * `useConnectWallet()` — connect a user's external wallet (MetaMask
3
+ * or any EIP-1193-compatible injected provider).
4
+ *
5
+ * import { useConnectWallet } from '@horus-wallet/sdk-react/connect';
6
+ *
7
+ * const { connect, disconnect, wallet, available } = useConnectWallet();
8
+ *
9
+ * // In a click handler:
10
+ * const w = await connect();
11
+ * // → { address: '0xabc…', chainId: 1, provider: <EIP-1193> }
12
+ *
13
+ * Separate from the embedded-wallet hooks: this surface NEVER touches
14
+ * Horus's backend. The user's private key stays in their wallet
15
+ * extension; Horus never sees it. The hook just gives partners a
16
+ * unified way to surface "Connect" / "Switch" / "Sign with MetaMask"
17
+ * alongside their embedded-wallet flow.
18
+ *
19
+ * Lives in the `/connect` sub-entrypoint so partners who only use
20
+ * embedded wallets don't pay the bundle cost for EIP-1193 plumbing.
21
+ *
22
+ * v1 scope: injected providers only (`window.ethereum`). v2 will add
23
+ * WalletConnect via its own peer dep (large enough to warrant its own
24
+ * opt-in).
25
+ */
26
+ /**
27
+ * EIP-1193 provider — the minimal surface we depend on. Defining it
28
+ * locally keeps the SDK free of an `ethers` / `viem` peer-dep just for
29
+ * type signatures. Partners can cast `provider` to ethers' or viem's
30
+ * provider type on the way out.
31
+ */
32
+ interface Eip1193Provider {
33
+ request: (args: {
34
+ method: string;
35
+ params?: unknown[] | object;
36
+ }) => Promise<unknown>;
37
+ on?: (eventName: string, handler: (...args: any[]) => void) => void;
38
+ removeListener?: (eventName: string, handler: (...args: any[]) => void) => void;
39
+ isMetaMask?: boolean;
40
+ isCoinbaseWallet?: boolean;
41
+ isRabby?: boolean;
42
+ isBraveWallet?: boolean;
43
+ }
44
+ interface ConnectedExternalWallet {
45
+ /** EVM address (`0x…`, 40 hex chars). */
46
+ address: string;
47
+ /** Chain ID as a base-10 number (e.g., 1 for Ethereum mainnet). */
48
+ chainId: number;
49
+ /** Best-effort wallet brand name based on injected provider flags. */
50
+ walletName: string;
51
+ /** The raw EIP-1193 provider, passable to ethers / viem signers. */
52
+ provider: Eip1193Provider;
53
+ }
54
+ interface UseConnectWalletResult {
55
+ /**
56
+ * Currently-connected wallet, or `null` if the user hasn't connected
57
+ * (or has disconnected via their wallet UI).
58
+ */
59
+ wallet: ConnectedExternalWallet | null;
60
+ /** True while a `connect()` call is in flight. */
61
+ pending: boolean;
62
+ /** Last error from a connect/disconnect attempt. */
63
+ error: Error | undefined;
64
+ /** True if an EIP-1193 provider is detected on the page. */
65
+ available: boolean;
66
+ /** Prompt the user to connect their injected wallet. */
67
+ connect: () => Promise<ConnectedExternalWallet>;
68
+ /** Forget the current connection (does NOT log the user out of their wallet extension). */
69
+ disconnect: () => void;
70
+ }
71
+ declare function useConnectWallet(): UseConnectWalletResult;
72
+
73
+ /**
74
+ * `useExternalSignMessage()` / `useExternalSignTypedData()` /
75
+ * `useExternalSendTransaction()` — signing helpers for external
76
+ * (EIP-1193) wallets connected via `useConnectWallet`.
77
+ *
78
+ * Separate from the embedded-wallet hooks because the dispatch model
79
+ * is fundamentally different: external wallets sign in the browser
80
+ * (no backend round-trip, no Bearer auth), while embedded wallets are
81
+ * signed by the Horus backend.
82
+ *
83
+ * Pattern:
84
+ *
85
+ * import {
86
+ * useConnectWallet,
87
+ * useExternalSignMessage,
88
+ * } from '@horus-wallet/sdk-react/connect';
89
+ *
90
+ * const { wallet, connect } = useConnectWallet();
91
+ * const { signMessage } = useExternalSignMessage();
92
+ *
93
+ * const sig = await signMessage(wallet ?? await connect(), 'hello');
94
+ *
95
+ * Why a `wallet` parameter on every call instead of context state:
96
+ * partners often need to sign with a SPECIFIC wallet (the user might
97
+ * have multiple injected providers, e.g. MetaMask + Coinbase Wallet in
98
+ * the same browser). Forcing the wallet through the call site keeps
99
+ * intent explicit and avoids the "active wallet leaked into the
100
+ * wrong sign" footgun.
101
+ */
102
+
103
+ interface UseExternalSignMessageResult {
104
+ signMessage: (wallet: ConnectedExternalWallet, message: string) => Promise<string>;
105
+ pending: boolean;
106
+ error: Error | undefined;
107
+ }
108
+ declare function useExternalSignMessage(): UseExternalSignMessageResult;
109
+ /**
110
+ * EIP-712 typed-data signing via the external wallet. The wallet pops
111
+ * its own confirmation UI showing the typed-data fields; we don't
112
+ * render anything in the partner page.
113
+ */
114
+ interface UseExternalSignTypedDataResult {
115
+ signTypedData: (wallet: ConnectedExternalWallet, typedData: {
116
+ domain: Record<string, unknown>;
117
+ types: Record<string, Array<{
118
+ name: string;
119
+ type: string;
120
+ }>>;
121
+ primaryType?: string;
122
+ message: Record<string, unknown>;
123
+ }) => Promise<string>;
124
+ pending: boolean;
125
+ error: Error | undefined;
126
+ }
127
+ declare function useExternalSignTypedData(): UseExternalSignTypedDataResult;
128
+ /**
129
+ * Send a transaction from the external wallet. The wallet broadcasts
130
+ * via its own RPC and returns the tx hash. Confirmation polling is
131
+ * the partner's job — same as `useSendTransaction` for embedded.
132
+ */
133
+ interface ExternalTx {
134
+ to: string;
135
+ value?: string;
136
+ data?: string;
137
+ gas?: string;
138
+ /**
139
+ * Optional — if the external wallet is on the wrong chain, the
140
+ * partner can pass the target chainId here and the hook will request
141
+ * a switchEthereumChain before sending. The hook does NOT auto-add
142
+ * unknown chains; if the wallet rejects the switch, the call throws.
143
+ */
144
+ chainId?: number;
145
+ }
146
+ interface UseExternalSendTransactionResult {
147
+ sendTransaction: (wallet: ConnectedExternalWallet, tx: ExternalTx) => Promise<{
148
+ hash: string;
149
+ }>;
150
+ pending: boolean;
151
+ error: Error | undefined;
152
+ }
153
+ declare function useExternalSendTransaction(): UseExternalSendTransactionResult;
154
+
155
+ export { type ConnectedExternalWallet, type Eip1193Provider, useConnectWallet, useExternalSendTransaction, useExternalSignMessage, useExternalSignTypedData };
@@ -0,0 +1,188 @@
1
+ // src/connect/useConnectWallet.ts
2
+ import { useCallback, useEffect, useState } from "react";
3
+ function detectProvider() {
4
+ if (typeof window === "undefined") return null;
5
+ const provider = window.ethereum;
6
+ return provider ?? null;
7
+ }
8
+ function nameFor(provider) {
9
+ if (provider.isMetaMask) return "MetaMask";
10
+ if (provider.isCoinbaseWallet) return "Coinbase Wallet";
11
+ if (provider.isRabby) return "Rabby";
12
+ if (provider.isBraveWallet) return "Brave Wallet";
13
+ return "Injected Wallet";
14
+ }
15
+ function useConnectWallet() {
16
+ const [wallet, setWallet] = useState(null);
17
+ const [pending, setPending] = useState(false);
18
+ const [error, setError] = useState(void 0);
19
+ const [available, setAvailable] = useState(false);
20
+ useEffect(() => {
21
+ setAvailable(detectProvider() !== null);
22
+ }, []);
23
+ useEffect(() => {
24
+ const provider = detectProvider();
25
+ if (!provider || !provider.on) return;
26
+ const onAccountsChanged = (accounts) => {
27
+ if (!accounts || accounts.length === 0) {
28
+ setWallet(null);
29
+ return;
30
+ }
31
+ setWallet((prev) => prev ? { ...prev, address: accounts[0] } : prev);
32
+ };
33
+ const onChainChanged = (chainIdHex) => {
34
+ const chainId = Number(chainIdHex);
35
+ if (!Number.isFinite(chainId)) return;
36
+ setWallet((prev) => prev ? { ...prev, chainId } : prev);
37
+ };
38
+ provider.on("accountsChanged", onAccountsChanged);
39
+ provider.on("chainChanged", onChainChanged);
40
+ return () => {
41
+ provider.removeListener?.("accountsChanged", onAccountsChanged);
42
+ provider.removeListener?.("chainChanged", onChainChanged);
43
+ };
44
+ }, []);
45
+ const connect = useCallback(async () => {
46
+ const provider = detectProvider();
47
+ if (!provider) {
48
+ const err = new Error(
49
+ "No injected wallet provider detected. Install MetaMask (or similar) and reload."
50
+ );
51
+ setError(err);
52
+ throw err;
53
+ }
54
+ setPending(true);
55
+ setError(void 0);
56
+ try {
57
+ const accounts = await provider.request({
58
+ method: "eth_requestAccounts"
59
+ });
60
+ if (!accounts || accounts.length === 0) {
61
+ throw new Error("No accounts returned from wallet.");
62
+ }
63
+ const chainIdHex = await provider.request({
64
+ method: "eth_chainId"
65
+ });
66
+ const chainId = Number(chainIdHex);
67
+ const next = {
68
+ address: accounts[0],
69
+ chainId: Number.isFinite(chainId) ? chainId : 0,
70
+ walletName: nameFor(provider),
71
+ provider
72
+ };
73
+ setWallet(next);
74
+ return next;
75
+ } catch (err) {
76
+ const e = err instanceof Error ? err : new Error(String(err));
77
+ setError(e);
78
+ throw e;
79
+ } finally {
80
+ setPending(false);
81
+ }
82
+ }, []);
83
+ const disconnect = useCallback(() => {
84
+ setWallet(null);
85
+ }, []);
86
+ return { wallet, pending, error, available, connect, disconnect };
87
+ }
88
+
89
+ // src/connect/useExternalSign.ts
90
+ import { useCallback as useCallback2, useState as useState2 } from "react";
91
+ function useExternalSignMessage() {
92
+ const [pending, setPending] = useState2(false);
93
+ const [error, setError] = useState2(void 0);
94
+ const signMessage = useCallback2(
95
+ async (wallet, message) => {
96
+ setPending(true);
97
+ setError(void 0);
98
+ try {
99
+ const sig = await wallet.provider.request({
100
+ method: "personal_sign",
101
+ params: [message, wallet.address]
102
+ });
103
+ return sig;
104
+ } catch (err) {
105
+ const e = err instanceof Error ? err : new Error(String(err));
106
+ setError(e);
107
+ throw e;
108
+ } finally {
109
+ setPending(false);
110
+ }
111
+ },
112
+ []
113
+ );
114
+ return { signMessage, pending, error };
115
+ }
116
+ function useExternalSignTypedData() {
117
+ const [pending, setPending] = useState2(false);
118
+ const [error, setError] = useState2(void 0);
119
+ const signTypedData = useCallback2(
120
+ async (wallet, typedData) => {
121
+ setPending(true);
122
+ setError(void 0);
123
+ try {
124
+ const sig = await wallet.provider.request({
125
+ method: "eth_signTypedData_v4",
126
+ params: [wallet.address, JSON.stringify(typedData)]
127
+ });
128
+ return sig;
129
+ } catch (err) {
130
+ const e = err instanceof Error ? err : new Error(String(err));
131
+ setError(e);
132
+ throw e;
133
+ } finally {
134
+ setPending(false);
135
+ }
136
+ },
137
+ []
138
+ );
139
+ return { signTypedData, pending, error };
140
+ }
141
+ function useExternalSendTransaction() {
142
+ const [pending, setPending] = useState2(false);
143
+ const [error, setError] = useState2(void 0);
144
+ const sendTransaction = useCallback2(
145
+ async (wallet, tx) => {
146
+ setPending(true);
147
+ setError(void 0);
148
+ try {
149
+ if (tx.chainId && tx.chainId !== wallet.chainId) {
150
+ await wallet.provider.request({
151
+ method: "wallet_switchEthereumChain",
152
+ params: [{ chainId: `0x${tx.chainId.toString(16)}` }]
153
+ });
154
+ }
155
+ const params = {
156
+ from: wallet.address,
157
+ to: tx.to
158
+ };
159
+ if (tx.value !== void 0) params.value = toHex(tx.value);
160
+ if (tx.data !== void 0) params.data = tx.data;
161
+ if (tx.gas !== void 0) params.gas = toHex(tx.gas);
162
+ const hash = await wallet.provider.request({
163
+ method: "eth_sendTransaction",
164
+ params: [params]
165
+ });
166
+ return { hash };
167
+ } catch (err) {
168
+ const e = err instanceof Error ? err : new Error(String(err));
169
+ setError(e);
170
+ throw e;
171
+ } finally {
172
+ setPending(false);
173
+ }
174
+ },
175
+ []
176
+ );
177
+ return { sendTransaction, pending, error };
178
+ }
179
+ function toHex(v) {
180
+ if (v.startsWith("0x")) return v;
181
+ return `0x${BigInt(v).toString(16)}`;
182
+ }
183
+ export {
184
+ useConnectWallet,
185
+ useExternalSendTransaction,
186
+ useExternalSignMessage,
187
+ useExternalSignTypedData
188
+ };