Skip to content

Commit ce5845e

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

File tree

5 files changed

+206
-0
lines changed

5 files changed

+206
-0
lines changed

.changeset/use-sign-transaction.md

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 `useSignTransaction` hook for signing transactions without broadcasting via `stx_signTransaction`. Enables sponsored transaction flows.

examples/vite-react/src/app.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
useWallets,
1010
useWriteContract,
1111
useTransferSTX,
12+
useSignTransaction,
1213
createContractConfig,
1314
} from '@satoshai/kit';
1415

@@ -102,6 +103,7 @@ const Wallet = ({ useModal }: { useModal: boolean }) => {
102103
) : null}
103104
<TransferSTXDemo />
104105
<WriteContractDemo address={address} />
106+
<SignTransactionDemo />
105107
<button onClick={() => disconnect()}>Disconnect</button>
106108
</div>
107109
);
@@ -213,6 +215,63 @@ const TransferSTXDemo = () => {
213215
);
214216
};
215217

218+
// Demonstrates useSignTransaction hook
219+
const SignTransactionDemo = () => {
220+
const [transaction, setTransaction] = useState('');
221+
const [broadcast, setBroadcast] = useState(false);
222+
const { signTransaction, isPending, isSuccess, isError, data, error, reset } = useSignTransaction();
223+
224+
const handleSign = () => {
225+
if (!transaction) return;
226+
signTransaction(
227+
{ transaction, broadcast },
228+
{
229+
onSuccess: (result) => console.log('Transaction signed:', result),
230+
onError: (err) => console.error('Transaction signing failed:', err),
231+
}
232+
);
233+
};
234+
235+
return (
236+
<div style={{ marginTop: '1rem', marginBottom: '1rem' }}>
237+
<h3>Sign Transaction</h3>
238+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', maxWidth: '400px' }}>
239+
<input
240+
type="text"
241+
placeholder="Serialized transaction hex"
242+
value={transaction}
243+
onChange={(e) => setTransaction(e.target.value)}
244+
disabled={isPending}
245+
/>
246+
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
247+
<input
248+
type="checkbox"
249+
checked={broadcast}
250+
onChange={(e) => setBroadcast(e.target.checked)}
251+
disabled={isPending}
252+
/>
253+
Broadcast
254+
</label>
255+
<button onClick={handleSign} disabled={isPending || !transaction}>
256+
{isPending ? 'Signing...' : 'Sign Transaction'}
257+
</button>
258+
</div>
259+
{isSuccess && data && (
260+
<div style={{ color: 'green' }}>
261+
<p>Signed TX: {data.transaction}</p>
262+
{data.txid && <p>TXID: {data.txid}</p>}
263+
<button onClick={reset}>Clear</button>
264+
</div>
265+
)}
266+
{isError && (
267+
<p style={{ color: 'red' }}>
268+
Error: {error?.message} <button onClick={reset}>Clear</button>
269+
</p>
270+
)}
271+
</div>
272+
);
273+
};
274+
216275
// Demonstrates typed useWriteContract with ABI inference
217276
const WriteContractDemo = ({ address }: { address: string }) => {
218277
const { writeContract, isPending, isSuccess, isError, data, error } = useWriteContract();

packages/kit/README.md

Lines changed: 23 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+
- **`useSignTransaction`** — Sign serialized transactions (sponsored tx flows)
1213
- **`useWriteContract`** — Call smart contracts with post-conditions
1314
- **`useTransferSTX`** — Native STX transfers
1415
- **`useBnsName`** — Resolve BNS v2 names
@@ -224,6 +225,28 @@ writeContract({
224225
});
225226
```
226227

228+
### `useSignTransaction()`
229+
230+
Sign a serialized transaction without automatically broadcasting it. Useful for sponsored transaction flows where a separate service pays the fee.
231+
232+
> **Note:** OKX wallet does not support raw transaction signing and will throw an error.
233+
234+
```ts
235+
const { signTransaction, signTransactionAsync, data, error, isPending } = useSignTransaction();
236+
237+
// Callback style
238+
signTransaction({ transaction: '0x0100...', broadcast: false }, {
239+
onSuccess: ({ transaction, txid }) => {},
240+
onError: (error) => {},
241+
});
242+
243+
// Async style
244+
const { transaction, txid } = await signTransactionAsync({
245+
transaction: '0x0100...',
246+
broadcast: false,
247+
});
248+
```
249+
227250
### `useBnsName()`
228251

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

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+
useSignTransaction,
16+
type SignTransactionVariables,
17+
type SignTransactionData,
18+
type SignTransactionOptions,
19+
} from './hooks/use-sign-transaction';
1420
export {
1521
useTransferSTX,
1622
type TransferSTXVariables,

0 commit comments

Comments
 (0)