Skip to content

Commit 4daa747

Browse files
satoshai-devclaude
andcommitted
feat: add useSignStructuredMessage hook (#41)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0e6241e commit 4daa747

File tree

5 files changed

+215
-1
lines changed

5 files changed

+215
-1
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@satoshai/kit": minor
3+
---
4+
5+
Add `useSignStructuredMessage` hook for SIP-018 structured data signing via `stx_signStructuredMessage`.

examples/vite-react/src/app.tsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState } from 'react';
2-
import { PostConditionMode, makeUnsignedSTXTokenTransfer, AnchorMode } from '@stacks/transactions';
2+
import { PostConditionMode, makeUnsignedSTXTokenTransfer, AnchorMode, tupleCV, stringAsciiCV, uintCV } from '@stacks/transactions';
33
import {
44
StacksWalletProvider,
55
useAddress,
@@ -11,6 +11,7 @@ import {
1111
useTransferSTX,
1212
useSignTransaction,
1313
createContractConfig,
14+
useSignStructuredMessage,
1415
} from '@satoshai/kit';
1516

1617
const wcProjectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID as string | undefined;
@@ -102,6 +103,7 @@ const Wallet = ({ useModal }: { useModal: boolean }) => {
102103
</p>
103104
) : null}
104105
<TransferSTXDemo />
106+
<SignStructuredMessageDemo />
105107
<WriteContractDemo address={address} />
106108
<SignTransactionDemo address={address} />
107109
<button onClick={() => disconnect()}>Disconnect</button>
@@ -215,6 +217,51 @@ const TransferSTXDemo = () => {
215217
);
216218
};
217219

220+
// Demonstrates useSignStructuredMessage hook (SIP-018)
221+
const SignStructuredMessageDemo = () => {
222+
const { signStructuredMessage, isPending, isSuccess, isError, data, error, reset } =
223+
useSignStructuredMessage();
224+
225+
const handleSign = () => {
226+
signStructuredMessage(
227+
{
228+
domain: tupleCV({
229+
name: stringAsciiCV('ExampleApp'),
230+
version: stringAsciiCV('1.0'),
231+
'chain-id': uintCV(1),
232+
}),
233+
message: tupleCV({
234+
action: stringAsciiCV('authorize'),
235+
amount: uintCV(1000),
236+
}),
237+
},
238+
{
239+
onSuccess: (result) => console.log('Structured message signed:', result.signature),
240+
onError: (err) => console.error('Structured message signing failed:', err),
241+
}
242+
);
243+
};
244+
245+
return (
246+
<div style={{ marginTop: '1rem', marginBottom: '1rem' }}>
247+
<h3>Sign Structured Message (SIP-018)</h3>
248+
<button onClick={handleSign} disabled={isPending}>
249+
{isPending ? 'Signing...' : 'Sign Structured Message'}
250+
</button>
251+
{isSuccess && (
252+
<p style={{ color: 'green' }}>
253+
Signature: {data?.signature.slice(0, 20)}... <button onClick={reset}>Clear</button>
254+
</p>
255+
)}
256+
{isError && (
257+
<p style={{ color: 'red' }}>
258+
Error: {error?.message} <button onClick={reset}>Clear</button>
259+
</p>
260+
)}
261+
</div>
262+
);
263+
};
264+
218265
// Demonstrates useSignTransaction hook
219266
const SignTransactionDemo = ({ address }: { address: string }) => {
220267
const [broadcast, setBroadcast] = useState(false);

packages/kit/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Typesafe Stacks wallet & contract interaction library for React. Wagmi-inspired
99
- **`useWallets`** — Configured wallets with availability status
1010
- **`useAddress`** — Access connected wallet address and status
1111
- **`useSignMessage`** — Sign arbitrary messages
12+
- **`useSignStructuredMessage`** — Sign SIP-018 structured data
1213
- **`useSignTransaction`** — Sign serialized transactions (sponsored tx flows)
1314
- **`useWriteContract`** — Call smart contracts with post-conditions
1415
- **`useTransferSTX`** — Native STX transfers
@@ -181,6 +182,40 @@ signMessage({ message: 'Hello Stacks' }, {
181182
const { publicKey, signature } = await signMessageAsync({ message: 'Hello Stacks' });
182183
```
183184

185+
### `useSignStructuredMessage()`
186+
187+
Sign SIP-018 structured data for typed, verifiable off-chain messages.
188+
189+
> **Note:** OKX wallet does not support structured message signing and will throw an error.
190+
191+
```ts
192+
import { tupleCV, stringAsciiCV, uintCV } from '@stacks/transactions';
193+
194+
const { signStructuredMessage, signStructuredMessageAsync, data, error, isPending } = useSignStructuredMessage();
195+
196+
// Callback style
197+
signStructuredMessage({
198+
domain: tupleCV({
199+
name: stringAsciiCV('MyApp'),
200+
version: stringAsciiCV('1.0'),
201+
'chain-id': uintCV(1),
202+
}),
203+
message: tupleCV({
204+
action: stringAsciiCV('authorize'),
205+
amount: uintCV(1000),
206+
}),
207+
}, {
208+
onSuccess: ({ publicKey, signature }) => {},
209+
onError: (error) => {},
210+
});
211+
212+
// Async style
213+
const { publicKey, signature } = await signStructuredMessageAsync({
214+
domain: tupleCV({ ... }),
215+
message: tupleCV({ ... }),
216+
});
217+
```
218+
184219
### `useTransferSTX()`
185220

186221
```ts
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
'use client';
2+
3+
import { request } from '@stacks/connect';
4+
import type { ClarityValue, TupleCV } from '@stacks/transactions';
5+
import { useCallback, useMemo, useState } from 'react';
6+
7+
import type { MutationStatus } from '../provider/stacks-wallet-provider.types';
8+
import { useAddress } from './use-address';
9+
10+
export interface SignStructuredMessageVariables {
11+
message: ClarityValue;
12+
domain: TupleCV;
13+
}
14+
15+
export interface SignStructuredMessageData {
16+
publicKey: string;
17+
signature: string;
18+
}
19+
20+
export interface SignStructuredMessageOptions {
21+
onSuccess?: (data: SignStructuredMessageData) => void;
22+
onError?: (error: Error) => void;
23+
onSettled?: (
24+
data: SignStructuredMessageData | undefined,
25+
error: Error | null
26+
) => void;
27+
}
28+
29+
export const useSignStructuredMessage = () => {
30+
const { isConnected, provider } = useAddress();
31+
const [data, setData] = useState<SignStructuredMessageData | undefined>(
32+
undefined
33+
);
34+
const [error, setError] = useState<Error | null>(null);
35+
const [status, setStatus] = useState<MutationStatus>('idle');
36+
37+
const signStructuredMessageAsync = useCallback(
38+
async (
39+
variables: SignStructuredMessageVariables
40+
): Promise<SignStructuredMessageData> => {
41+
if (!isConnected) {
42+
throw new Error('Wallet is not connected');
43+
}
44+
45+
if (provider === 'okx') {
46+
throw new Error(
47+
'Structured message signing is not supported by OKX wallet'
48+
);
49+
}
50+
51+
setStatus('pending');
52+
setError(null);
53+
setData(undefined);
54+
55+
try {
56+
const result = await request('stx_signStructuredMessage', {
57+
message: variables.message,
58+
domain: variables.domain,
59+
});
60+
61+
setData(result);
62+
setStatus('success');
63+
return result;
64+
} catch (err) {
65+
const error =
66+
err instanceof Error ? err : new Error(String(err));
67+
setError(error);
68+
setStatus('error');
69+
throw error;
70+
}
71+
},
72+
[isConnected, provider]
73+
);
74+
75+
const signStructuredMessage = useCallback(
76+
(
77+
variables: SignStructuredMessageVariables,
78+
options?: SignStructuredMessageOptions
79+
) => {
80+
signStructuredMessageAsync(variables)
81+
.then((data) => {
82+
options?.onSuccess?.(data);
83+
options?.onSettled?.(data, null);
84+
})
85+
.catch((error) => {
86+
options?.onError?.(error);
87+
options?.onSettled?.(undefined, error);
88+
});
89+
},
90+
[signStructuredMessageAsync]
91+
);
92+
93+
const reset = useCallback(() => {
94+
setData(undefined);
95+
setError(null);
96+
setStatus('idle');
97+
}, []);
98+
99+
return useMemo(
100+
() => ({
101+
signStructuredMessage,
102+
signStructuredMessageAsync,
103+
reset,
104+
data,
105+
error,
106+
isError: status === 'error',
107+
isIdle: status === 'idle',
108+
isPending: status === 'pending',
109+
isSuccess: status === 'success',
110+
status,
111+
}),
112+
[
113+
signStructuredMessage,
114+
signStructuredMessageAsync,
115+
reset,
116+
data,
117+
error,
118+
status,
119+
]
120+
);
121+
};

packages/kit/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ export {
1111
type SignMessageData,
1212
type SignMessageOptions,
1313
} from './hooks/use-sign-message';
14+
export {
15+
useSignStructuredMessage,
16+
type SignStructuredMessageVariables,
17+
type SignStructuredMessageData,
18+
type SignStructuredMessageOptions,
19+
} from './hooks/use-sign-structured-message';
1420
export {
1521
useSignTransaction,
1622
type SignTransactionVariables,

0 commit comments

Comments
 (0)