Skip to content

Commit bcb9e65

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

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 } from '@stacks/transactions';
2+
import { PostConditionMode, tupleCV, stringAsciiCV, uintCV } from '@stacks/transactions';
33
import {
44
StacksWalletProvider,
55
useAddress,
@@ -10,6 +10,7 @@ import {
1010
useWriteContract,
1111
useTransferSTX,
1212
createContractConfig,
13+
useSignStructuredMessage,
1314
} from '@satoshai/kit';
1415

1516
const wcProjectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID as string | undefined;
@@ -101,6 +102,7 @@ const Wallet = ({ useModal }: { useModal: boolean }) => {
101102
</p>
102103
) : null}
103104
<TransferSTXDemo />
105+
<SignStructuredMessageDemo />
104106
<WriteContractDemo address={address} />
105107
<button onClick={() => disconnect()}>Disconnect</button>
106108
</div>
@@ -213,6 +215,51 @@ const TransferSTXDemo = () => {
213215
);
214216
};
215217

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

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
- **`useWriteContract`** — Call smart contracts with post-conditions
1314
- **`useTransferSTX`** — Native STX transfers
1415
- **`useBnsName`** — Resolve BNS v2 names
@@ -180,6 +181,40 @@ signMessage({ message: 'Hello Stacks' }, {
180181
const { publicKey, signature } = await signMessageAsync({ message: 'Hello Stacks' });
181182
```
182183

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

185220
```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
useTransferSTX,
1622
type TransferSTXVariables,

0 commit comments

Comments
 (0)