@satoshai/kit 0.5.0 → 0.7.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/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @satoshai/kit
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@satoshai/kit)](https://www.npmjs.com/package/@satoshai/kit)
4
+ [![CI](https://github.com/satoshai-dev/kit/actions/workflows/ci.yml/badge.svg)](https://github.com/satoshai-dev/kit/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
3
7
  Typesafe Stacks wallet & contract interaction library for React. Wagmi-inspired hook API for connecting wallets, signing messages, and calling contracts on the Stacks blockchain.
4
8
 
5
9
  ## Features
@@ -9,6 +13,8 @@ Typesafe Stacks wallet & contract interaction library for React. Wagmi-inspired
9
13
  - **`useWallets`** — Configured wallets with availability status
10
14
  - **`useAddress`** — Access connected wallet address and status
11
15
  - **`useSignMessage`** — Sign arbitrary messages
16
+ - **`useSignStructuredMessage`** — Sign SIP-018 structured data
17
+ - **`useSignTransaction`** — Sign serialized transactions (sponsored tx flows)
12
18
  - **`useWriteContract`** — Call smart contracts with post-conditions
13
19
  - **`useTransferSTX`** — Native STX transfers
14
20
  - **`useBnsName`** — Resolve BNS v2 names
@@ -180,6 +186,40 @@ signMessage({ message: 'Hello Stacks' }, {
180
186
  const { publicKey, signature } = await signMessageAsync({ message: 'Hello Stacks' });
181
187
  ```
182
188
 
189
+ ### `useSignStructuredMessage()`
190
+
191
+ Sign SIP-018 structured data for typed, verifiable off-chain messages.
192
+
193
+ > **Note:** OKX wallet does not support structured message signing and will throw an error.
194
+
195
+ ```ts
196
+ import { tupleCV, stringAsciiCV, uintCV } from '@stacks/transactions';
197
+
198
+ const { signStructuredMessage, signStructuredMessageAsync, data, error, isPending } = useSignStructuredMessage();
199
+
200
+ // Callback style
201
+ signStructuredMessage({
202
+ domain: tupleCV({
203
+ name: stringAsciiCV('MyApp'),
204
+ version: stringAsciiCV('1.0'),
205
+ 'chain-id': uintCV(1),
206
+ }),
207
+ message: tupleCV({
208
+ action: stringAsciiCV('authorize'),
209
+ amount: uintCV(1000),
210
+ }),
211
+ }, {
212
+ onSuccess: ({ publicKey, signature }) => {},
213
+ onError: (error) => {},
214
+ });
215
+
216
+ // Async style
217
+ const { publicKey, signature } = await signStructuredMessageAsync({
218
+ domain: tupleCV({ ... }),
219
+ message: tupleCV({ ... }),
220
+ });
221
+ ```
222
+
183
223
  ### `useTransferSTX()`
184
224
 
185
225
  ```ts
@@ -224,6 +264,28 @@ writeContract({
224
264
  });
225
265
  ```
226
266
 
267
+ ### `useSignTransaction()`
268
+
269
+ Sign a serialized transaction without automatically broadcasting it. Useful for sponsored transaction flows where a separate service pays the fee.
270
+
271
+ > **Note:** OKX wallet does not support raw transaction signing and will throw an error.
272
+
273
+ ```ts
274
+ const { signTransaction, signTransactionAsync, data, error, isPending } = useSignTransaction();
275
+
276
+ // Callback style
277
+ signTransaction({ transaction: '0x0100...', broadcast: false }, {
278
+ onSuccess: ({ transaction, txid }) => {},
279
+ onError: (error) => {},
280
+ });
281
+
282
+ // Async style
283
+ const { transaction, txid } = await signTransactionAsync({
284
+ transaction: '0x0100...',
285
+ broadcast: false,
286
+ });
287
+ ```
288
+
227
289
  ### `useBnsName()`
228
290
 
229
291
  ```ts
@@ -255,6 +317,29 @@ All 6 wallets work with both headless (`connect('xverse')`) and modal (`connect(
255
317
  | WalletConnect | `wallet-connect` |
256
318
  | OKX | `okx` |
257
319
 
320
+ ### Wallet Support Matrix
321
+
322
+ | Hook | Xverse | Leather | Asigna | Fordefi | WalletConnect | OKX |
323
+ |------|--------|---------|--------|---------|---------------|-----|
324
+ | `useConnect` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
325
+ | `useSignMessage` | ✓ | ✓ | ? | ? | ~ | ✓ |
326
+ | `useSignStructuredMessage` | ✓ | ✓ | ? | ? | ~ | ✗ |
327
+ | `useSignTransaction` | ✓ | ✓ | ? | ? | ~ | ✗ |
328
+ | `useWriteContract` | ✓ | ✓ | ✓ | ✓ | ~ | ✓ |
329
+ | `useTransferSTX` | ✓ | ✓ | ✓ | ✓ | ~ | ✓ |
330
+
331
+ ✓ Confirmed supported | ✗ Unsupported (throws error) | ? Unverified | ~ Depends on the connected wallet
332
+
333
+ **Notes:**
334
+
335
+ - **OKX** uses a proprietary API (`window.okxwallet.stacks`) instead of the standard `@stacks/connect` RPC. `useSignStructuredMessage` and `useSignTransaction` are explicitly unsupported and will throw.
336
+ - **Asigna** is a multisig wallet. Transaction-based hooks (`useWriteContract`, `useTransferSTX`) work, but message signing hooks may be limited since there is no multisig message signature standard on Stacks.
337
+ - **Fordefi** supports transactions and contract calls on Stacks, but their [supported blockchains](https://docs.fordefi.com/docs/supported-blockchains) page does not list Stacks under message signing capabilities.
338
+ - **WalletConnect** is a relay protocol — all methods are forwarded, but actual support depends on the wallet on the other end.
339
+ - **Xverse** and **Leather** implement the full [SIP-030](https://github.com/janniks/sips/blob/main/sips/sip-030/sip-030-wallet-interface.md) interface.
340
+
341
+ This matrix was compiled from wallet documentation as of March 2026. Sources: [Xverse Sats Connect docs](https://docs.xverse.app/sats-connect/stacks-methods), [Leather developer docs](https://leather.gitbook.io/developers), [Asigna docs](https://asigna.gitbook.io/asigna), [Fordefi docs](https://docs.fordefi.com/docs/supported-blockchains), [@stacks/connect WalletConnect source](https://github.com/stx-labs/connect/tree/main/packages/connect/src/walletconnect).
342
+
258
343
  ## Peer Dependencies
259
344
 
260
345
  - `react` ^18 or ^19
package/dist/index.cjs CHANGED
@@ -6,6 +6,69 @@ var jsxRuntime = require('react/jsx-runtime');
6
6
  var transactions = require('@stacks/transactions');
7
7
  var bnsV2Sdk = require('bns-v2-sdk');
8
8
 
9
+ // src/errors.ts
10
+ var BaseError = class extends Error {
11
+ name = "StacksKitError";
12
+ shortMessage;
13
+ constructor(shortMessage, options) {
14
+ const message = [
15
+ shortMessage,
16
+ options?.details && `Details: ${options.details}`
17
+ ].filter(Boolean).join("\n\n");
18
+ super(message, options?.cause ? { cause: options.cause } : void 0);
19
+ this.shortMessage = shortMessage;
20
+ }
21
+ walk(fn) {
22
+ return walk(this, fn);
23
+ }
24
+ };
25
+ function walk(err, fn) {
26
+ if (fn?.(err)) return err;
27
+ if (err && typeof err === "object" && "cause" in err) {
28
+ return walk(err.cause, fn);
29
+ }
30
+ return err;
31
+ }
32
+ var WalletNotConnectedError = class extends BaseError {
33
+ name = "WalletNotConnectedError";
34
+ constructor() {
35
+ super("Wallet is not connected");
36
+ }
37
+ };
38
+ var WalletNotFoundError = class extends BaseError {
39
+ name = "WalletNotFoundError";
40
+ wallet;
41
+ constructor({ wallet }) {
42
+ super(`${wallet} wallet not found`, {
43
+ details: "The wallet extension may not be installed."
44
+ });
45
+ this.wallet = wallet;
46
+ }
47
+ };
48
+ var UnsupportedMethodError = class extends BaseError {
49
+ name = "UnsupportedMethodError";
50
+ method;
51
+ wallet;
52
+ constructor({ method, wallet }) {
53
+ super(`${method} is not supported by ${wallet} wallet`);
54
+ this.method = method;
55
+ this.wallet = wallet;
56
+ }
57
+ };
58
+ var WalletRequestError = class extends BaseError {
59
+ name = "WalletRequestError";
60
+ method;
61
+ wallet;
62
+ constructor({ method, wallet, cause }) {
63
+ super(`${wallet} wallet request failed`, {
64
+ cause,
65
+ details: cause.message
66
+ });
67
+ this.method = method;
68
+ this.wallet = wallet;
69
+ }
70
+ };
71
+
9
72
  // src/constants/stacks-provider-mapping.ts
10
73
  var STACKS_TO_STACKS_CONNECT_PROVIDERS = {
11
74
  xverse: "XverseProviders.BitcoinProvider",
@@ -685,7 +748,7 @@ var useSignMessage = () => {
685
748
  const signMessageAsync = react.useCallback(
686
749
  async (variables) => {
687
750
  if (!isConnected) {
688
- throw new Error("Wallet is not connected");
751
+ throw new WalletNotConnectedError();
689
752
  }
690
753
  setStatus("pending");
691
754
  setError(null);
@@ -694,7 +757,7 @@ var useSignMessage = () => {
694
757
  let result;
695
758
  if (provider === "okx") {
696
759
  if (!window.okxwallet) {
697
- throw new Error("OKX wallet not found");
760
+ throw new WalletNotFoundError({ wallet: "OKX" });
698
761
  }
699
762
  result = await window.okxwallet.stacks.signMessage({
700
763
  message: variables.message
@@ -711,7 +774,11 @@ var useSignMessage = () => {
711
774
  setStatus("success");
712
775
  return result;
713
776
  } catch (err) {
714
- const error2 = err instanceof Error ? err : new Error(String(err));
777
+ const error2 = err instanceof BaseError ? err : new WalletRequestError({
778
+ method: "stx_signMessage",
779
+ wallet: provider ?? "unknown",
780
+ cause: err instanceof Error ? err : new Error(String(err))
781
+ });
715
782
  setError(error2);
716
783
  setStatus("error");
717
784
  throw error2;
@@ -752,6 +819,163 @@ var useSignMessage = () => {
752
819
  [signMessage, signMessageAsync, reset, data, error, status]
753
820
  );
754
821
  };
822
+ var useSignStructuredMessage = () => {
823
+ const { isConnected, provider } = useAddress();
824
+ const [data, setData] = react.useState(
825
+ void 0
826
+ );
827
+ const [error, setError] = react.useState(null);
828
+ const [status, setStatus] = react.useState("idle");
829
+ const signStructuredMessageAsync = react.useCallback(
830
+ async (variables) => {
831
+ if (!isConnected) {
832
+ throw new WalletNotConnectedError();
833
+ }
834
+ if (provider === "okx") {
835
+ throw new UnsupportedMethodError({
836
+ method: "stx_signStructuredMessage",
837
+ wallet: "OKX"
838
+ });
839
+ }
840
+ setStatus("pending");
841
+ setError(null);
842
+ setData(void 0);
843
+ try {
844
+ const result = await connect.request("stx_signStructuredMessage", {
845
+ message: variables.message,
846
+ domain: variables.domain
847
+ });
848
+ setData(result);
849
+ setStatus("success");
850
+ return result;
851
+ } catch (err) {
852
+ const error2 = err instanceof BaseError ? err : new WalletRequestError({
853
+ method: "stx_signStructuredMessage",
854
+ wallet: provider ?? "unknown",
855
+ cause: err instanceof Error ? err : new Error(String(err))
856
+ });
857
+ setError(error2);
858
+ setStatus("error");
859
+ throw error2;
860
+ }
861
+ },
862
+ [isConnected, provider]
863
+ );
864
+ const signStructuredMessage = react.useCallback(
865
+ (variables, options) => {
866
+ signStructuredMessageAsync(variables).then((data2) => {
867
+ options?.onSuccess?.(data2);
868
+ options?.onSettled?.(data2, null);
869
+ }).catch((error2) => {
870
+ options?.onError?.(error2);
871
+ options?.onSettled?.(void 0, error2);
872
+ });
873
+ },
874
+ [signStructuredMessageAsync]
875
+ );
876
+ const reset = react.useCallback(() => {
877
+ setData(void 0);
878
+ setError(null);
879
+ setStatus("idle");
880
+ }, []);
881
+ return react.useMemo(
882
+ () => ({
883
+ signStructuredMessage,
884
+ signStructuredMessageAsync,
885
+ reset,
886
+ data,
887
+ error,
888
+ isError: status === "error",
889
+ isIdle: status === "idle",
890
+ isPending: status === "pending",
891
+ isSuccess: status === "success",
892
+ status
893
+ }),
894
+ [
895
+ signStructuredMessage,
896
+ signStructuredMessageAsync,
897
+ reset,
898
+ data,
899
+ error,
900
+ status
901
+ ]
902
+ );
903
+ };
904
+ var useSignTransaction = () => {
905
+ const { isConnected, provider } = useAddress();
906
+ const [data, setData] = react.useState(void 0);
907
+ const [error, setError] = react.useState(null);
908
+ const [status, setStatus] = react.useState("idle");
909
+ const signTransactionAsync = react.useCallback(
910
+ async (variables) => {
911
+ if (!isConnected) {
912
+ throw new WalletNotConnectedError();
913
+ }
914
+ if (provider === "okx") {
915
+ throw new UnsupportedMethodError({
916
+ method: "stx_signTransaction",
917
+ wallet: "OKX"
918
+ });
919
+ }
920
+ setStatus("pending");
921
+ setError(null);
922
+ setData(void 0);
923
+ try {
924
+ const result = await connect.request("stx_signTransaction", {
925
+ transaction: variables.transaction,
926
+ ...variables.broadcast !== void 0 && {
927
+ broadcast: variables.broadcast
928
+ }
929
+ });
930
+ setData(result);
931
+ setStatus("success");
932
+ return result;
933
+ } catch (err) {
934
+ const error2 = err instanceof BaseError ? err : new WalletRequestError({
935
+ method: "stx_signTransaction",
936
+ wallet: provider ?? "unknown",
937
+ cause: err instanceof Error ? err : new Error(String(err))
938
+ });
939
+ setError(error2);
940
+ setStatus("error");
941
+ throw error2;
942
+ }
943
+ },
944
+ [isConnected, provider]
945
+ );
946
+ const signTransaction = react.useCallback(
947
+ (variables, options) => {
948
+ signTransactionAsync(variables).then((data2) => {
949
+ options?.onSuccess?.(data2);
950
+ options?.onSettled?.(data2, null);
951
+ }).catch((error2) => {
952
+ options?.onError?.(error2);
953
+ options?.onSettled?.(void 0, error2);
954
+ });
955
+ },
956
+ [signTransactionAsync]
957
+ );
958
+ const reset = react.useCallback(() => {
959
+ setData(void 0);
960
+ setError(null);
961
+ setStatus("idle");
962
+ }, []);
963
+ return react.useMemo(
964
+ () => ({
965
+ signTransaction,
966
+ signTransactionAsync,
967
+ reset,
968
+ data,
969
+ error,
970
+ isError: status === "error",
971
+ isIdle: status === "idle",
972
+ isPending: status === "pending",
973
+ isSuccess: status === "success",
974
+ status
975
+ }),
976
+ [signTransaction, signTransactionAsync, reset, data, error, status]
977
+ );
978
+ };
755
979
 
756
980
  // src/utils/get-network-from-address.ts
757
981
  var getNetworkFromAddress = (address) => {
@@ -773,7 +997,7 @@ var useTransferSTX = () => {
773
997
  const transferSTXAsync = react.useCallback(
774
998
  async (variables) => {
775
999
  if (!isConnected || !address) {
776
- throw new Error("Wallet is not connected");
1000
+ throw new WalletNotConnectedError();
777
1001
  }
778
1002
  setStatus("pending");
779
1003
  setError(null);
@@ -781,7 +1005,7 @@ var useTransferSTX = () => {
781
1005
  try {
782
1006
  if (provider === "okx") {
783
1007
  if (!window.okxwallet) {
784
- throw new Error("OKX wallet not found");
1008
+ throw new WalletNotFoundError({ wallet: "OKX" });
785
1009
  }
786
1010
  const response2 = await window.okxwallet.stacks.signTransaction({
787
1011
  txType: "token_transfer",
@@ -816,7 +1040,11 @@ var useTransferSTX = () => {
816
1040
  setStatus("success");
817
1041
  return response.txid;
818
1042
  } catch (err) {
819
- const error2 = err instanceof Error ? err : new Error(String(err));
1043
+ const error2 = err instanceof BaseError ? err : new WalletRequestError({
1044
+ method: "stx_transferStx",
1045
+ wallet: provider ?? "unknown",
1046
+ cause: err instanceof Error ? err : new Error(String(err))
1047
+ });
820
1048
  setError(error2);
821
1049
  setStatus("error");
822
1050
  throw error2;
@@ -1004,7 +1232,7 @@ var useWriteContract = () => {
1004
1232
  const writeContractAsync = react.useCallback(
1005
1233
  async (variables) => {
1006
1234
  if (!isConnected || !address) {
1007
- throw new Error("Wallet is not connected");
1235
+ throw new WalletNotConnectedError();
1008
1236
  }
1009
1237
  setStatus("pending");
1010
1238
  setError(null);
@@ -1013,7 +1241,7 @@ var useWriteContract = () => {
1013
1241
  try {
1014
1242
  if (provider === "okx") {
1015
1243
  if (!window.okxwallet) {
1016
- throw new Error("OKX wallet not found");
1244
+ throw new WalletNotFoundError({ wallet: "OKX" });
1017
1245
  }
1018
1246
  const response2 = await window.okxwallet.stacks.signTransaction({
1019
1247
  contractAddress: variables.address,
@@ -1048,7 +1276,11 @@ var useWriteContract = () => {
1048
1276
  setStatus("success");
1049
1277
  return response.txid;
1050
1278
  } catch (err) {
1051
- const error2 = err instanceof Error ? err : new Error(String(err));
1279
+ const error2 = err instanceof BaseError ? err : new WalletRequestError({
1280
+ method: "stx_callContract",
1281
+ wallet: provider ?? "unknown",
1282
+ cause: err instanceof Error ? err : new Error(String(err))
1283
+ });
1052
1284
  setError(error2);
1053
1285
  setStatus("error");
1054
1286
  throw error2;
@@ -1133,8 +1365,13 @@ function createContractConfig(config) {
1133
1365
  return config;
1134
1366
  }
1135
1367
 
1368
+ exports.BaseError = BaseError;
1136
1369
  exports.SUPPORTED_STACKS_WALLETS = SUPPORTED_STACKS_WALLETS;
1137
1370
  exports.StacksWalletProvider = StacksWalletProvider;
1371
+ exports.UnsupportedMethodError = UnsupportedMethodError;
1372
+ exports.WalletNotConnectedError = WalletNotConnectedError;
1373
+ exports.WalletNotFoundError = WalletNotFoundError;
1374
+ exports.WalletRequestError = WalletRequestError;
1138
1375
  exports.createContractConfig = createContractConfig;
1139
1376
  exports.getLocalStorageWallet = getLocalStorageWallet;
1140
1377
  exports.getNetworkFromAddress = getNetworkFromAddress;
@@ -1144,6 +1381,8 @@ exports.useBnsName = useBnsName;
1144
1381
  exports.useConnect = useConnect;
1145
1382
  exports.useDisconnect = useDisconnect;
1146
1383
  exports.useSignMessage = useSignMessage;
1384
+ exports.useSignStructuredMessage = useSignStructuredMessage;
1385
+ exports.useSignTransaction = useSignTransaction;
1147
1386
  exports.useTransferSTX = useTransferSTX;
1148
1387
  exports.useWallets = useWallets;
1149
1388
  exports.useWriteContract = useWriteContract;