@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 +32 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useSignSolanaTransaction.d.ts +3 -0
- package/dist/hooks/useSignSolanaTransaction.d.ts.map +1 -0
- package/dist/hooks/useSignSolanaTransaction.js +34 -0
- package/dist/hooks/useSignSolanaTransaction.js.map +1 -0
- package/package.json +3 -3
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useSignSolanaTransaction.test.tsx +297 -0
- package/src/hooks/useSignSolanaTransaction.ts +47 -0
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.
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -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"}
|
package/dist/hooks/index.js
CHANGED
package/dist/hooks/index.js.map
CHANGED
|
@@ -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 @@
|
|
|
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.
|
|
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.
|
|
20
|
-
"@meshconnect/uwc-types": "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",
|
package/src/hooks/index.ts
CHANGED
|
@@ -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
|
+
}
|