@sodax/wallet-sdk-react 1.5.6-beta → 2.0.0-rc.1

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.
Files changed (211) hide show
  1. package/README.md +103 -145
  2. package/ai-exported/AGENTS.md +122 -0
  3. package/ai-exported/integration/README.md +102 -0
  4. package/ai-exported/integration/ai-rules.md +136 -0
  5. package/ai-exported/integration/architecture.md +181 -0
  6. package/ai-exported/integration/examples/01-minimal-evm.tsx +75 -0
  7. package/ai-exported/integration/examples/02-multi-chain-modal.tsx +169 -0
  8. package/ai-exported/integration/examples/03-nextjs-app-router.tsx +99 -0
  9. package/ai-exported/integration/examples/04-walletconnect-setup.tsx +89 -0
  10. package/ai-exported/integration/examples/README.md +29 -0
  11. package/ai-exported/integration/recipes/batch-operations.md +223 -0
  12. package/ai-exported/integration/recipes/bridge-to-sdk.md +164 -0
  13. package/ai-exported/integration/recipes/chain-detection.md +254 -0
  14. package/ai-exported/integration/recipes/connect-button.md +156 -0
  15. package/ai-exported/integration/recipes/multi-chain-modal.md +199 -0
  16. package/ai-exported/integration/recipes/setup.md +158 -0
  17. package/ai-exported/integration/recipes/sign-message.md +137 -0
  18. package/ai-exported/integration/recipes/sub-path-imports.md +95 -0
  19. package/ai-exported/integration/recipes/switch-chain.md +141 -0
  20. package/ai-exported/integration/recipes/walletconnect-setup.md +139 -0
  21. package/ai-exported/integration/reference/api-surface.md +175 -0
  22. package/ai-exported/integration/reference/chain-support.md +78 -0
  23. package/ai-exported/integration/reference/connectors.md +74 -0
  24. package/ai-exported/integration/reference/hooks.md +204 -0
  25. package/ai-exported/integration/reference/wallet-brands.md +106 -0
  26. package/ai-exported/migration/README.md +49 -0
  27. package/ai-exported/migration/ai-rules.md +144 -0
  28. package/ai-exported/migration/breaking-changes.md +305 -0
  29. package/ai-exported/migration/checklist.md +159 -0
  30. package/ai-exported/migration/recipes/connect-button.md +166 -0
  31. package/ai-exported/migration/recipes/multi-chain-modal.md +244 -0
  32. package/ai-exported/migration/recipes/ssr-setup.md +162 -0
  33. package/ai-exported/migration/recipes/walletconnect-migration.md +168 -0
  34. package/ai-exported/migration/reference/components.md +73 -0
  35. package/ai-exported/migration/reference/config.md +307 -0
  36. package/ai-exported/migration/reference/hooks.md +278 -0
  37. package/ai-exported/migration/reference/imports.md +157 -0
  38. package/dist/XConnector-B9YQTVJ4.d.ts +146 -0
  39. package/dist/chunk-2BOUGCJ7.mjs +150 -0
  40. package/dist/chunk-2BOUGCJ7.mjs.map +1 -0
  41. package/dist/chunk-66BAUK56.mjs +202 -0
  42. package/dist/chunk-66BAUK56.mjs.map +1 -0
  43. package/dist/chunk-7ULB6DW4.mjs +102 -0
  44. package/dist/chunk-7ULB6DW4.mjs.map +1 -0
  45. package/dist/chunk-BKJB527E.mjs +125 -0
  46. package/dist/chunk-BKJB527E.mjs.map +1 -0
  47. package/dist/chunk-BXJLBR4G.mjs +88 -0
  48. package/dist/chunk-BXJLBR4G.mjs.map +1 -0
  49. package/dist/chunk-E5IAZ7E6.mjs +186 -0
  50. package/dist/chunk-E5IAZ7E6.mjs.map +1 -0
  51. package/dist/chunk-MAQ47Q52.mjs +33 -0
  52. package/dist/chunk-MAQ47Q52.mjs.map +1 -0
  53. package/dist/chunk-MXZVF5HR.mjs +34 -0
  54. package/dist/chunk-MXZVF5HR.mjs.map +1 -0
  55. package/dist/chunk-N5A2TMF6.mjs +33 -0
  56. package/dist/chunk-N5A2TMF6.mjs.map +1 -0
  57. package/dist/chunk-NY7U7OJW.mjs +64 -0
  58. package/dist/chunk-NY7U7OJW.mjs.map +1 -0
  59. package/dist/chunk-PJLEJVAU.mjs +140 -0
  60. package/dist/chunk-PJLEJVAU.mjs.map +1 -0
  61. package/dist/chunk-PLCA4ZDJ.mjs +1585 -0
  62. package/dist/chunk-PLCA4ZDJ.mjs.map +1 -0
  63. package/dist/chunk-TZMKDXFA.mjs +3 -0
  64. package/dist/chunk-TZMKDXFA.mjs.map +1 -0
  65. package/dist/chunk-X2MHIWXO.mjs +100 -0
  66. package/dist/chunk-X2MHIWXO.mjs.map +1 -0
  67. package/dist/chunk-XZ7CHO2S.mjs +41 -0
  68. package/dist/chunk-XZ7CHO2S.mjs.map +1 -0
  69. package/dist/config-OlnzyEUE.d.ts +146 -0
  70. package/dist/index.cjs +2784 -1594
  71. package/dist/index.cjs.map +1 -1
  72. package/dist/index.d.ts +768 -1498
  73. package/dist/index.mjs +463 -2004
  74. package/dist/index.mjs.map +1 -1
  75. package/dist/xchains/bitcoin/index.cjs +1927 -0
  76. package/dist/xchains/bitcoin/index.cjs.map +1 -0
  77. package/dist/xchains/bitcoin/index.d.ts +125 -0
  78. package/dist/xchains/bitcoin/index.mjs +16 -0
  79. package/dist/xchains/bitcoin/index.mjs.map +1 -0
  80. package/dist/xchains/evm/index.cjs +316 -0
  81. package/dist/xchains/evm/index.cjs.map +1 -0
  82. package/dist/xchains/evm/index.d.ts +39 -0
  83. package/dist/xchains/evm/index.mjs +5 -0
  84. package/dist/xchains/evm/index.mjs.map +1 -0
  85. package/dist/xchains/icon/index.cjs +311 -0
  86. package/dist/xchains/icon/index.cjs.map +1 -0
  87. package/dist/xchains/icon/index.d.ts +37 -0
  88. package/dist/xchains/icon/index.mjs +7 -0
  89. package/dist/xchains/icon/index.mjs.map +1 -0
  90. package/dist/xchains/injective/index.cjs +223 -0
  91. package/dist/xchains/injective/index.cjs.map +1 -0
  92. package/dist/xchains/injective/index.d.ts +35 -0
  93. package/dist/xchains/injective/index.mjs +5 -0
  94. package/dist/xchains/injective/index.mjs.map +1 -0
  95. package/dist/xchains/near/index.cjs +190 -0
  96. package/dist/xchains/near/index.cjs.map +1 -0
  97. package/dist/xchains/near/index.d.ts +34 -0
  98. package/dist/xchains/near/index.mjs +6 -0
  99. package/dist/xchains/near/index.mjs.map +1 -0
  100. package/dist/xchains/solana/index.cjs +186 -0
  101. package/dist/xchains/solana/index.cjs.map +1 -0
  102. package/dist/xchains/solana/index.d.ts +26 -0
  103. package/dist/xchains/solana/index.mjs +7 -0
  104. package/dist/xchains/solana/index.mjs.map +1 -0
  105. package/dist/xchains/stacks/index.cjs +240 -0
  106. package/dist/xchains/stacks/index.cjs.map +1 -0
  107. package/dist/xchains/stacks/index.d.ts +36 -0
  108. package/dist/xchains/stacks/index.mjs +5 -0
  109. package/dist/xchains/stacks/index.mjs.map +1 -0
  110. package/dist/xchains/stellar/index.cjs +322 -0
  111. package/dist/xchains/stellar/index.cjs.map +1 -0
  112. package/dist/xchains/stellar/index.d.ts +44 -0
  113. package/dist/xchains/stellar/index.mjs +6 -0
  114. package/dist/xchains/stellar/index.mjs.map +1 -0
  115. package/dist/xchains/sui/index.cjs +248 -0
  116. package/dist/xchains/sui/index.cjs.map +1 -0
  117. package/dist/xchains/sui/index.d.ts +37 -0
  118. package/dist/xchains/sui/index.mjs +7 -0
  119. package/dist/xchains/sui/index.mjs.map +1 -0
  120. package/docs/ADDING_A_NEW_CHAIN.md +440 -0
  121. package/docs/ARCHITECTURE.md +291 -0
  122. package/docs/BATCH_OPERATIONS.md +267 -0
  123. package/docs/CHAIN_DETECTION.md +216 -0
  124. package/docs/CONFIGURE_PROVIDER.md +360 -0
  125. package/docs/CONNECTORS.md +247 -0
  126. package/docs/CONNECT_FLOW.md +276 -0
  127. package/docs/EVM_SWITCH_CHAIN.md +161 -0
  128. package/docs/SIGN_MESSAGE.md +213 -0
  129. package/docs/SUB_PATH_EXPORTS.md +246 -0
  130. package/docs/WALLETCONNECT.md +154 -0
  131. package/docs/WALLET_MODAL.md +331 -0
  132. package/docs/WALLET_PROVIDER_BRIDGE.md +226 -0
  133. package/package.json +34 -9
  134. package/skills/SKILLS.md +84 -0
  135. package/skills/bridge-to-sdk.md +148 -0
  136. package/skills/connect-button.md +116 -0
  137. package/skills/evm-only-walletconnect.md +111 -0
  138. package/skills/multi-chain-modal.md +178 -0
  139. package/skills/setup.md +107 -0
  140. package/dist/index.d.cts +0 -1579
  141. package/src/Hydrate.ts +0 -65
  142. package/src/SodaxWalletProvider.tsx +0 -97
  143. package/src/actions/getXChainType.ts +0 -8
  144. package/src/actions/getXService.ts +0 -33
  145. package/src/actions/index.ts +0 -2
  146. package/src/assets/wallets/hana.svg +0 -6
  147. package/src/assets/wallets/havah.svg +0 -76
  148. package/src/assets/wallets/keplr.svg +0 -30
  149. package/src/assets/wallets/metamask.svg +0 -60
  150. package/src/assets/wallets/phantom.svg +0 -4
  151. package/src/assets/wallets/sui.svg +0 -20
  152. package/src/core/XConnector.ts +0 -54
  153. package/src/core/XService.ts +0 -85
  154. package/src/core/index.ts +0 -2
  155. package/src/hooks/index.ts +0 -11
  156. package/src/hooks/useEthereumChainId.ts +0 -44
  157. package/src/hooks/useEvmSwitchChain.ts +0 -91
  158. package/src/hooks/useWalletProvider.ts +0 -206
  159. package/src/hooks/useXAccount.ts +0 -51
  160. package/src/hooks/useXAccounts.ts +0 -56
  161. package/src/hooks/useXBalances.ts +0 -65
  162. package/src/hooks/useXConnect.ts +0 -118
  163. package/src/hooks/useXConnection.ts +0 -72
  164. package/src/hooks/useXConnectors.ts +0 -72
  165. package/src/hooks/useXDisconnect.ts +0 -73
  166. package/src/hooks/useXService.ts +0 -8
  167. package/src/hooks/useXSignMessage.ts +0 -82
  168. package/src/index.ts +0 -19
  169. package/src/types/index.ts +0 -22
  170. package/src/useXWagmiStore.ts +0 -116
  171. package/src/utils/index.ts +0 -21
  172. package/src/xchains/bitcoin/BitcoinXConnector.ts +0 -34
  173. package/src/xchains/bitcoin/BitcoinXService.ts +0 -40
  174. package/src/xchains/bitcoin/OKXXConnector.ts +0 -117
  175. package/src/xchains/bitcoin/UnisatXConnector.ts +0 -117
  176. package/src/xchains/bitcoin/XverseXConnector.ts +0 -232
  177. package/src/xchains/bitcoin/index.ts +0 -7
  178. package/src/xchains/bitcoin/useBitcoinXConnectors.ts +0 -14
  179. package/src/xchains/evm/EvmXConnector.ts +0 -27
  180. package/src/xchains/evm/EvmXService.ts +0 -211
  181. package/src/xchains/evm/index.ts +0 -3
  182. package/src/xchains/icon/IconHanaXConnector.ts +0 -39
  183. package/src/xchains/icon/IconXService.ts +0 -117
  184. package/src/xchains/icon/actions.ts +0 -28
  185. package/src/xchains/icon/iconex/index.tsx +0 -46
  186. package/src/xchains/icon/index.ts +0 -2
  187. package/src/xchains/injective/InjectiveXConnector.ts +0 -60
  188. package/src/xchains/injective/InjectiveXService.ts +0 -62
  189. package/src/xchains/injective/actions.ts +0 -32
  190. package/src/xchains/injective/index.ts +0 -2
  191. package/src/xchains/near/NearXConnector.ts +0 -42
  192. package/src/xchains/near/NearXService.ts +0 -46
  193. package/src/xchains/near/useNearXConnectors.ts +0 -23
  194. package/src/xchains/solana/SolanaXConnector.ts +0 -26
  195. package/src/xchains/solana/SolanaXService.ts +0 -46
  196. package/src/xchains/solana/index.ts +0 -2
  197. package/src/xchains/stacks/StacksXConnector.ts +0 -63
  198. package/src/xchains/stacks/StacksXService.ts +0 -59
  199. package/src/xchains/stacks/constants.ts +0 -42
  200. package/src/xchains/stacks/index.ts +0 -4
  201. package/src/xchains/stacks/useStacksXConnectors.ts +0 -7
  202. package/src/xchains/stellar/CustomSorobanServer.ts +0 -93
  203. package/src/xchains/stellar/StellarWalletsKitXConnector.ts +0 -53
  204. package/src/xchains/stellar/StellarXService.ts +0 -93
  205. package/src/xchains/stellar/actions.ts +0 -24
  206. package/src/xchains/stellar/index.tsx +0 -2
  207. package/src/xchains/stellar/useStellarXConnectors.ts +0 -21
  208. package/src/xchains/stellar/utils.ts +0 -49
  209. package/src/xchains/sui/SuiXConnector.ts +0 -28
  210. package/src/xchains/sui/SuiXService.ts +0 -66
  211. package/src/xchains/sui/index.ts +0 -2
@@ -0,0 +1,291 @@
1
+ # Architecture
2
+
3
+ `@sodax/wallet-sdk-react` is structured around five core ideas: a single Zustand store as source of truth, a chain registry that abstracts over heterogeneous wallet SDKs, a Provider/Hydrator/Actions trio for chains that need React context, async persistence with cleanup for stale connections, and store-first hooks that never call native chain SDK hooks directly.
4
+
5
+ This document covers how these pieces fit together. For consumer-facing API, see [`CONNECT_FLOW.md`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONNECT_FLOW.md) and friends.
6
+
7
+ ## Table of contents
8
+
9
+ 1. [High-level layout](#high-level-layout)
10
+ 2. [Zustand store — single source of truth](#zustand-store--single-source-of-truth)
11
+ 3. [Chain registry — abstraction over wallet SDKs](#chain-registry--abstraction-over-wallet-sdks)
12
+ 4. [Provider-managed vs non-provider chains](#provider-managed-vs-non-provider-chains)
13
+ 5. [Provider/Hydrator/Actions trio](#providerhydratoractions-trio)
14
+ 6. [Persistence and hydration](#persistence-and-hydration)
15
+ 7. [Store-first hooks](#store-first-hooks)
16
+ 8. [Bridge to wallet-sdk-core](#bridge-to-wallet-sdk-core)
17
+
18
+ ---
19
+
20
+ ## High-level layout
21
+
22
+ ```
23
+ ┌─ <SodaxWalletProvider config={...}>
24
+ │ ├─ <WalletConfigProvider value={config}> (React context — read by Hydrators)
25
+ │ ├─ <EvmProvider> (if config.EVM) ← provider-managed
26
+ │ │ ├─ <WagmiProvider>
27
+ │ │ │ ├─ <EvmHydrator> ← writes connection + provider to store
28
+ │ │ │ └─ <EvmActions> ← registers ChainActions in store
29
+ │ │ └─ children
30
+ │ ├─ <SolanaProvider> (if config.SOLANA) ← provider-managed
31
+ │ │ └─ <Hydrator>+<Actions>+<adapter>
32
+ │ ├─ <SuiProvider> (if config.SUI) ← provider-managed
33
+ │ │ └─ <Hydrator>+<Actions>+<adapter>
34
+ │ └─ useInitChainServices(config)
35
+ │ ├─ chainRegistry[<chain>].createService(walletConfig)
36
+ │ ├─ chainRegistry[<chain>].defaultConnectors(walletConfig)
37
+ │ ├─ chainRegistry[<chain>].createActions?(...) ← non-provider only
38
+ │ ├─ chainRegistry[<chain>].discoverConnectors? ← async (Stellar)
39
+ │ └─ persist hydration → cleanupDisabledConnections()
40
+
41
+ └─ Consumer hooks read from useXWalletStore — never from wagmi/wallet-adapter/dapp-kit directly.
42
+ ```
43
+
44
+ Sources:
45
+ - [`SodaxWalletProvider.tsx`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/src/SodaxWalletProvider.tsx)
46
+ - [`useXWalletStore.ts`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/src/useXWalletStore.ts)
47
+ - [`chainRegistry.ts`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/src/chainRegistry.ts)
48
+
49
+ ---
50
+
51
+ ## Zustand store — single source of truth
52
+
53
+ [`useXWalletStore`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/src/useXWalletStore.ts) holds **everything** consumers read:
54
+
55
+ ```typescript
56
+ type XWalletStore = {
57
+ xServices: Partial<Record<ChainType, XService>>; // chain service singletons
58
+ xConnections: Partial<Record<ChainType, XConnection>>; // PERSISTED
59
+ xConnectorsByChain: Partial<Record<ChainType, XConnector[]>>;
60
+ enabledChains: ChainType[];
61
+ chainActions: Partial<Record<ChainType, ChainActions>>; // connect/disconnect/signMessage
62
+ walletProviders: Partial<Record<ChainType, IWalletProvider>>; // bridge to wallet-sdk-core
63
+ walletConfig: SodaxWalletConfig | undefined; // user-supplied config snapshot
64
+
65
+ setXConnection(chainType, conn): void;
66
+ unsetXConnection(chainType): void;
67
+ setXConnectors(chainType, conns): void;
68
+ registerChainActions(chainType, actions): void;
69
+ setWalletProvider(chainType, provider): void;
70
+ initChainServices(config): void;
71
+ cleanupDisabledConnections(): void;
72
+ };
73
+ ```
74
+
75
+ ### Middleware stack
76
+
77
+ ```typescript
78
+ devtools(persist(immer((set, get) => ({...})), { ... }))
79
+ ```
80
+
81
+ | Layer | Role |
82
+ |-------|------|
83
+ | `immer` | Lets `set(state => { state.xConnections.EVM = ... })` work without manual spreading |
84
+ | `persist` | Mirrors `xConnections` to `localStorage` (key `'xwagmi-store'`); rehydrates on first mount |
85
+ | `devtools` | Redux DevTools integration for debugging |
86
+
87
+ ### What's persisted
88
+
89
+ Only `xConnections`. The rest (services, connectors, actions, wallet providers) is reconstructed on every page load — these contain SDK class instances that don't survive `JSON.stringify`.
90
+
91
+ ```typescript
92
+ partialize: state => ({ xConnections: state.xConnections })
93
+ ```
94
+
95
+ Storage key is **`'xwagmi-store'`** (kept from v1 for backward compat — existing users don't lose connections on upgrade).
96
+
97
+ ### Why one store
98
+
99
+ Earlier iterations had per-chain stores. The single-store design wins because:
100
+
101
+ - `useXAccounts()` / `useXConnections()` / `useChainGroups()` need cross-chain data; per-chain stores would force consumers to subscribe to N stores and fan-in.
102
+ - Persist/hydration semantics are uniform — one store, one rehydrate event, one cleanup pass.
103
+ - Concurrent updates across chains (e.g. user connects EVM and Solana in parallel via `useBatchConnect`) don't race with separate Zustand instances.
104
+
105
+ The `useWalletModalStore` (modal lifecycle) is a separate slice intentionally — modal state is ephemeral UI state, persists nothing, and shares no concerns with connection state.
106
+
107
+ ---
108
+
109
+ ## Chain registry — abstraction over wallet SDKs
110
+
111
+ Each chain family is a single entry in [`chainRegistry`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/src/chainRegistry.ts):
112
+
113
+ ```typescript
114
+ type ChainServiceFactory<S extends XService = XService> = {
115
+ createService(walletConfig?): S;
116
+ displayName: string;
117
+ iconUrl?: string;
118
+ defaultConnectors(walletConfig?): XConnector[];
119
+ providerManaged: boolean;
120
+ createActions?(service, getStore): ChainActions;
121
+ createWalletProvider?(service, getStore): IWalletProvider | undefined;
122
+ discoverConnectors?(service, getStore): Promise<void>;
123
+ };
124
+
125
+ export const chainRegistry: Record<string, ChainServiceFactory> = {
126
+ EVM: { createService: () => EvmXService.getInstance(), defaultConnectors: () => [], providerManaged: true, ... },
127
+ SOLANA: { ..., providerManaged: true },
128
+ SUI: { ..., providerManaged: true },
129
+ BITCOIN: { ..., providerManaged: false, createActions, createWalletProvider, ... },
130
+ ICON: { ..., providerManaged: false, ... },
131
+ INJECTIVE: { ..., providerManaged: false, createActions, createWalletProvider, ... },
132
+ STELLAR: { ..., providerManaged: false, discoverConnectors, ... },
133
+ NEAR: { ..., providerManaged: false, ... },
134
+ STACKS: { ..., providerManaged: false, ... },
135
+ };
136
+ ```
137
+
138
+ `createChainServices()` walks the registry, calls `createService` and `defaultConnectors` per enabled chain, registers `ChainActions` for non-provider chains, and triggers `discoverConnectors` for chains that need async wallet detection (Stellar).
139
+
140
+ The registry is the **only** place that imports concrete chain classes. Hooks downstream depend on `IXService` / `IXConnector` interfaces — adding a new chain doesn't ripple through hook code.
141
+
142
+ See [`ADDING_A_NEW_CHAIN.md`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/ADDING_A_NEW_CHAIN.md) for the chain-onboarding workflow.
143
+
144
+ ---
145
+
146
+ ## Provider-managed vs non-provider chains
147
+
148
+ | Property | Provider-managed (EVM/Solana/Sui) | Non-provider (Bitcoin/ICON/Injective/Stellar/NEAR/Stacks) |
149
+ |----------|-----------------------------------|------------------------------------------------------------|
150
+ | Native SDK | wagmi / @solana/wallet-adapter / @mysten/dapp-kit | sats-connect / icon-sdk-js / @injectivelabs/wallet-* / etc. |
151
+ | React provider needed | Yes | No |
152
+ | Connection-state writer | `<Hydrator>` component | Store side-effect inside `setXConnection()` |
153
+ | `ChainActions.connect/disconnect` | Triggers native SDK only — never writes state | `createDefaultActions` reads store, calls connector, writes state |
154
+ | Wallet provider construction | `<Hydrator>` builds & writes | Side-effect of `setXConnection` via `chainRegistry.<chain>.createWalletProvider` |
155
+ | Connector discovery | EIP-6963 / vendor protocol → wagmi/adapter discovers reactively | Static list from `defaultConnectors()` at init time, or async via `discoverConnectors` |
156
+
157
+ The split exists because some wallet ecosystems (EVM, Solana, Sui) have established React libraries with their own context providers — wrapping our store on top of theirs is cheaper than reimplementing connection management. The remaining six chains have lighter-weight SDKs (or no React layer at all), so we own the lifecycle directly.
158
+
159
+ ---
160
+
161
+ ## Provider/Hydrator/Actions trio
162
+
163
+ Provider-managed chains use a 3-component pattern:
164
+
165
+ ```
166
+ <EvmProvider config={...}>
167
+ <WagmiProvider>
168
+ <EvmHydrator /> ← reactive writes to store
169
+ <EvmActions /> ← register ChainActions
170
+ {children}
171
+ </WagmiProvider>
172
+ </EvmProvider>
173
+ ```
174
+
175
+ | Component | Role |
176
+ |-----------|------|
177
+ | `<{Chain}Provider>` | Wraps the native SDK provider (wagmi / wallet-adapter / dapp-kit) |
178
+ | `<{Chain}Hydrator>` | **Sole writer of connection state + wallet providers**. Subscribes to native SDK hooks (`useAccount`, `useConnectors`, `useWalletClient`) and writes through `setXConnection` / `setWalletProvider`. Returns `null` |
179
+ | `<{Chain}Actions>` | Registers `ChainActions.connect/disconnect/signMessage` using a ref to native SDK functions. The registered closures only **trigger** SDK operations; they never write state directly |
180
+
181
+ ### Single-writer invariant
182
+
183
+ Only the Hydrator writes connection state for provider-managed chains. The Actions component **does not** call `setXConnection` after a successful native connect — the Hydrator observes the wagmi/adapter status flip and handles it.
184
+
185
+ This is why `useXConnect.mutateAsync(connector)` resolves with `undefined` for EVM/Solana/Sui ([Connect Flow caveat](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONNECT_FLOW.md#provider-managed-chains-caveat)). The mutation kicks off `wagmi.connect()` and resolves; the Hydrator independently observes the status change and writes the account.
186
+
187
+ The split prevents two failure modes:
188
+
189
+ 1. **Race conditions** if both Actions and Hydrator wrote on different events — the more recent write wins, which may be the wrong one (e.g. wallet returns address quickly but wagmi is still in `'connecting'`).
190
+ 2. **Stale state** if Actions wrote on connect but didn't subscribe to subsequent disconnect events — wagmi `'disconnected'` would never reach the store.
191
+
192
+ Centralizing in the Hydrator means there's exactly one effect tree responsible for keeping the store in sync.
193
+
194
+ ### Sui's special concern — `signPersonalMessage` ref
195
+
196
+ The Actions component holds a ref to the native SDK's signing function:
197
+
198
+ ```typescript
199
+ const signMessageRef = useRef(signPersonalMessage);
200
+ useEffect(() => { signMessageRef.current = signPersonalMessage; }, [signPersonalMessage]);
201
+
202
+ useEffect(() => {
203
+ registerChainActions('SUI', {
204
+ signMessage: async (message) => signMessageRef.current({ message: ... }),
205
+ ...
206
+ });
207
+ }, []); // register once on mount
208
+ ```
209
+
210
+ The registration runs once on mount; calling `signMessageRef.current(...)` always invokes the latest function. This avoids re-registering on every render (which would invalidate downstream `useEffect` deps).
211
+
212
+ ---
213
+
214
+ ## Persistence and hydration
215
+
216
+ Zustand's `persist` middleware writes `xConnections` to `localStorage` synchronously on every change and rehydrates on first mount. The lifecycle:
217
+
218
+ ```
219
+ mount
220
+
221
+ useInitChainServices(config) called
222
+
223
+ initChainServices(config) — synchronously builds services, connectors, ChainActions
224
+
225
+ register .onFinishHydration(afterHydration) ← wait for persist
226
+
227
+ ... persist middleware finishes async hydration ...
228
+
229
+ afterHydration runs:
230
+ ├─ cleanupDisabledConnections() ← remove xConnections for chains not in enabledChains
231
+ ├─ reconnectIcon() if config.ICON ← reconnect Hana wallet
232
+ ├─ reconnectInjective() if config.INJECTIVE
233
+ └─ reconnectStellar() if config.STELLAR
234
+ ```
235
+
236
+ ### `cleanupDisabledConnections`
237
+
238
+ Persist restores `xConnections` from `localStorage` blind to the current `enabledChains`. If the user disabled a chain that was previously connected, the persisted entry would otherwise sit forever.
239
+
240
+ `cleanupDisabledConnections()` walks `xConnections` and deletes any entry whose chain isn't in the current `enabledChains` set. Runs once after persist hydration.
241
+
242
+ ### Hydration flag for UI
243
+
244
+ `useConnectedChains` exposes `status: 'loading' | 'ready'` derived from `useXWalletStore.persist.hasHydrated()`. Use it to gate "Connect wallet" → "Connected" UIs and avoid first-paint flicker. See [`CHAIN_DETECTION.md`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CHAIN_DETECTION.md#hydration-status--gating-reload-flicker).
245
+
246
+ ### Provider-managed reconnect
247
+
248
+ Wagmi/wallet-adapter/dapp-kit have their **own** persistence layers — they store the last connector id and auto-reconnect on `WagmiProvider` mount (gated by `reconnectOnMount`). The Hydrator observes the resulting `'connected'` status and writes to our store. We don't replicate the reconnect logic for provider-managed chains.
249
+
250
+ Non-provider chains (ICON, Injective, Stellar) have no auto-reconnect — `reconnectXxx()` helpers in `useInitChainServices` re-call `connect()` on the previously persisted connector after hydration.
251
+
252
+ **Bitcoin, NEAR, and Stacks** have **no reconnect helper at all**. Bitcoin restores its `walletProvider` from `window.*` + the persisted `XAccount` via `BitcoinXConnector.recreateWalletProvider` (no popup). NEAR and Stacks do not auto-reconnect on reload — the user must re-connect manually.
253
+
254
+ ---
255
+
256
+ ## Store-first hooks
257
+
258
+ Every public hook in `src/hooks/` reads from `useXWalletStore` — **none** call native SDK hooks (`useAccount` from wagmi, `useWallet` from `@solana/wallet-adapter-react`) directly. This is by design:
259
+
260
+ - **Consistent shape** across chains. `useXAccount({ xChainType: 'EVM' })` and `useXAccount({ xChainType: 'SOLANA' })` return the same `XAccount` shape.
261
+ - **Decoupling from native SDKs** — swapping wagmi v3 for v4 is a Hydrator-internal change; consumer code doesn't break.
262
+ - **Single subscription model** — Zustand selector functions deduplicate re-renders (only consumers reading the changed slice re-render).
263
+
264
+ Native SDK hook usage is confined to:
265
+ 1. Hydrator components (sole subscribers to wagmi/adapter/dapp-kit state).
266
+ 2. Actions components (call wagmi functions inside registered closures).
267
+ 3. `useEvmSwitchChain` (special case — wagmi's `useSwitchChain` and `useAccount` for the chain-mismatch check).
268
+ 4. `useEthereumChainId` (Injective MetaMask special case).
269
+
270
+ If you find yourself reaching for `useAccount` in app code, prefer `useXAccount` — same data, chain-agnostic.
271
+
272
+ ---
273
+
274
+ ## Bridge to wallet-sdk-core
275
+
276
+ `@sodax/wallet-sdk-react` produces typed `IXxxWalletProvider` instances from `@sodax/wallet-sdk-core` and stores them in `walletProviders`. Consumers retrieve them via `useWalletProvider()`.
277
+
278
+ - **Provider-managed chains**: Hydrator builds e.g. `new EvmWalletProvider({ walletClient, publicClient, defaults })` from wagmi's clients on every relevant change. Memoized to avoid spurious re-renders.
279
+ - **Non-provider chains**: `chainRegistry.<chain>.createWalletProvider(service, getStore)` is invoked as a side-effect of `setXConnection()` — when the user connects, the provider materializes immediately.
280
+
281
+ The provider classes live in `@sodax/wallet-sdk-core`, **not** here. This package's responsibility is wiring up React state + hydration; the providers themselves are framework-agnostic and can be constructed directly in Node.js scripts (see [`packages/sdk/docs/WALLET_PROVIDERS.md`](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/WALLET_PROVIDERS.md)).
282
+
283
+ ---
284
+
285
+ ## Related docs
286
+
287
+ - [Configure SodaxWalletProvider](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONFIGURE_PROVIDER.md) — config schema for the lifecycle described here
288
+ - [Connect Flow](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONNECT_FLOW.md) — consumer-facing API
289
+ - [Wallet Provider Bridge](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/WALLET_PROVIDER_BRIDGE.md) — `useWalletProvider` consumes the `walletProviders` slice
290
+ - [Adding a New Chain](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/ADDING_A_NEW_CHAIN.md) — chain-onboarding workflow
291
+ - [Sub-path Exports](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/SUB_PATH_EXPORTS.md) — barrel vs deep-import boundary
@@ -0,0 +1,267 @@
1
+ # Batch Operations
2
+
3
+ `useBatchConnect` and `useBatchDisconnect` orchestrate multi-chain wallet operations sequentially using **wallet brand identifiers** (e.g. `'hana'`, `'phantom'`) instead of individual connectors. Pass an identifier and the hooks discover every chain that wallet supports across the registry, then connect/disconnect them in order.
4
+
5
+ Sequential by design — many extensions share popup singletons, so parallel attempts would race a single popup. Errors are collected, not thrown — `run()` always resolves.
6
+
7
+ The pure helpers ([`resolveBatchTargets`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/src/hooks/useBatchConnect.ts), [`runBatchConnect`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/src/hooks/useBatchConnect.ts), [`resolveDisconnectTargets`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/src/hooks/useBatchDisconnect.ts), [`runBatchDisconnect`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/src/hooks/useBatchDisconnect.ts)) are exported for testability outside React.
8
+
9
+ ## Table of contents
10
+
11
+ 1. [Identifier matching](#identifier-matching)
12
+ 2. [`useBatchConnect`](#usebatchconnect)
13
+ 3. [`useBatchDisconnect`](#usebatchdisconnect)
14
+ 4. [Progress events](#progress-events)
15
+ 5. [Status lifecycle and concurrency](#status-lifecycle-and-concurrency)
16
+ 6. [Wallet install detection — `useIsWalletInstalled`](#wallet-install-detection--useiswalletinstalled)
17
+ 7. [When to use the lower-level hooks instead](#when-to-use-the-lower-level-hooks-instead)
18
+
19
+ ---
20
+
21
+ ## Identifier matching
22
+
23
+ Both batch hooks (and `useIsWalletInstalled`) take a `connectors: readonly string[]` field. Each entry is a **wallet brand identifier** matched **case-insensitive substring** against `connector.id` and `connector.name`:
24
+
25
+ | Identifier | Matches connectors |
26
+ |------------|--------------------|
27
+ | `'hana'` | Hana on EVM (`io.havah.hana`), Hana on ICON (`hana`), Hana on Sui, Hana on Stellar… |
28
+ | `'phantom'` | Phantom on Solana (`phantom`), Phantom on EVM (`app.phantom`) |
29
+ | `'metamask'` | MetaMask on EVM (`io.metamask`), Injective MetaMask connector |
30
+ | `'xverse'` | Xverse on Bitcoin (`xverse`) |
31
+
32
+ Earlier identifiers in the array win **per chain**. Use this for fallback chains:
33
+
34
+ ```typescript
35
+ // Prefer Hana, fall back to Phantom on chains where Hana isn't available
36
+ const { run } = useBatchConnect({ connectors: ['hana', 'phantom'] });
37
+ ```
38
+
39
+ The matcher is implemented by [`matchesConnectorIdentifier`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/src/utils/matchConnectorIdentifier.ts) — pure, case-insensitive `includes`.
40
+
41
+ To target a specific connector (not a brand), bypass this API and use `useXConnectors({ xChainType }).find(c => c.id === '...')` + `useXConnect` directly.
42
+
43
+ ---
44
+
45
+ ## `useBatchConnect`
46
+
47
+ Connect every chain where one of the supplied identifiers matches an installed connector. Sequential, never throws, dedupes concurrent runs.
48
+
49
+ ```tsx
50
+ import { useBatchConnect } from '@sodax/wallet-sdk-react';
51
+
52
+ function ConnectAllHana() {
53
+ const { run, status, result, reset } = useBatchConnect({
54
+ connectors: ['hana'],
55
+ skipConnected: true, // skip chains already connected at run() time
56
+ onProgress: event => {
57
+ console.log(`[batch] ${event.chainType}: ${event.outcome}`);
58
+ },
59
+ });
60
+
61
+ return (
62
+ <div>
63
+ <button onClick={run} disabled={status === 'running'}>
64
+ {status === 'running' ? 'Connecting all Hana chains…' : 'Connect with Hana'}
65
+ </button>
66
+ {status === 'done' && result && (
67
+ <div>
68
+ <p>{result.successful.length} connected</p>
69
+ <p>{result.failed.length} failed</p>
70
+ <p>{result.skipped.length} skipped</p>
71
+ <button onClick={reset}>Reset</button>
72
+ </div>
73
+ )}
74
+ </div>
75
+ );
76
+ }
77
+ ```
78
+
79
+ ### Options
80
+
81
+ | Field | Type | Effect |
82
+ |-------|------|--------|
83
+ | `connectors` | `readonly string[]` | Identifiers (required, non-empty) |
84
+ | `skipConnected` | `boolean` | Skip chains already holding an account at `run()` time. Default `false` |
85
+ | `onProgress` | `(event) => void` | Per-target progress event — see [Progress events](#progress-events) |
86
+
87
+ ### Result shape
88
+
89
+ ```typescript
90
+ type BatchConnectResult = {
91
+ successful: ChainType[];
92
+ failed: Array<{ chainType: ChainType; error: Error }>;
93
+ skipped: ChainType[];
94
+ };
95
+ ```
96
+
97
+ `run()` returns `Promise<BatchConnectResult>` — never rejects. The same result is also exposed on the hook's `result` field after the batch settles.
98
+
99
+ ---
100
+
101
+ ## `useBatchDisconnect`
102
+
103
+ Mirror of `useBatchConnect` for disconnect:
104
+
105
+ ```tsx
106
+ import { useBatchDisconnect } from '@sodax/wallet-sdk-react';
107
+
108
+ function DisconnectAll() {
109
+ const { run, status, result } = useBatchDisconnect();
110
+ // No `connectors` → disconnect every currently-connected chain regardless of wallet
111
+
112
+ return (
113
+ <button onClick={run} disabled={status === 'running'}>
114
+ {status === 'running' ? 'Disconnecting…' : 'Disconnect all chains'}
115
+ </button>
116
+ );
117
+ }
118
+
119
+ function DisconnectHanaOnly() {
120
+ const { run } = useBatchDisconnect({ connectors: ['hana'] });
121
+ // Only disconnect chains whose CURRENTLY ACTIVE connector matches 'hana'
122
+ return <button onClick={run}>Disconnect Hana</button>;
123
+ }
124
+ ```
125
+
126
+ ### Options
127
+
128
+ | Field | Type | Effect |
129
+ |-------|------|--------|
130
+ | `connectors` | `readonly string[]` (optional) | Filter chains whose **active** connector matches one of these identifiers. Omit to disconnect all currently-connected chains |
131
+ | `onProgress` | `(event) => void` | Per-target progress event |
132
+
133
+ ### Scope difference vs `useBatchConnect`
134
+
135
+ `useBatchConnect.connectors` is **mandatory** — it picks **which connector** to use on each chain.
136
+
137
+ `useBatchDisconnect.connectors` is **optional** — it filters **which chains** to disconnect by checking the **currently active** connector against the identifiers. Chains where the active connector doesn't match are left untouched.
138
+
139
+ ```typescript
140
+ // Disconnect every chain (regardless of which wallet is active)
141
+ useBatchDisconnect();
142
+
143
+ // Disconnect only chains where Hana is active
144
+ useBatchDisconnect({ connectors: ['hana'] });
145
+
146
+ // Disconnect chains where either Hana or Xverse is active
147
+ useBatchDisconnect({ connectors: ['hana', 'xverse'] });
148
+ ```
149
+
150
+ ### Result shape
151
+
152
+ ```typescript
153
+ type BatchDisconnectResult = {
154
+ successful: ChainType[];
155
+ failed: Array<{ chainType: ChainType; error: Error }>;
156
+ };
157
+ ```
158
+
159
+ No `skipped` field — disconnect doesn't have a skip semantic.
160
+
161
+ ---
162
+
163
+ ## Progress events
164
+
165
+ Both hooks accept `onProgress` for live UI updates without waiting for `run()` to resolve:
166
+
167
+ ```typescript
168
+ const { run } = useBatchConnect({
169
+ connectors: ['hana'],
170
+ onProgress: event => {
171
+ switch (event.outcome) {
172
+ case 'success':
173
+ toast(`✓ Connected ${event.chainType}`);
174
+ break;
175
+ case 'failure':
176
+ toast.error(`✗ ${event.chainType}: ${event.error.message}`);
177
+ break;
178
+ case 'skipped':
179
+ toast(`⏭ ${event.chainType} already connected`);
180
+ break;
181
+ }
182
+ },
183
+ });
184
+ ```
185
+
186
+ | Hook | Event variants |
187
+ |------|----------------|
188
+ | `useBatchConnect` | `{ outcome: 'success' \| 'failure' \| 'skipped', chainType, error? }` |
189
+ | `useBatchDisconnect` | `{ outcome: 'success' \| 'failure', chainType, error? }` |
190
+
191
+ **`onProgress` is isolated from the batch result** — a throwing callback is caught and logged via `console.error`. The batch continues normally. This is intentional: a render-time crash in toast code shouldn't abort a multi-chain disconnect halfway through.
192
+
193
+ `onProgress` is read from a ref each time, so passing an inline arrow function is safe — `run` won't be rebuilt every render.
194
+
195
+ ---
196
+
197
+ ## Status lifecycle and concurrency
198
+
199
+ | `status` | Meaning |
200
+ |----------|---------|
201
+ | `'idle'` | Initial / after `reset()` |
202
+ | `'running'` | A `run()` is in flight |
203
+ | `'done'` | The last `run()` settled — `result` is populated |
204
+
205
+ **Concurrent `run()` calls are deduped** — the second call returns the existing in-flight promise. This protects extensions that share popup singletons:
206
+
207
+ ```typescript
208
+ const { run } = useBatchConnect({ connectors: ['hana'] });
209
+ // Two calls — only one batch executes; both promises resolve to the same result.
210
+ const [a, b] = await Promise.all([run(), run()]);
211
+ ```
212
+
213
+ **`reset()` does NOT abort an in-flight batch** — there is no cancellation signal. It only clears the observable `status` / `result`. When the in-flight batch eventually resolves, `status` flips back to `'done'` and `result` re-populates. Typical usage: call `reset()` only after `status === 'done'`.
214
+
215
+ ---
216
+
217
+ ## Wallet install detection — `useIsWalletInstalled`
218
+
219
+ Companion read hook that uses the same identifier matching to detect whether a wallet brand is installed for any (or specific) chain.
220
+
221
+ ```typescript
222
+ import { useIsWalletInstalled } from '@sodax/wallet-sdk-react';
223
+
224
+ // True if any Hana variant is installed across any enabled chain
225
+ const isHanaInstalled = useIsWalletInstalled({ connectors: ['hana'] });
226
+
227
+ // True if any wallet is installed for the given chain
228
+ const hasBitcoinWallet = useIsWalletInstalled({ chainType: 'BITCOIN' });
229
+
230
+ // AND filter — Hana specifically on EVM
231
+ const hanaOnEvm = useIsWalletInstalled({ connectors: ['hana'], chainType: 'EVM' });
232
+ ```
233
+
234
+ The options union enforces at the type level that **at least one** of `connectors` / `chainType` is present — calling with `{}` returns `false` plus a one-time console warning. An empty `connectors: []` is treated as explicit "match nothing".
235
+
236
+ Use this hook to gate an `useBatchConnect` button:
237
+
238
+ ```tsx
239
+ const isInstalled = useIsWalletInstalled({ connectors: ['hana'] });
240
+ const { run } = useBatchConnect({ connectors: ['hana'] });
241
+
242
+ return isInstalled ? (
243
+ <button onClick={run}>Connect Hana</button>
244
+ ) : (
245
+ <a href="https://hana-wallet.com">Install Hana</a>
246
+ );
247
+ ```
248
+
249
+ ---
250
+
251
+ ## When to use the lower-level hooks instead
252
+
253
+ Reach for `useXConnect` / `useXDisconnect` directly when:
254
+
255
+ - **You need a specific connector**, not a brand. The identifier matcher is intentionally fuzzy; `useXConnectors` lets you pick by exact `id`.
256
+ - **Order is user-driven**, not registry-driven. The batch hooks iterate in the chain registry's order; if the user picked an explicit sequence in your UI, drive `useXConnect` from that sequence yourself.
257
+ - **You need cancellation**. Batch hooks have no cancel signal — drive `useXConnect` directly inside an `AbortController`-aware controller if you need to bail out.
258
+ - **You only have one chain to operate on**. The batch concurrency guard adds no value for single-chain flows.
259
+
260
+ ---
261
+
262
+ ## Related docs
263
+
264
+ - [Connect Flow](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONNECT_FLOW.md) — `useXConnect` / `useXDisconnect` underlying primitives
265
+ - [Wallet Modal](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/WALLET_MODAL.md) — multi-chain modal that internally drives `useBatchConnect` for "connect all" flows
266
+ - [Chain Detection](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CHAIN_DETECTION.md) — `useChainGroups`, `useConnectedChains` for surfacing batch results
267
+ - [Connectors](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONNECTORS.md) — `IXConnector` shape, exact `id` matching for fine-grained control