@meshconnect/uwc-react 0.7.1 → 0.8.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.
- 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/useSignTypedData.d.ts +26 -0
- package/dist/hooks/useSignTypedData.d.ts.map +1 -0
- package/dist/hooks/useSignTypedData.js +62 -0
- package/dist/hooks/useSignTypedData.js.map +1 -0
- package/package.json +3 -3
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useSignTypedData.test.tsx +252 -0
- package/src/hooks/useSignTypedData.ts +73 -0
package/dist/hooks/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './useWallets';
|
|
|
6
6
|
export * from './useWallet';
|
|
7
7
|
export * from './useNetworks';
|
|
8
8
|
export * from './useSignMessage';
|
|
9
|
+
export * from './useSignTypedData';
|
|
9
10
|
export * from './useTransaction';
|
|
10
11
|
export * from './useWalletCapabilities';
|
|
11
12
|
export * from './useDetectedWallets';
|
|
@@ -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;AACpC,cAAc,4BAA4B,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,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,4BAA4B,CAAA"}
|
package/dist/hooks/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export * from './useWallets';
|
|
|
6
6
|
export * from './useWallet';
|
|
7
7
|
export * from './useNetworks';
|
|
8
8
|
export * from './useSignMessage';
|
|
9
|
+
export * from './useSignTypedData';
|
|
9
10
|
export * from './useTransaction';
|
|
10
11
|
export * from './useWalletCapabilities';
|
|
11
12
|
export * from './useDetectedWallets';
|
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;AACpC,cAAc,4BAA4B,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,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,4BAA4B,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { UseSignTypedDataReturn } from '@meshconnect/uwc-types';
|
|
2
|
+
/**
|
|
3
|
+
* Hook for signing EIP-712 typed structured data (eth_signTypedData_v4).
|
|
4
|
+
*
|
|
5
|
+
* Used by ERC-3009 (Transfer With Authorization) and EIP-2612 (Permit) relay
|
|
6
|
+
* flows — the user signs an off-chain authorization and Mesh's backend submits
|
|
7
|
+
* the transaction and pays gas. EVM-only (eip155); throws for TON connections.
|
|
8
|
+
*
|
|
9
|
+
* @returns Object containing signTypedData function, loading state, last signature, and error
|
|
10
|
+
* @throws Error if used outside of ConnectionProvider
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const { signTypedData, isLoading, signature } = useSignTypedData()
|
|
14
|
+
*
|
|
15
|
+
* const handleSign = async () => {
|
|
16
|
+
* try {
|
|
17
|
+
* const sig = await signTypedData(typedData)
|
|
18
|
+
* // send sig to the relay endpoint
|
|
19
|
+
* } catch (error) {
|
|
20
|
+
* console.error('Failed to sign:', error)
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function useSignTypedData(): UseSignTypedDataReturn;
|
|
26
|
+
//# sourceMappingURL=useSignTypedData.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSignTypedData.d.ts","sourceRoot":"","sources":["../../src/hooks/useSignTypedData.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,wBAAwB,CAAA;AAE/B;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,gBAAgB,IAAI,sBAAsB,CAyCzD"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useContext, useCallback, useState } from 'react';
|
|
2
|
+
import { ConnectionContext } from '../providers/ConnectionProvider';
|
|
3
|
+
/**
|
|
4
|
+
* Hook for signing EIP-712 typed structured data (eth_signTypedData_v4).
|
|
5
|
+
*
|
|
6
|
+
* Used by ERC-3009 (Transfer With Authorization) and EIP-2612 (Permit) relay
|
|
7
|
+
* flows — the user signs an off-chain authorization and Mesh's backend submits
|
|
8
|
+
* the transaction and pays gas. EVM-only (eip155); throws for TON connections.
|
|
9
|
+
*
|
|
10
|
+
* @returns Object containing signTypedData function, loading state, last signature, and error
|
|
11
|
+
* @throws Error if used outside of ConnectionProvider
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const { signTypedData, isLoading, signature } = useSignTypedData()
|
|
15
|
+
*
|
|
16
|
+
* const handleSign = async () => {
|
|
17
|
+
* try {
|
|
18
|
+
* const sig = await signTypedData(typedData)
|
|
19
|
+
* // send sig to the relay endpoint
|
|
20
|
+
* } catch (error) {
|
|
21
|
+
* console.error('Failed to sign:', error)
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function useSignTypedData() {
|
|
27
|
+
const context = useContext(ConnectionContext);
|
|
28
|
+
if (!context) {
|
|
29
|
+
throw new Error('useSignTypedData must be used within a ConnectionProvider');
|
|
30
|
+
}
|
|
31
|
+
const { connector, session } = context;
|
|
32
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
33
|
+
const [signature, setSignature] = useState();
|
|
34
|
+
const [error, setError] = useState();
|
|
35
|
+
const signTypedData = useCallback(async (typedData) => {
|
|
36
|
+
if (!session.activeAddress) {
|
|
37
|
+
throw new Error('No wallet connected');
|
|
38
|
+
}
|
|
39
|
+
setIsLoading(true);
|
|
40
|
+
setError(undefined);
|
|
41
|
+
try {
|
|
42
|
+
const sig = await connector.signTypedData(typedData);
|
|
43
|
+
setSignature(sig);
|
|
44
|
+
return sig;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const walletError = err;
|
|
48
|
+
setError(walletError);
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
setIsLoading(false);
|
|
53
|
+
}
|
|
54
|
+
}, [connector, session.activeAddress]);
|
|
55
|
+
return {
|
|
56
|
+
signTypedData,
|
|
57
|
+
isLoading,
|
|
58
|
+
signature,
|
|
59
|
+
error
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=useSignTypedData.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSignTypedData.js","sourceRoot":"","sources":["../../src/hooks/useSignTypedData.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AAOnE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,OAAO,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAA;IAE7C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAA;IAC9E,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,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,EAAsB,CAAA;IAChE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,EAA2B,CAAA;IAE7D,MAAM,aAAa,GAAG,WAAW,CAC/B,KAAK,EAAE,SAA0B,EAAmB,EAAE;QACpD,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAA;QACxC,CAAC;QAED,YAAY,CAAC,IAAI,CAAC,CAAA;QAClB,QAAQ,CAAC,SAAS,CAAC,CAAA;QACnB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,SAAS,CAAC,CAAA;YACpD,YAAY,CAAC,GAAG,CAAC,CAAA;YACjB,OAAO,GAAG,CAAA;QACZ,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;QACL,aAAa;QACb,SAAS;QACT,SAAS;QACT,KAAK;KACN,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meshconnect/uwc-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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.9.0",
|
|
20
|
+
"@meshconnect/uwc-types": "0.15.0"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"react": "^18.0.0",
|
package/src/hooks/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './useWallets'
|
|
|
6
6
|
export * from './useWallet'
|
|
7
7
|
export * from './useNetworks'
|
|
8
8
|
export * from './useSignMessage'
|
|
9
|
+
export * from './useSignTypedData'
|
|
9
10
|
export * from './useTransaction'
|
|
10
11
|
export * from './useWalletCapabilities'
|
|
11
12
|
export * from './useDetectedWallets'
|
|
@@ -0,0 +1,252 @@
|
|
|
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 { useSignTypedData } from './useSignTypedData'
|
|
6
|
+
import type {
|
|
7
|
+
EIP712TypedData,
|
|
8
|
+
UseSignTypedDataReturn
|
|
9
|
+
} from '@meshconnect/uwc-types'
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Fixtures + helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const USDC_TYPED_DATA: EIP712TypedData = {
|
|
16
|
+
domain: {
|
|
17
|
+
name: 'USD Coin',
|
|
18
|
+
version: '2',
|
|
19
|
+
chainId: 1,
|
|
20
|
+
verifyingContract: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
|
|
21
|
+
},
|
|
22
|
+
types: {
|
|
23
|
+
TransferWithAuthorization: [
|
|
24
|
+
{ name: 'from', type: 'address' },
|
|
25
|
+
{ name: 'to', type: 'address' },
|
|
26
|
+
{ name: 'value', type: 'uint256' },
|
|
27
|
+
{ name: 'validAfter', type: 'uint256' },
|
|
28
|
+
{ name: 'validBefore', type: 'uint256' },
|
|
29
|
+
{ name: 'nonce', type: 'bytes32' }
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
primaryType: 'TransferWithAuthorization',
|
|
33
|
+
message: {
|
|
34
|
+
from: '0xabc',
|
|
35
|
+
to: '0xdef',
|
|
36
|
+
value: '1000000',
|
|
37
|
+
validAfter: 0,
|
|
38
|
+
validBefore: 9999999999,
|
|
39
|
+
nonce: '0xdeadbeef'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Minimal stub that satisfies ConnectionContextValue's connector shape. */
|
|
44
|
+
function makeConnector(
|
|
45
|
+
signTypedData: (typedData: EIP712TypedData) => Promise<string>
|
|
46
|
+
) {
|
|
47
|
+
return {
|
|
48
|
+
signTypedData,
|
|
49
|
+
getSession: () => ({ isConnected: false }),
|
|
50
|
+
isReady: () => true,
|
|
51
|
+
subscribe: () => () => {},
|
|
52
|
+
getWallets: () => [],
|
|
53
|
+
getNetworks: () => []
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Minimal context value that wraps a connector stub. Pass `{ activeAddress }`
|
|
59
|
+
* as an object (not a bare arg) so an explicit `undefined` survives.
|
|
60
|
+
*/
|
|
61
|
+
function makeContextValue(
|
|
62
|
+
connector: ReturnType<typeof makeConnector>,
|
|
63
|
+
{ activeAddress }: { activeAddress: string | undefined } = {
|
|
64
|
+
activeAddress: '0xUserAddress'
|
|
65
|
+
}
|
|
66
|
+
) {
|
|
67
|
+
return {
|
|
68
|
+
connector,
|
|
69
|
+
session: {
|
|
70
|
+
isConnected: activeAddress !== undefined,
|
|
71
|
+
walletId: undefined,
|
|
72
|
+
networkId: undefined,
|
|
73
|
+
activeAddress
|
|
74
|
+
},
|
|
75
|
+
wallets: [],
|
|
76
|
+
networks: [],
|
|
77
|
+
isReady: true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Renders `useSignTypedData` inside a component that exposes its return value
|
|
83
|
+
* via a captured ref. Mirrors useSignSolanaTransaction.test.tsx: createRoot +
|
|
84
|
+
* act, no @testing-library/react (not installed in this package).
|
|
85
|
+
*/
|
|
86
|
+
function renderHookInContext(
|
|
87
|
+
contextValue: ReturnType<typeof makeContextValue>
|
|
88
|
+
) {
|
|
89
|
+
const captured: { current: UseSignTypedDataReturn | null } = { current: null }
|
|
90
|
+
|
|
91
|
+
function Capture() {
|
|
92
|
+
captured.current = useSignTypedData()
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const container = document.createElement('div')
|
|
97
|
+
document.body.appendChild(container)
|
|
98
|
+
const root = createRoot(container)
|
|
99
|
+
|
|
100
|
+
async function render() {
|
|
101
|
+
await act(async () => {
|
|
102
|
+
root.render(
|
|
103
|
+
<ConnectionContext.Provider value={contextValue as never}>
|
|
104
|
+
<Capture />
|
|
105
|
+
</ConnectionContext.Provider>
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function unmount() {
|
|
111
|
+
await act(async () => {
|
|
112
|
+
root.unmount()
|
|
113
|
+
})
|
|
114
|
+
document.body.removeChild(container)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { captured, render, unmount }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Tests
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
describe('useSignTypedData', () => {
|
|
125
|
+
let container: HTMLDivElement
|
|
126
|
+
let root: ReturnType<typeof createRoot>
|
|
127
|
+
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
container = document.createElement('div')
|
|
130
|
+
document.body.appendChild(container)
|
|
131
|
+
root = createRoot(container)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
afterEach(async () => {
|
|
135
|
+
await act(async () => {
|
|
136
|
+
root.unmount()
|
|
137
|
+
})
|
|
138
|
+
if (document.body.contains(container)) {
|
|
139
|
+
document.body.removeChild(container)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('throws when rendered outside ConnectionProvider', () => {
|
|
144
|
+
function BareHook() {
|
|
145
|
+
useSignTypedData()
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
expect(() => {
|
|
150
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
151
|
+
try {
|
|
152
|
+
act(() => {
|
|
153
|
+
root.render(<BareHook />)
|
|
154
|
+
})
|
|
155
|
+
} finally {
|
|
156
|
+
errorSpy.mockRestore()
|
|
157
|
+
}
|
|
158
|
+
}).toThrowError('useSignTypedData must be used within a ConnectionProvider')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('returns the signature from connector.signTypedData on success', async () => {
|
|
162
|
+
const mockSign = vi.fn().mockResolvedValue('0xtypedsig')
|
|
163
|
+
const ctxValue = makeContextValue(makeConnector(mockSign))
|
|
164
|
+
const { captured, render, unmount } = renderHookInContext(ctxValue)
|
|
165
|
+
|
|
166
|
+
await render()
|
|
167
|
+
expect(captured.current).not.toBeNull()
|
|
168
|
+
|
|
169
|
+
let result: string | undefined
|
|
170
|
+
await act(async () => {
|
|
171
|
+
result = await captured.current!.signTypedData(USDC_TYPED_DATA)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
expect(mockSign).toHaveBeenCalledOnce()
|
|
175
|
+
expect(mockSign).toHaveBeenCalledWith(USDC_TYPED_DATA)
|
|
176
|
+
expect(result).toBe('0xtypedsig')
|
|
177
|
+
expect(captured.current!.signature).toBe('0xtypedsig')
|
|
178
|
+
await unmount()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('sets error state and re-throws when connector rejects', async () => {
|
|
182
|
+
const walletError = { code: 4001, message: 'User rejected' }
|
|
183
|
+
const mockSign = vi.fn().mockRejectedValue(walletError)
|
|
184
|
+
const ctxValue = makeContextValue(makeConnector(mockSign))
|
|
185
|
+
const { captured, render, unmount } = renderHookInContext(ctxValue)
|
|
186
|
+
|
|
187
|
+
await render()
|
|
188
|
+
|
|
189
|
+
let thrownError: unknown
|
|
190
|
+
await act(async () => {
|
|
191
|
+
try {
|
|
192
|
+
await captured.current!.signTypedData(USDC_TYPED_DATA)
|
|
193
|
+
} catch (err) {
|
|
194
|
+
thrownError = err
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
expect(thrownError).toBe(walletError)
|
|
199
|
+
expect(captured.current!.error).toBe(walletError)
|
|
200
|
+
await unmount()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('sets isLoading=true during call and false after success', async () => {
|
|
204
|
+
let resolveSign!: (v: string) => void
|
|
205
|
+
const pendingSign = new Promise<string>(res => {
|
|
206
|
+
resolveSign = res
|
|
207
|
+
})
|
|
208
|
+
const mockSign = vi.fn().mockReturnValue(pendingSign)
|
|
209
|
+
const ctxValue = makeContextValue(makeConnector(mockSign))
|
|
210
|
+
const { captured, render, unmount } = renderHookInContext(ctxValue)
|
|
211
|
+
|
|
212
|
+
await render()
|
|
213
|
+
|
|
214
|
+
let callPromise: Promise<string>
|
|
215
|
+
await act(async () => {
|
|
216
|
+
callPromise = captured.current!.signTypedData(USDC_TYPED_DATA)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
expect(captured.current!.isLoading).toBe(true)
|
|
220
|
+
|
|
221
|
+
await act(async () => {
|
|
222
|
+
resolveSign('0xsig')
|
|
223
|
+
await callPromise!
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
expect(captured.current!.isLoading).toBe(false)
|
|
227
|
+
await unmount()
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('throws "No wallet connected" when there is no active address', async () => {
|
|
231
|
+
const mockSign = vi.fn()
|
|
232
|
+
const ctxValue = makeContextValue(makeConnector(mockSign), {
|
|
233
|
+
activeAddress: undefined
|
|
234
|
+
})
|
|
235
|
+
const { captured, render, unmount } = renderHookInContext(ctxValue)
|
|
236
|
+
|
|
237
|
+
await render()
|
|
238
|
+
|
|
239
|
+
let thrownError: unknown
|
|
240
|
+
await act(async () => {
|
|
241
|
+
try {
|
|
242
|
+
await captured.current!.signTypedData(USDC_TYPED_DATA)
|
|
243
|
+
} catch (err) {
|
|
244
|
+
thrownError = err
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
expect((thrownError as Error).message).toBe('No wallet connected')
|
|
249
|
+
expect(mockSign).not.toHaveBeenCalled()
|
|
250
|
+
await unmount()
|
|
251
|
+
})
|
|
252
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useContext, useCallback, useState } from 'react'
|
|
2
|
+
import { ConnectionContext } from '../providers/ConnectionProvider'
|
|
3
|
+
import type {
|
|
4
|
+
EIP712TypedData,
|
|
5
|
+
UseSignTypedDataReturn,
|
|
6
|
+
WalletError
|
|
7
|
+
} from '@meshconnect/uwc-types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hook for signing EIP-712 typed structured data (eth_signTypedData_v4).
|
|
11
|
+
*
|
|
12
|
+
* Used by ERC-3009 (Transfer With Authorization) and EIP-2612 (Permit) relay
|
|
13
|
+
* flows — the user signs an off-chain authorization and Mesh's backend submits
|
|
14
|
+
* the transaction and pays gas. EVM-only (eip155); throws for TON connections.
|
|
15
|
+
*
|
|
16
|
+
* @returns Object containing signTypedData function, loading state, last signature, and error
|
|
17
|
+
* @throws Error if used outside of ConnectionProvider
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const { signTypedData, isLoading, signature } = useSignTypedData()
|
|
21
|
+
*
|
|
22
|
+
* const handleSign = async () => {
|
|
23
|
+
* try {
|
|
24
|
+
* const sig = await signTypedData(typedData)
|
|
25
|
+
* // send sig to the relay endpoint
|
|
26
|
+
* } catch (error) {
|
|
27
|
+
* console.error('Failed to sign:', error)
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function useSignTypedData(): UseSignTypedDataReturn {
|
|
33
|
+
const context = useContext(ConnectionContext)
|
|
34
|
+
|
|
35
|
+
if (!context) {
|
|
36
|
+
throw new Error('useSignTypedData must be used within a ConnectionProvider')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { connector, session } = context
|
|
40
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
41
|
+
const [signature, setSignature] = useState<string | undefined>()
|
|
42
|
+
const [error, setError] = useState<WalletError | undefined>()
|
|
43
|
+
|
|
44
|
+
const signTypedData = useCallback(
|
|
45
|
+
async (typedData: EIP712TypedData): Promise<string> => {
|
|
46
|
+
if (!session.activeAddress) {
|
|
47
|
+
throw new Error('No wallet connected')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setIsLoading(true)
|
|
51
|
+
setError(undefined)
|
|
52
|
+
try {
|
|
53
|
+
const sig = await connector.signTypedData(typedData)
|
|
54
|
+
setSignature(sig)
|
|
55
|
+
return sig
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const walletError = err as WalletError
|
|
58
|
+
setError(walletError)
|
|
59
|
+
throw err
|
|
60
|
+
} finally {
|
|
61
|
+
setIsLoading(false)
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
[connector, session.activeAddress]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
signTypedData,
|
|
69
|
+
isLoading,
|
|
70
|
+
signature,
|
|
71
|
+
error
|
|
72
|
+
}
|
|
73
|
+
}
|