@meshconnect/uwc-react 0.6.11 → 0.7.0-snapshot.6b75329

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.
package/README.md CHANGED
@@ -199,6 +199,34 @@ function WalletList() {
199
199
  }
200
200
  ```
201
201
 
202
+ #### useSignSolanaTransaction
203
+
204
+ Sign a Solana transaction **without broadcasting it** — for fee-payer relay flows where a relay (e.g. your backend) pays the network fee and submits the transaction. Raw bytes in, signed bytes out, so callers don't need `@solana/web3.js`:
205
+
206
+ ```tsx
207
+ import { useSignSolanaTransaction } from '@meshconnect/uwc-react'
208
+
209
+ function SignButton({ unsignedTx }: { unsignedTx: Uint8Array }) {
210
+ const { signSolanaTransaction, isLoading, error } = useSignSolanaTransaction()
211
+
212
+ const onSign = async () => {
213
+ const signedTx = await signSolanaTransaction(unsignedTx)
214
+ // Hand signedTx to your relay to broadcast — UWC never sends it.
215
+ }
216
+
217
+ return (
218
+ <div>
219
+ <button onClick={onSign} disabled={isLoading}>
220
+ {isLoading ? 'Signing...' : 'Sign Transaction'}
221
+ </button>
222
+ {error && <p>{error.message}</p>}
223
+ </div>
224
+ )
225
+ }
226
+ ```
227
+
228
+ Works with injected wallets, WalletConnect, and through the iframe bridge.
229
+
202
230
  ## API Reference
203
231
 
204
232
  ### ConnectionProvider
@@ -230,4 +258,7 @@ Returns the current session state.
230
258
  Returns the list of available wallets.
231
259
 
232
260
  #### useNetworks()
233
- Returns the list of available networks.
261
+ Returns the list of available networks.
262
+
263
+ #### useSignSolanaTransaction()
264
+ Returns a sign-only `signSolanaTransaction(bytes: Uint8Array) => Promise<Uint8Array>` for Solana fee-payer relay flows, plus `isLoading` and `error` state. Never broadcasts — the relay owns submission.
@@ -9,4 +9,5 @@ export * from './useSignMessage';
9
9
  export * from './useTransaction';
10
10
  export * from './useWalletCapabilities';
11
11
  export * from './useDetectedWallets';
12
+ export * from './useSignSolanaTransaction';
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,4BAA4B,CAAA"}
@@ -9,4 +9,5 @@ export * from './useSignMessage';
9
9
  export * from './useTransaction';
10
10
  export * from './useWalletCapabilities';
11
11
  export * from './useDetectedWallets';
12
+ export * from './useSignSolanaTransaction';
12
13
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,4BAA4B,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { UseSignSolanaTransactionReturn } from '@meshconnect/uwc-types';
2
+ export declare function useSignSolanaTransaction(): UseSignSolanaTransactionReturn;
3
+ //# sourceMappingURL=useSignSolanaTransaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSignSolanaTransaction.d.ts","sourceRoot":"","sources":["../../src/hooks/useSignSolanaTransaction.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,8BAA8B,EAE/B,MAAM,wBAAwB,CAAA;AAE/B,wBAAgB,wBAAwB,IAAI,8BAA8B,CAuCzE"}
@@ -0,0 +1,34 @@
1
+ import { useContext, useCallback, useState } from 'react';
2
+ import { ConnectionContext } from '../providers/ConnectionProvider';
3
+ export function useSignSolanaTransaction() {
4
+ const context = useContext(ConnectionContext);
5
+ if (!context) {
6
+ throw new Error('useSignSolanaTransaction must be used within a ConnectionProvider');
7
+ }
8
+ const { connector, session } = context;
9
+ const [isLoading, setIsLoading] = useState(false);
10
+ const [error, setError] = useState();
11
+ const signSolanaTransaction = useCallback(async (serializedTx) => {
12
+ if (!session.activeAddress) {
13
+ throw new Error('No wallet connected');
14
+ }
15
+ if (!serializedTx?.length) {
16
+ throw new Error('Transaction bytes are required');
17
+ }
18
+ setIsLoading(true);
19
+ setError(undefined);
20
+ try {
21
+ return await connector.signSolanaTransaction(serializedTx);
22
+ }
23
+ catch (err) {
24
+ const walletError = err;
25
+ setError(walletError);
26
+ throw err;
27
+ }
28
+ finally {
29
+ setIsLoading(false);
30
+ }
31
+ }, [connector, session.activeAddress]);
32
+ return { signSolanaTransaction, isLoading, error };
33
+ }
34
+ //# sourceMappingURL=useSignSolanaTransaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSignSolanaTransaction.js","sourceRoot":"","sources":["../../src/hooks/useSignSolanaTransaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AAMnE,MAAM,UAAU,wBAAwB;IACtC,MAAM,OAAO,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAA;IAE7C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,mEAAmE,CACpE,CAAA;IACH,CAAC;IAED,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IACtC,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACjD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,EAA2B,CAAA;IAE7D,MAAM,qBAAqB,GAAG,WAAW,CACvC,KAAK,EAAE,YAAwB,EAAuB,EAAE;QACtD,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAA;QACxC,CAAC;QAED,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAA;QACnD,CAAC;QAED,YAAY,CAAC,IAAI,CAAC,CAAA;QAClB,QAAQ,CAAC,SAAS,CAAC,CAAA;QACnB,IAAI,CAAC;YACH,OAAO,MAAM,SAAS,CAAC,qBAAqB,CAAC,YAAY,CAAC,CAAA;QAC5D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,WAAW,GAAG,GAAkB,CAAA;YACtC,QAAQ,CAAC,WAAW,CAAC,CAAA;YACrB,MAAM,GAAG,CAAA;QACX,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAA;QACrB,CAAC;IACH,CAAC,EACD,CAAC,SAAS,EAAE,OAAO,CAAC,aAAa,CAAC,CACnC,CAAA;IAED,OAAO,EAAE,qBAAqB,EAAE,SAAS,EAAE,KAAK,EAAE,CAAA;AACpD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meshconnect/uwc-react",
3
- "version": "0.6.11",
3
+ "version": "0.7.0-snapshot.6b75329",
4
4
  "description": "React hooks and components for Universal Wallet Connector",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,8 +16,8 @@
16
16
  "src"
17
17
  ],
18
18
  "dependencies": {
19
- "@meshconnect/uwc-core": "0.7.11",
20
- "@meshconnect/uwc-types": "0.13.0"
19
+ "@meshconnect/uwc-core": "0.8.0-snapshot.6b75329",
20
+ "@meshconnect/uwc-types": "0.14.0-snapshot.6b75329"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "react": "^18.0.0",
@@ -9,3 +9,4 @@ export * from './useSignMessage'
9
9
  export * from './useTransaction'
10
10
  export * from './useWalletCapabilities'
11
11
  export * from './useDetectedWallets'
12
+ export * from './useSignSolanaTransaction'
@@ -0,0 +1,297 @@
1
+ import React, { act } from 'react'
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { ConnectionContext } from '../providers/ConnectionProvider'
5
+ import { useSignSolanaTransaction } from './useSignSolanaTransaction'
6
+ import type { UseSignSolanaTransactionReturn } from '@meshconnect/uwc-types'
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Helpers
10
+ // ---------------------------------------------------------------------------
11
+
12
+ /** Minimal stub that satisfies ConnectionContextValue's connector shape. */
13
+ function makeConnector(
14
+ signSolanaTransaction: (tx: Uint8Array) => Promise<Uint8Array>
15
+ ) {
16
+ return {
17
+ signSolanaTransaction,
18
+ // other connector methods are not exercised by this hook
19
+ getSession: () => ({ isConnected: false }),
20
+ isReady: () => true,
21
+ subscribe: () => () => {},
22
+ getWallets: () => [],
23
+ getNetworks: () => []
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Minimal context value that wraps a connector stub. Pass `{ activeAddress }`
29
+ * as an object (not a bare arg) so an explicit `undefined` survives — a default
30
+ * parameter would otherwise replace `undefined` with the connected address.
31
+ */
32
+ function makeContextValue(
33
+ connector: ReturnType<typeof makeConnector>,
34
+ { activeAddress }: { activeAddress: string | undefined } = {
35
+ activeAddress: 'SoLanaTestAddr1111111111111111111111111111'
36
+ }
37
+ ) {
38
+ return {
39
+ connector,
40
+ session: {
41
+ isConnected: activeAddress !== undefined,
42
+ walletId: undefined,
43
+ networkId: undefined,
44
+ activeAddress
45
+ },
46
+ wallets: [],
47
+ networks: [],
48
+ isReady: true
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Renders `useSignSolanaTransaction` inside a component that exposes its
54
+ * return value via a captured ref. Returns the ref and cleanup helpers.
55
+ *
56
+ * Pattern mirrors ConnectionProvider.singleton.test.tsx: createRoot + act,
57
+ * no @testing-library/react (not installed in this package).
58
+ */
59
+ function renderHookInContext(
60
+ contextValue: ReturnType<typeof makeContextValue>
61
+ ) {
62
+ const captured: { current: UseSignSolanaTransactionReturn | null } = {
63
+ current: null
64
+ }
65
+
66
+ function Capture() {
67
+ captured.current = useSignSolanaTransaction()
68
+ return null
69
+ }
70
+
71
+ const container = document.createElement('div')
72
+ document.body.appendChild(container)
73
+ const root = createRoot(container)
74
+
75
+ async function render() {
76
+ await act(async () => {
77
+ root.render(
78
+ <ConnectionContext.Provider value={contextValue as never}>
79
+ <Capture />
80
+ </ConnectionContext.Provider>
81
+ )
82
+ })
83
+ }
84
+
85
+ async function unmount() {
86
+ await act(async () => {
87
+ root.unmount()
88
+ })
89
+ document.body.removeChild(container)
90
+ }
91
+
92
+ return { captured, render, unmount }
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Tests
97
+ // ---------------------------------------------------------------------------
98
+
99
+ describe('useSignSolanaTransaction', () => {
100
+ let container: HTMLDivElement
101
+ let root: ReturnType<typeof createRoot>
102
+
103
+ beforeEach(() => {
104
+ container = document.createElement('div')
105
+ document.body.appendChild(container)
106
+ root = createRoot(container)
107
+ })
108
+
109
+ afterEach(async () => {
110
+ await act(async () => {
111
+ root.unmount()
112
+ })
113
+ if (document.body.contains(container)) {
114
+ document.body.removeChild(container)
115
+ }
116
+ })
117
+
118
+ // (a) throws when used outside ConnectionProvider
119
+ it('throws when rendered outside ConnectionProvider', () => {
120
+ function BareHook() {
121
+ useSignSolanaTransaction()
122
+ return null
123
+ }
124
+
125
+ expect(() => {
126
+ // Synchronous render — React will throw during render, caught here.
127
+ // Suppress the React error-boundary noise this logs on the way out.
128
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
129
+ try {
130
+ act(() => {
131
+ root.render(<BareHook />)
132
+ })
133
+ } finally {
134
+ errorSpy.mockRestore()
135
+ }
136
+ }).toThrowError(
137
+ 'useSignSolanaTransaction must be used within a ConnectionProvider'
138
+ )
139
+ })
140
+
141
+ // (b) returns the Uint8Array from connector.signSolanaTransaction on success
142
+ it('returns the signed bytes from connector on success', async () => {
143
+ const signedBytes = new Uint8Array([1, 2, 3, 4])
144
+ const mockSign = vi.fn().mockResolvedValue(signedBytes)
145
+ const ctxValue = makeContextValue(makeConnector(mockSign))
146
+ const { captured, render, unmount } = renderHookInContext(ctxValue)
147
+
148
+ await render()
149
+ expect(captured.current).not.toBeNull()
150
+
151
+ const input = new Uint8Array([9, 8, 7])
152
+ let result: Uint8Array | undefined
153
+
154
+ await act(async () => {
155
+ result = await captured.current!.signSolanaTransaction(input)
156
+ })
157
+
158
+ expect(mockSign).toHaveBeenCalledOnce()
159
+ expect(mockSign).toHaveBeenCalledWith(input)
160
+ expect(result).toBe(signedBytes)
161
+ await unmount()
162
+ })
163
+
164
+ // (c) sets error state and re-throws when connector.signSolanaTransaction rejects
165
+ it('sets error state and re-throws when connector rejects', async () => {
166
+ const walletError = { code: 4001, message: 'User rejected' }
167
+ const mockSign = vi.fn().mockRejectedValue(walletError)
168
+ const ctxValue = makeContextValue(makeConnector(mockSign))
169
+ const { captured, render, unmount } = renderHookInContext(ctxValue)
170
+
171
+ await render()
172
+
173
+ let thrownError: unknown
174
+ await act(async () => {
175
+ try {
176
+ await captured.current!.signSolanaTransaction(new Uint8Array([1]))
177
+ } catch (err) {
178
+ thrownError = err
179
+ }
180
+ })
181
+
182
+ expect(thrownError).toBe(walletError)
183
+ expect(captured.current!.error).toBe(walletError)
184
+ await unmount()
185
+ })
186
+
187
+ // (d) isLoading is true during the call and false after — success path
188
+ it('sets isLoading=true during call and false after success', async () => {
189
+ let resolveSign!: (v: Uint8Array) => void
190
+ const pendingSign = new Promise<Uint8Array>(res => {
191
+ resolveSign = res
192
+ })
193
+ const mockSign = vi.fn().mockReturnValue(pendingSign)
194
+ const ctxValue = makeContextValue(makeConnector(mockSign))
195
+ const { captured, render, unmount } = renderHookInContext(ctxValue)
196
+
197
+ await render()
198
+
199
+ // Start the async call but don't await it yet — capture loading mid-flight
200
+ let callPromise: Promise<Uint8Array>
201
+ await act(async () => {
202
+ callPromise = captured.current!.signSolanaTransaction(new Uint8Array([5]))
203
+ })
204
+
205
+ // At this point the hook has flipped isLoading=true (setIsLoading runs before the await)
206
+ expect(captured.current!.isLoading).toBe(true)
207
+
208
+ // Settle the promise
209
+ await act(async () => {
210
+ resolveSign(new Uint8Array([99]))
211
+ await callPromise!
212
+ })
213
+
214
+ expect(captured.current!.isLoading).toBe(false)
215
+ await unmount()
216
+ })
217
+
218
+ // (d) isLoading is false after failure path
219
+ it('sets isLoading=false after connector rejects', async () => {
220
+ let rejectSign!: (err: unknown) => void
221
+ const pendingSign = new Promise<Uint8Array>((_, rej) => {
222
+ rejectSign = rej
223
+ })
224
+ const mockSign = vi.fn().mockReturnValue(pendingSign)
225
+ const ctxValue = makeContextValue(makeConnector(mockSign))
226
+ const { captured, render, unmount } = renderHookInContext(ctxValue)
227
+
228
+ await render()
229
+
230
+ let callPromise: Promise<Uint8Array>
231
+ await act(async () => {
232
+ callPromise = captured.current!.signSolanaTransaction(new Uint8Array([6]))
233
+ })
234
+
235
+ expect(captured.current!.isLoading).toBe(true)
236
+
237
+ await act(async () => {
238
+ rejectSign({ code: 4001, message: 'cancelled' })
239
+ try {
240
+ await callPromise!
241
+ } catch {
242
+ // expected
243
+ }
244
+ })
245
+
246
+ expect(captured.current!.isLoading).toBe(false)
247
+ await unmount()
248
+ })
249
+
250
+ // (e) throws "No wallet connected" before touching the connector when no active address
251
+ it('throws "No wallet connected" when there is no active address', async () => {
252
+ const mockSign = vi.fn()
253
+ const ctxValue = makeContextValue(makeConnector(mockSign), {
254
+ activeAddress: undefined
255
+ })
256
+ const { captured, render, unmount } = renderHookInContext(ctxValue)
257
+
258
+ await render()
259
+
260
+ let thrownError: unknown
261
+ await act(async () => {
262
+ try {
263
+ await captured.current!.signSolanaTransaction(new Uint8Array([1]))
264
+ } catch (err) {
265
+ thrownError = err
266
+ }
267
+ })
268
+
269
+ expect((thrownError as Error).message).toBe('No wallet connected')
270
+ expect(mockSign).not.toHaveBeenCalled()
271
+ await unmount()
272
+ })
273
+
274
+ // (f) rejects empty input before touching the connector
275
+ it('throws "Transaction bytes are required" for empty bytes', async () => {
276
+ const mockSign = vi.fn()
277
+ const ctxValue = makeContextValue(makeConnector(mockSign))
278
+ const { captured, render, unmount } = renderHookInContext(ctxValue)
279
+
280
+ await render()
281
+
282
+ let thrownError: unknown
283
+ await act(async () => {
284
+ try {
285
+ await captured.current!.signSolanaTransaction(new Uint8Array(0))
286
+ } catch (err) {
287
+ thrownError = err
288
+ }
289
+ })
290
+
291
+ expect((thrownError as Error).message).toBe(
292
+ 'Transaction bytes are required'
293
+ )
294
+ expect(mockSign).not.toHaveBeenCalled()
295
+ await unmount()
296
+ })
297
+ })
@@ -0,0 +1,47 @@
1
+ import { useContext, useCallback, useState } from 'react'
2
+ import { ConnectionContext } from '../providers/ConnectionProvider'
3
+ import type {
4
+ UseSignSolanaTransactionReturn,
5
+ WalletError
6
+ } from '@meshconnect/uwc-types'
7
+
8
+ export function useSignSolanaTransaction(): UseSignSolanaTransactionReturn {
9
+ const context = useContext(ConnectionContext)
10
+
11
+ if (!context) {
12
+ throw new Error(
13
+ 'useSignSolanaTransaction must be used within a ConnectionProvider'
14
+ )
15
+ }
16
+
17
+ const { connector, session } = context
18
+ const [isLoading, setIsLoading] = useState(false)
19
+ const [error, setError] = useState<WalletError | undefined>()
20
+
21
+ const signSolanaTransaction = useCallback(
22
+ async (serializedTx: Uint8Array): Promise<Uint8Array> => {
23
+ if (!session.activeAddress) {
24
+ throw new Error('No wallet connected')
25
+ }
26
+
27
+ if (!serializedTx?.length) {
28
+ throw new Error('Transaction bytes are required')
29
+ }
30
+
31
+ setIsLoading(true)
32
+ setError(undefined)
33
+ try {
34
+ return await connector.signSolanaTransaction(serializedTx)
35
+ } catch (err) {
36
+ const walletError = err as WalletError
37
+ setError(walletError)
38
+ throw err
39
+ } finally {
40
+ setIsLoading(false)
41
+ }
42
+ },
43
+ [connector, session.activeAddress]
44
+ )
45
+
46
+ return { signSolanaTransaction, isLoading, error }
47
+ }