@multiplatform.one/web3 7.24.0 → 7.26.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.
Files changed (363) hide show
  1. package/README.md +33 -0
  2. package/dist/cjs/bridge/AuthBridge.cjs +124 -0
  3. package/dist/cjs/bridge/AuthBridge.native.js +186 -0
  4. package/dist/cjs/bridge/AuthBridge.native.js.map +1 -0
  5. package/dist/cjs/bridge/BridgeClient.cjs +12 -9
  6. package/dist/cjs/bridge/BridgeClient.native.js +17 -12
  7. package/dist/cjs/bridge/BridgeClient.native.js.map +1 -1
  8. package/dist/cjs/bridge/BridgeHost.cjs +11 -3
  9. package/dist/cjs/bridge/BridgeHost.native.js +11 -3
  10. package/dist/cjs/bridge/BridgeHost.native.js.map +1 -1
  11. package/dist/cjs/bridge/GpgBridge.cjs +61 -0
  12. package/dist/cjs/bridge/GpgBridge.native.js +111 -0
  13. package/dist/cjs/bridge/GpgBridge.native.js.map +1 -0
  14. package/dist/cjs/bridge/SuiBridge.cjs +51 -0
  15. package/dist/cjs/bridge/SuiBridge.native.js +99 -0
  16. package/dist/cjs/bridge/SuiBridge.native.js.map +1 -0
  17. package/dist/cjs/bridge/authTypes.cjs +28 -0
  18. package/dist/cjs/bridge/authTypes.native.js +31 -0
  19. package/dist/cjs/bridge/authTypes.native.js.map +1 -0
  20. package/dist/cjs/bridge/core.cjs +65 -0
  21. package/dist/cjs/bridge/core.native.js +68 -0
  22. package/dist/cjs/bridge/core.native.js.map +1 -0
  23. package/dist/cjs/bridge/hosting.cjs +91 -0
  24. package/dist/cjs/bridge/hosting.native.js +117 -0
  25. package/dist/cjs/bridge/hosting.native.js.map +1 -0
  26. package/dist/cjs/bridge/index.cjs +3 -17
  27. package/dist/cjs/bridge/index.native.js +2 -16
  28. package/dist/cjs/bridge/index.native.js.map +1 -1
  29. package/dist/cjs/bridge/nativeHost.cjs +91 -0
  30. package/dist/cjs/bridge/nativeHost.native.js +118 -0
  31. package/dist/cjs/bridge/nativeHost.native.js.map +1 -0
  32. package/dist/cjs/bridge/protocol.cjs +30 -1
  33. package/dist/cjs/bridge/protocol.native.js +147 -1
  34. package/dist/cjs/bridge/protocol.native.js.map +1 -1
  35. package/dist/cjs/core/family.cjs +88 -0
  36. package/dist/cjs/core/family.native.js +115 -0
  37. package/dist/cjs/core/family.native.js.map +1 -0
  38. package/dist/cjs/core/index.cjs +41 -0
  39. package/dist/cjs/core/index.native.js +44 -0
  40. package/dist/cjs/core/index.native.js.map +1 -0
  41. package/dist/cjs/core/message.cjs +89 -0
  42. package/dist/cjs/core/message.native.js +104 -0
  43. package/dist/cjs/core/message.native.js.map +1 -0
  44. package/dist/cjs/core/names.cjs +51 -0
  45. package/dist/cjs/core/names.native.js +95 -0
  46. package/dist/cjs/core/names.native.js.map +1 -0
  47. package/dist/cjs/core/react.cjs +48 -0
  48. package/dist/cjs/core/react.native.js +52 -0
  49. package/dist/cjs/core/react.native.js.map +1 -0
  50. package/dist/cjs/core/store.cjs +450 -0
  51. package/dist/cjs/core/store.native.js +637 -0
  52. package/dist/cjs/core/store.native.js.map +1 -0
  53. package/dist/cjs/core/types.cjs +18 -0
  54. package/dist/cjs/core/types.native.js +21 -0
  55. package/dist/cjs/core/types.native.js.map +1 -0
  56. package/dist/cjs/index.cjs +3 -1
  57. package/dist/cjs/index.native.js +2 -0
  58. package/dist/cjs/index.native.js.map +1 -1
  59. package/dist/cjs/providers/WalletProvider.cjs +2 -0
  60. package/dist/cjs/providers/WalletProvider.native.js +123 -90
  61. package/dist/cjs/providers/WalletProvider.native.js.map +1 -1
  62. package/dist/cjs/providers/index.cjs +1 -0
  63. package/dist/cjs/providers/index.native.js +1 -0
  64. package/dist/cjs/providers/index.native.js.map +1 -1
  65. package/dist/cjs/providers/native/deepLink.cjs +40 -23
  66. package/dist/cjs/providers/native/deepLink.native.js +43 -25
  67. package/dist/cjs/providers/native/deepLink.native.js.map +1 -1
  68. package/dist/cjs/providers/native/inject.cjs +2 -4
  69. package/dist/cjs/providers/native/inject.native.js +2 -4
  70. package/dist/cjs/providers/native/inject.native.js.map +1 -1
  71. package/dist/cjs/providers/walletBridge.cjs +35 -0
  72. package/dist/cjs/providers/walletBridge.native.js +38 -0
  73. package/dist/cjs/providers/walletBridge.native.js.map +1 -0
  74. package/dist/cjs/providers/walletConfig.native.js.map +1 -1
  75. package/dist/esm/bridge/AuthBridge.mjs +99 -0
  76. package/dist/esm/bridge/AuthBridge.mjs.map +1 -0
  77. package/dist/esm/bridge/AuthBridge.native.js +158 -0
  78. package/dist/esm/bridge/AuthBridge.native.js.map +1 -0
  79. package/dist/esm/bridge/BridgeClient.mjs +13 -10
  80. package/dist/esm/bridge/BridgeClient.mjs.map +1 -1
  81. package/dist/esm/bridge/BridgeClient.native.js +18 -13
  82. package/dist/esm/bridge/BridgeClient.native.js.map +1 -1
  83. package/dist/esm/bridge/BridgeHost.mjs +12 -4
  84. package/dist/esm/bridge/BridgeHost.mjs.map +1 -1
  85. package/dist/esm/bridge/BridgeHost.native.js +12 -4
  86. package/dist/esm/bridge/BridgeHost.native.js.map +1 -1
  87. package/dist/esm/bridge/GpgBridge.mjs +36 -0
  88. package/dist/esm/bridge/GpgBridge.mjs.map +1 -0
  89. package/dist/esm/bridge/GpgBridge.native.js +83 -0
  90. package/dist/esm/bridge/GpgBridge.native.js.map +1 -0
  91. package/dist/esm/bridge/SuiBridge.mjs +26 -0
  92. package/dist/esm/bridge/SuiBridge.mjs.map +1 -0
  93. package/dist/esm/bridge/SuiBridge.native.js +71 -0
  94. package/dist/esm/bridge/SuiBridge.native.js.map +1 -0
  95. package/dist/esm/bridge/authTypes.mjs +3 -0
  96. package/dist/esm/bridge/authTypes.mjs.map +1 -0
  97. package/dist/esm/bridge/authTypes.native.js +3 -0
  98. package/dist/esm/bridge/authTypes.native.js.map +1 -0
  99. package/dist/esm/bridge/core.mjs +17 -0
  100. package/dist/esm/bridge/core.mjs.map +1 -0
  101. package/dist/esm/bridge/core.native.js +17 -0
  102. package/dist/esm/bridge/core.native.js.map +1 -0
  103. package/dist/esm/bridge/hosting.mjs +62 -0
  104. package/dist/esm/bridge/hosting.mjs.map +1 -0
  105. package/dist/esm/bridge/hosting.native.js +85 -0
  106. package/dist/esm/bridge/hosting.native.js.map +1 -0
  107. package/dist/esm/bridge/index.mjs +2 -9
  108. package/dist/esm/bridge/index.mjs.map +1 -1
  109. package/dist/esm/bridge/index.native.js +2 -9
  110. package/dist/esm/bridge/index.native.js.map +1 -1
  111. package/dist/esm/bridge/nativeHost.mjs +65 -0
  112. package/dist/esm/bridge/nativeHost.mjs.map +1 -0
  113. package/dist/esm/bridge/nativeHost.native.js +89 -0
  114. package/dist/esm/bridge/nativeHost.native.js.map +1 -0
  115. package/dist/esm/bridge/protocol.mjs +28 -2
  116. package/dist/esm/bridge/protocol.mjs.map +1 -1
  117. package/dist/esm/bridge/protocol.native.js +145 -2
  118. package/dist/esm/bridge/protocol.native.js.map +1 -1
  119. package/dist/esm/core/family.mjs +62 -0
  120. package/dist/esm/core/family.mjs.map +1 -0
  121. package/dist/esm/core/family.native.js +86 -0
  122. package/dist/esm/core/family.native.js.map +1 -0
  123. package/dist/esm/core/index.mjs +7 -0
  124. package/dist/esm/core/index.mjs.map +1 -0
  125. package/dist/esm/core/index.native.js +7 -0
  126. package/dist/esm/core/index.native.js.map +1 -0
  127. package/dist/esm/core/message.mjs +63 -0
  128. package/dist/esm/core/message.mjs.map +1 -0
  129. package/dist/esm/core/message.native.js +75 -0
  130. package/dist/esm/core/message.native.js.map +1 -0
  131. package/dist/esm/core/names.mjs +25 -0
  132. package/dist/esm/core/names.mjs.map +1 -0
  133. package/dist/esm/core/names.native.js +66 -0
  134. package/dist/esm/core/names.native.js.map +1 -0
  135. package/dist/esm/core/react.mjs +21 -0
  136. package/dist/esm/core/react.mjs.map +1 -0
  137. package/dist/esm/core/react.native.js +22 -0
  138. package/dist/esm/core/react.native.js.map +1 -0
  139. package/dist/esm/core/store.mjs +424 -0
  140. package/dist/esm/core/store.mjs.map +1 -0
  141. package/dist/esm/core/store.native.js +608 -0
  142. package/dist/esm/core/store.native.js.map +1 -0
  143. package/dist/esm/core/types.mjs +2 -0
  144. package/dist/esm/core/types.mjs.map +1 -0
  145. package/dist/esm/core/types.native.js +2 -0
  146. package/dist/esm/core/types.native.js.map +1 -0
  147. package/dist/esm/index.mjs +3 -2
  148. package/dist/esm/index.mjs.map +1 -1
  149. package/dist/esm/index.native.js +3 -2
  150. package/dist/esm/index.native.js.map +1 -1
  151. package/dist/esm/providers/WalletProvider.mjs +2 -1
  152. package/dist/esm/providers/WalletProvider.mjs.map +1 -1
  153. package/dist/esm/providers/WalletProvider.native.js +125 -93
  154. package/dist/esm/providers/WalletProvider.native.js.map +1 -1
  155. package/dist/esm/providers/index.mjs +2 -2
  156. package/dist/esm/providers/index.mjs.map +1 -1
  157. package/dist/esm/providers/index.native.js +2 -2
  158. package/dist/esm/providers/index.native.js.map +1 -1
  159. package/dist/esm/providers/native/deepLink.mjs +36 -24
  160. package/dist/esm/providers/native/deepLink.mjs.map +1 -1
  161. package/dist/esm/providers/native/deepLink.native.js +39 -26
  162. package/dist/esm/providers/native/deepLink.native.js.map +1 -1
  163. package/dist/esm/providers/native/inject.mjs +1 -3
  164. package/dist/esm/providers/native/inject.mjs.map +1 -1
  165. package/dist/esm/providers/native/inject.native.js +1 -3
  166. package/dist/esm/providers/native/inject.native.js.map +1 -1
  167. package/dist/esm/providers/walletBridge.mjs +9 -0
  168. package/dist/esm/providers/walletBridge.mjs.map +1 -0
  169. package/dist/esm/providers/walletBridge.native.js +9 -0
  170. package/dist/esm/providers/walletBridge.native.js.map +1 -0
  171. package/dist/esm/providers/walletConfig.mjs.map +1 -1
  172. package/dist/esm/providers/walletConfig.native.js.map +1 -1
  173. package/dist/jsx/bridge/AuthBridge.mjs +99 -0
  174. package/dist/jsx/bridge/AuthBridge.mjs.map +1 -0
  175. package/dist/jsx/bridge/AuthBridge.native.js +186 -0
  176. package/dist/jsx/bridge/AuthBridge.native.js.map +1 -0
  177. package/dist/jsx/bridge/BridgeClient.mjs +13 -10
  178. package/dist/jsx/bridge/BridgeClient.mjs.map +1 -1
  179. package/dist/jsx/bridge/BridgeClient.native.js +17 -12
  180. package/dist/jsx/bridge/BridgeClient.native.js.map +1 -1
  181. package/dist/jsx/bridge/BridgeHost.mjs +12 -4
  182. package/dist/jsx/bridge/BridgeHost.mjs.map +1 -1
  183. package/dist/jsx/bridge/BridgeHost.native.js +11 -3
  184. package/dist/jsx/bridge/BridgeHost.native.js.map +1 -1
  185. package/dist/jsx/bridge/GpgBridge.mjs +36 -0
  186. package/dist/jsx/bridge/GpgBridge.mjs.map +1 -0
  187. package/dist/jsx/bridge/GpgBridge.native.js +111 -0
  188. package/dist/jsx/bridge/GpgBridge.native.js.map +1 -0
  189. package/dist/jsx/bridge/SuiBridge.mjs +26 -0
  190. package/dist/jsx/bridge/SuiBridge.mjs.map +1 -0
  191. package/dist/jsx/bridge/SuiBridge.native.js +99 -0
  192. package/dist/jsx/bridge/SuiBridge.native.js.map +1 -0
  193. package/dist/jsx/bridge/authTypes.mjs +3 -0
  194. package/dist/jsx/bridge/authTypes.mjs.map +1 -0
  195. package/dist/jsx/bridge/authTypes.native.js +31 -0
  196. package/dist/jsx/bridge/authTypes.native.js.map +1 -0
  197. package/dist/jsx/bridge/core.mjs +17 -0
  198. package/dist/jsx/bridge/core.mjs.map +1 -0
  199. package/dist/jsx/bridge/core.native.js +68 -0
  200. package/dist/jsx/bridge/core.native.js.map +1 -0
  201. package/dist/jsx/bridge/hosting.mjs +62 -0
  202. package/dist/jsx/bridge/hosting.mjs.map +1 -0
  203. package/dist/jsx/bridge/hosting.native.js +117 -0
  204. package/dist/jsx/bridge/hosting.native.js.map +1 -0
  205. package/dist/jsx/bridge/index.mjs +2 -9
  206. package/dist/jsx/bridge/index.mjs.map +1 -1
  207. package/dist/jsx/bridge/index.native.js +2 -16
  208. package/dist/jsx/bridge/index.native.js.map +1 -1
  209. package/dist/jsx/bridge/nativeHost.mjs +65 -0
  210. package/dist/jsx/bridge/nativeHost.mjs.map +1 -0
  211. package/dist/jsx/bridge/nativeHost.native.js +118 -0
  212. package/dist/jsx/bridge/nativeHost.native.js.map +1 -0
  213. package/dist/jsx/bridge/protocol.mjs +28 -2
  214. package/dist/jsx/bridge/protocol.mjs.map +1 -1
  215. package/dist/jsx/bridge/protocol.native.js +147 -1
  216. package/dist/jsx/bridge/protocol.native.js.map +1 -1
  217. package/dist/jsx/core/family.mjs +62 -0
  218. package/dist/jsx/core/family.mjs.map +1 -0
  219. package/dist/jsx/core/family.native.js +115 -0
  220. package/dist/jsx/core/family.native.js.map +1 -0
  221. package/dist/jsx/core/index.mjs +7 -0
  222. package/dist/jsx/core/index.mjs.map +1 -0
  223. package/dist/jsx/core/index.native.js +44 -0
  224. package/dist/jsx/core/index.native.js.map +1 -0
  225. package/dist/jsx/core/message.mjs +63 -0
  226. package/dist/jsx/core/message.mjs.map +1 -0
  227. package/dist/jsx/core/message.native.js +104 -0
  228. package/dist/jsx/core/message.native.js.map +1 -0
  229. package/dist/jsx/core/names.mjs +25 -0
  230. package/dist/jsx/core/names.mjs.map +1 -0
  231. package/dist/jsx/core/names.native.js +95 -0
  232. package/dist/jsx/core/names.native.js.map +1 -0
  233. package/dist/jsx/core/react.mjs +21 -0
  234. package/dist/jsx/core/react.mjs.map +1 -0
  235. package/dist/jsx/core/react.native.js +52 -0
  236. package/dist/jsx/core/react.native.js.map +1 -0
  237. package/dist/jsx/core/store.mjs +424 -0
  238. package/dist/jsx/core/store.mjs.map +1 -0
  239. package/dist/jsx/core/store.native.js +637 -0
  240. package/dist/jsx/core/store.native.js.map +1 -0
  241. package/dist/jsx/core/types.mjs +2 -0
  242. package/dist/jsx/core/types.mjs.map +1 -0
  243. package/dist/jsx/core/types.native.js +21 -0
  244. package/dist/jsx/core/types.native.js.map +1 -0
  245. package/dist/jsx/index.js +3 -2
  246. package/dist/jsx/index.js.map +1 -1
  247. package/dist/jsx/index.mjs +3 -2
  248. package/dist/jsx/index.mjs.map +1 -1
  249. package/dist/jsx/index.native.js +2 -0
  250. package/dist/jsx/index.native.js.map +1 -1
  251. package/dist/jsx/providers/WalletProvider.mjs +2 -1
  252. package/dist/jsx/providers/WalletProvider.mjs.map +1 -1
  253. package/dist/jsx/providers/WalletProvider.native.js +123 -90
  254. package/dist/jsx/providers/WalletProvider.native.js.map +1 -1
  255. package/dist/jsx/providers/index.mjs +2 -2
  256. package/dist/jsx/providers/index.mjs.map +1 -1
  257. package/dist/jsx/providers/index.native.js +1 -0
  258. package/dist/jsx/providers/index.native.js.map +1 -1
  259. package/dist/jsx/providers/native/deepLink.mjs +36 -24
  260. package/dist/jsx/providers/native/deepLink.mjs.map +1 -1
  261. package/dist/jsx/providers/native/deepLink.native.js +43 -25
  262. package/dist/jsx/providers/native/deepLink.native.js.map +1 -1
  263. package/dist/jsx/providers/native/inject.mjs +1 -3
  264. package/dist/jsx/providers/native/inject.mjs.map +1 -1
  265. package/dist/jsx/providers/native/inject.native.js +2 -4
  266. package/dist/jsx/providers/native/inject.native.js.map +1 -1
  267. package/dist/jsx/providers/walletBridge.mjs +9 -0
  268. package/dist/jsx/providers/walletBridge.mjs.map +1 -0
  269. package/dist/jsx/providers/walletBridge.native.js +38 -0
  270. package/dist/jsx/providers/walletBridge.native.js.map +1 -0
  271. package/dist/jsx/providers/walletConfig.mjs.map +1 -1
  272. package/dist/jsx/providers/walletConfig.native.js.map +1 -1
  273. package/docs/bridge-protocol.md +485 -0
  274. package/package.json +14 -6
  275. package/src/bridge/AuthBridge.ts +146 -0
  276. package/src/bridge/BridgeClient.spec.ts +12 -0
  277. package/src/bridge/BridgeClient.ts +15 -10
  278. package/src/bridge/BridgeHost.spec.ts +20 -4
  279. package/src/bridge/BridgeHost.ts +16 -5
  280. package/src/bridge/GpgBridge.ts +49 -0
  281. package/src/bridge/SuiBridge.ts +34 -0
  282. package/src/bridge/authTypes.ts +76 -0
  283. package/src/bridge/core.ts +42 -0
  284. package/src/bridge/hosting.spec.ts +98 -0
  285. package/src/bridge/hosting.ts +99 -0
  286. package/src/bridge/index.ts +1 -8
  287. package/src/bridge/nativeHost.spec.ts +165 -0
  288. package/src/bridge/nativeHost.ts +120 -0
  289. package/src/bridge/protocol.spec.ts +24 -1
  290. package/src/bridge/protocol.ts +44 -1
  291. package/src/core/family.spec.ts +117 -0
  292. package/src/core/family.ts +103 -0
  293. package/src/core/imports.spec.ts +82 -0
  294. package/src/core/index.ts +47 -0
  295. package/src/core/message.spec.ts +178 -0
  296. package/src/core/message.ts +119 -0
  297. package/src/core/names.ts +29 -0
  298. package/src/core/react.spec.tsx +75 -0
  299. package/src/core/react.ts +34 -0
  300. package/src/core/store.spec.ts +890 -0
  301. package/src/core/store.ts +558 -0
  302. package/src/core/types.ts +96 -0
  303. package/src/index.ts +6 -0
  304. package/src/providers/WalletProvider.native.tsx +139 -85
  305. package/src/providers/WalletProvider.tsx +1 -1
  306. package/src/providers/index.ts +2 -0
  307. package/src/providers/native/deepLink.spec.ts +73 -1
  308. package/src/providers/native/deepLink.ts +76 -28
  309. package/src/providers/native/inject.ts +1 -9
  310. package/src/providers/walletBridge.ts +24 -0
  311. package/src/providers/walletConfig.ts +15 -1
  312. package/types/bridge/AuthBridge.d.ts +58 -0
  313. package/types/bridge/AuthBridge.d.ts.map +1 -0
  314. package/types/bridge/BridgeClient.d.ts +3 -1
  315. package/types/bridge/BridgeClient.d.ts.map +1 -1
  316. package/types/bridge/BridgeHost.d.ts +1 -1
  317. package/types/bridge/BridgeHost.d.ts.map +1 -1
  318. package/types/bridge/GpgBridge.d.ts +23 -0
  319. package/types/bridge/GpgBridge.d.ts.map +1 -0
  320. package/types/bridge/SuiBridge.d.ts +18 -0
  321. package/types/bridge/SuiBridge.d.ts.map +1 -0
  322. package/types/bridge/authTypes.d.ts +71 -0
  323. package/types/bridge/authTypes.d.ts.map +1 -0
  324. package/types/bridge/core.d.ts +16 -0
  325. package/types/bridge/core.d.ts.map +1 -0
  326. package/types/bridge/hosting.d.ts +34 -0
  327. package/types/bridge/hosting.d.ts.map +1 -0
  328. package/types/bridge/index.d.ts +1 -8
  329. package/types/bridge/index.d.ts.map +1 -1
  330. package/types/bridge/nativeHost.d.ts +55 -0
  331. package/types/bridge/nativeHost.d.ts.map +1 -0
  332. package/types/bridge/protocol.d.ts +34 -1
  333. package/types/bridge/protocol.d.ts.map +1 -1
  334. package/types/core/family.d.ts +44 -0
  335. package/types/core/family.d.ts.map +1 -0
  336. package/types/core/index.d.ts +7 -0
  337. package/types/core/index.d.ts.map +1 -0
  338. package/types/core/message.d.ts +44 -0
  339. package/types/core/message.d.ts.map +1 -0
  340. package/types/core/names.d.ts +7 -0
  341. package/types/core/names.d.ts.map +1 -0
  342. package/types/core/react.d.ts +14 -0
  343. package/types/core/react.d.ts.map +1 -0
  344. package/types/core/store.d.ts +64 -0
  345. package/types/core/store.d.ts.map +1 -0
  346. package/types/core/types.d.ts +88 -0
  347. package/types/core/types.d.ts.map +1 -0
  348. package/types/index.d.ts +2 -1
  349. package/types/index.d.ts.map +1 -1
  350. package/types/providers/WalletProvider.d.ts +1 -0
  351. package/types/providers/WalletProvider.d.ts.map +1 -1
  352. package/types/providers/WalletProvider.native.d.ts +2 -1
  353. package/types/providers/WalletProvider.native.d.ts.map +1 -1
  354. package/types/providers/index.d.ts +1 -1
  355. package/types/providers/index.d.ts.map +1 -1
  356. package/types/providers/native/deepLink.d.ts +29 -4
  357. package/types/providers/native/deepLink.d.ts.map +1 -1
  358. package/types/providers/native/inject.d.ts +1 -7
  359. package/types/providers/native/inject.d.ts.map +1 -1
  360. package/types/providers/walletBridge.d.ts +17 -0
  361. package/types/providers/walletBridge.d.ts.map +1 -0
  362. package/types/providers/walletConfig.d.ts +18 -1
  363. package/types/providers/walletConfig.d.ts.map +1 -1
@@ -0,0 +1,890 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { deferred, memoryStorage, testDriver } from "../../tests/core/testDriver";
3
+ import { defineWalletFamily } from "./family";
4
+ import { createConnectionStore, defaultConnectionStorageKey } from "./store";
5
+ import type { ChainClient } from "../clients/types";
6
+ import type { ConnectionState, ConnectionStorage, DriverAccount } from "./types";
7
+
8
+ const evmAccount = { address: "0x1111111111111111111111111111111111111111", chainId: 137 };
9
+ const phantomAccount = {
10
+ address: "7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV",
11
+ chainId: "solana:mainnet",
12
+ };
13
+ const solflareAccount = {
14
+ address: "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
15
+ chainId: "solana:mainnet",
16
+ };
17
+
18
+ const message = { header: "wants you to sign in:", statement: null, chainId: null };
19
+
20
+ function setup(
21
+ storage: ConnectionStorage | null | "platform" = memoryStorage(),
22
+ closeTimeout?: number,
23
+ ) {
24
+ const eth = testDriver(
25
+ "evm",
26
+ [
27
+ { id: "metaMask", name: "MetaMask" },
28
+ { id: "coinbase", name: "Coinbase Wallet" },
29
+ ],
30
+ { metaMask: evmAccount, coinbase: evmAccount },
31
+ );
32
+ const sol = testDriver(
33
+ "solana",
34
+ [
35
+ { id: "phantom", name: "Phantom" },
36
+ { id: "solflare", name: "Solflare" },
37
+ ],
38
+ { phantom: phantomAccount, solflare: solflareAccount },
39
+ );
40
+ const makeEth = vi.fn(() => eth);
41
+ const makeSol = vi.fn(() => sol);
42
+ const store = createConnectionStore({
43
+ storage: storage === "platform" ? undefined : storage,
44
+ closeTimeout,
45
+ families: [
46
+ defineWalletFamily({
47
+ id: "eth",
48
+ label: "Ethereum",
49
+ aliases: ["evm"],
50
+ message,
51
+ driver: makeEth,
52
+ }),
53
+ defineWalletFamily({
54
+ id: "sol",
55
+ label: "Solana",
56
+ aliases: ["solana"],
57
+ message,
58
+ driver: makeSol,
59
+ }),
60
+ defineWalletFamily({ id: "gpg", label: "GPG", message, connectionless: true }),
61
+ ],
62
+ });
63
+ const seen: ConnectionState[] = [];
64
+ store.subscribe(() => seen.push(store.getState()));
65
+ return { store, eth, sol, makeEth, makeSol, seen };
66
+ }
67
+
68
+ /**
69
+ * Stores built on the same wallets, as a remount shares them: the same driver
70
+ * instances, or with `wrap`, a fresh driver per store over the same wallet.
71
+ */
72
+ function sharedDrivers(storage: ConnectionStorage = memoryStorage(), wrap = false) {
73
+ const eth = testDriver("evm", [{ id: "metaMask", name: "MetaMask" }], { metaMask: evmAccount });
74
+ const sol = testDriver(
75
+ "solana",
76
+ [
77
+ { id: "phantom", name: "Phantom" },
78
+ { id: "solflare", name: "Solflare" },
79
+ ],
80
+ { phantom: phantomAccount, solflare: solflareAccount },
81
+ );
82
+ const make = () =>
83
+ createConnectionStore({
84
+ storage,
85
+ families: [
86
+ defineWalletFamily({
87
+ id: "eth",
88
+ label: "Ethereum",
89
+ message,
90
+ driver: () => (wrap ? { ...eth } : eth),
91
+ }),
92
+ defineWalletFamily({
93
+ id: "sol",
94
+ label: "Solana",
95
+ message,
96
+ driver: () => (wrap ? { ...sol } : sol),
97
+ }),
98
+ ],
99
+ });
100
+ return { eth, sol, make };
101
+ }
102
+
103
+ const saved = (family: string, connectorId: string) => ({
104
+ [defaultConnectionStorageKey]: JSON.stringify({ family, connectorId }),
105
+ });
106
+
107
+ afterEach(() => {
108
+ vi.unstubAllGlobals();
109
+ });
110
+
111
+ describe("createConnectionStore", () => {
112
+ it("starts disconnected and asks no driver", () => {
113
+ const { store, makeEth, makeSol } = setup();
114
+ expect(store.getState()).toEqual({
115
+ status: "disconnected",
116
+ family: null,
117
+ connector: null,
118
+ address: null,
119
+ chainId: null,
120
+ client: null,
121
+ });
122
+ expect(makeEth).not.toHaveBeenCalled();
123
+ expect(makeSol).not.toHaveBeenCalled();
124
+ });
125
+
126
+ it("connect goes through connecting to connected and remembers {family, connectorId}", async () => {
127
+ const storage = memoryStorage();
128
+ const { store, sol, seen } = setup(storage);
129
+ const state = await store.connect("sol", "phantom");
130
+ expect(seen.map((entry) => entry.status)).toEqual(["connecting", "connected"]);
131
+ expect(seen[0]).toMatchObject({ family: "sol", connector: { id: "phantom", name: "Phantom" } });
132
+ expect(state).toMatchObject({
133
+ status: "connected",
134
+ family: "sol",
135
+ connector: { id: "phantom", name: "Phantom" },
136
+ address: phantomAccount.address,
137
+ chainId: "solana:mainnet",
138
+ });
139
+ expect(state.client?.address).toBe(phantomAccount.address);
140
+ expect(await state.client?.signMessage("hi")).toBe("solana:hi");
141
+ expect(sol.connect).toHaveBeenCalledWith("phantom", undefined);
142
+ expect(JSON.parse(storage.map.get(defaultConnectionStorageKey) ?? "")).toEqual({
143
+ family: "sol",
144
+ connectorId: "phantom",
145
+ });
146
+ });
147
+
148
+ it("an alias names the family and the store records the family id", async () => {
149
+ const storage = memoryStorage();
150
+ const { store } = setup(storage);
151
+ expect((await store.connect("evm", "metaMask")).family).toBe("eth");
152
+ expect(storage.map.get(defaultConnectionStorageKey)).toBe(
153
+ JSON.stringify({ family: "eth", connectorId: "metaMask" }),
154
+ );
155
+ });
156
+
157
+ it("passes connect options, such as onDisplayUri, to the driver", async () => {
158
+ const { store, eth } = setup();
159
+ const onDisplayUri = vi.fn();
160
+ await store.connect("eth", "metaMask", { onDisplayUri });
161
+ expect(eth.connect).toHaveBeenCalledWith("metaMask", { onDisplayUri });
162
+ });
163
+
164
+ it("disconnect closes the driver, clears the state and forgets the connection", async () => {
165
+ const storage = memoryStorage();
166
+ const { store, eth } = setup(storage);
167
+ await store.connect("eth", "metaMask");
168
+ await store.disconnect();
169
+ expect(eth.disconnect).toHaveBeenCalledTimes(1);
170
+ expect(store.getState().status).toBe("disconnected");
171
+ expect(store.getState().client).toBeNull();
172
+ expect(storage.map.has(defaultConnectionStorageKey)).toBe(false);
173
+ expect(eth.listeners.size).toBe(0);
174
+ });
175
+
176
+ it("one active connection: connecting another family closes the first and ignores it after", async () => {
177
+ const storage = memoryStorage();
178
+ const { store, eth, sol } = setup(storage);
179
+ await store.connect("eth", "metaMask");
180
+ await store.connect("sol", "solflare");
181
+ expect(eth.disconnect).toHaveBeenCalledTimes(1);
182
+ expect(store.getState()).toMatchObject({ family: "sol", address: solflareAccount.address });
183
+ eth.emit({ status: "connected", connectorId: "metaMask", account: evmAccount });
184
+ expect(store.getState()).toMatchObject({ family: "sol", address: solflareAccount.address });
185
+ expect(eth.listeners.size).toBe(0);
186
+ expect(sol.listeners.size).toBe(1);
187
+ expect(storage.map.get(defaultConnectionStorageKey)).toBe(
188
+ JSON.stringify({ family: "sol", connectorId: "solflare" }),
189
+ );
190
+ });
191
+
192
+ it("follows the active driver's account and chain changes", async () => {
193
+ const { store, eth } = setup();
194
+ await store.connect("eth", "metaMask");
195
+ const next = { address: "0x2222222222222222222222222222222222222222", chainId: 8453 };
196
+ eth.emit({ status: "connected", connectorId: "metaMask", account: next });
197
+ expect(store.getState()).toMatchObject({ status: "connected", ...next });
198
+ expect(store.getState().client?.address).toBe(next.address);
199
+ eth.emit({ status: "connected", connectorId: "coinbase", account: next });
200
+ expect(store.getState().connector).toEqual({ id: "coinbase", name: "Coinbase Wallet" });
201
+ });
202
+
203
+ it("a disconnect from the wallet side clears the state and keeps the record for a restore", async () => {
204
+ const storage = memoryStorage();
205
+ const { store, sol } = setup(storage);
206
+ await store.connect("sol", "phantom");
207
+ sol.emit({ status: "reconnecting", connectorId: "phantom", account: null });
208
+ expect(store.getState().status).toBe("connected");
209
+ sol.emit({ status: "disconnected", connectorId: null, account: null });
210
+ expect(store.getState().status).toBe("disconnected");
211
+ expect(sol.listeners.size).toBe(0);
212
+ expect(storage.map.get(defaultConnectionStorageKey)).toBe(
213
+ JSON.stringify({ family: "sol", connectorId: "phantom" }),
214
+ );
215
+ expect((await store.reconnect()).family).toBe("sol");
216
+ expect(sol.reconnect).toHaveBeenCalledWith("phantom");
217
+ });
218
+
219
+ it("a connector switch inside the active driver is remembered", async () => {
220
+ const storage = memoryStorage();
221
+ const { store, eth } = setup(storage);
222
+ await store.connect("eth", "metaMask");
223
+ eth.emit({ status: "connected", connectorId: "coinbase", account: evmAccount });
224
+ expect(store.getState().connector?.id).toBe("coinbase");
225
+ await vi.waitFor(() =>
226
+ expect(storage.map.get(defaultConnectionStorageKey)).toBe(
227
+ JSON.stringify({ family: "eth", connectorId: "coinbase" }),
228
+ ),
229
+ );
230
+ });
231
+
232
+ it("a repeated driver event changes nothing and builds no new client", async () => {
233
+ const { store, eth, seen } = setup();
234
+ await store.connect("eth", "metaMask");
235
+ const before = store.getState();
236
+ const calls = eth.client.mock.calls.length;
237
+ const notified = seen.length;
238
+ eth.emit({ status: "connected", connectorId: "metaMask", account: evmAccount });
239
+ expect(store.getState()).toBe(before);
240
+ expect(eth.client.mock.calls.length).toBe(calls);
241
+ expect(seen.length).toBe(notified);
242
+ });
243
+
244
+ it("an async client lands once built, and a late one for a closed connection does not", async () => {
245
+ const { store, eth } = setup();
246
+ const built = deferred<ChainClient | null>();
247
+ eth.client.mockReturnValueOnce(built.promise);
248
+ const state = await store.connect("eth", "metaMask");
249
+ expect(state).toMatchObject({ status: "connected", client: null });
250
+ const client = {
251
+ family: "evm",
252
+ address: evmAccount.address,
253
+ capabilities: [],
254
+ signMessage: async () => "0x",
255
+ } as ChainClient;
256
+ built.resolve(client);
257
+ await vi.waitFor(() => expect(store.getState().client).toBe(client));
258
+
259
+ const late = deferred<ChainClient | null>();
260
+ eth.client.mockReturnValueOnce(late.promise);
261
+ eth.emit({
262
+ status: "connected",
263
+ connectorId: "metaMask",
264
+ account: { ...evmAccount, chainId: 1 },
265
+ });
266
+ expect(store.getState()).toMatchObject({ chainId: 1, client: null });
267
+ await store.disconnect();
268
+ late.resolve(client);
269
+ await Promise.resolve();
270
+ expect(store.getState()).toMatchObject({ status: "disconnected", client: null });
271
+ });
272
+
273
+ it("a refused connect leaves the store disconnected and remembers nothing", async () => {
274
+ const storage = memoryStorage();
275
+ const { store, eth } = setup(storage);
276
+ eth.connect.mockRejectedValueOnce(new Error("User rejected the request."));
277
+ await expect(store.connect("eth", "metaMask")).rejects.toThrow("User rejected");
278
+ expect(store.getState().status).toBe("disconnected");
279
+ expect(storage.map.size).toBe(0);
280
+ });
281
+
282
+ it("refuses a connectionless family and an unregistered one", async () => {
283
+ const { store } = setup();
284
+ await expect(store.connect("gpg", "key")).rejects.toThrow(/connectionless/);
285
+ await expect(store.connect("sui", "slush")).rejects.toThrow(/not registered.*eth, sol, gpg/);
286
+ expect(store.getState().status).toBe("disconnected");
287
+ });
288
+
289
+ it("lists every connectable family's connectors in family order", () => {
290
+ const { store } = setup();
291
+ expect(store.connectors().map((entry) => `${entry.family}:${entry.connector.id}`)).toEqual([
292
+ "eth:metaMask",
293
+ "eth:coinbase",
294
+ "sol:phantom",
295
+ "sol:solflare",
296
+ ]);
297
+ });
298
+
299
+ it("keeps one connector list until a driver announces a change", () => {
300
+ const { store, eth } = setup();
301
+ const first = store.connectors();
302
+ expect(store.connectors()).toBe(first);
303
+ const changed = vi.fn();
304
+ const unsubscribe = store.subscribeConnectors(changed);
305
+ eth.announce([
306
+ { id: "metaMask", name: "MetaMask" },
307
+ { id: "coinbase", name: "Coinbase Wallet" },
308
+ { id: "io.rabby", name: "Rabby", installed: true },
309
+ ]);
310
+ expect(changed).toHaveBeenCalledTimes(1);
311
+ const second = store.connectors();
312
+ expect(second).not.toBe(first);
313
+ expect(second.map((entry) => entry.connector.id)).toContain("io.rabby");
314
+ expect(store.connectors()).toBe(second);
315
+ unsubscribe();
316
+ expect(eth.connectorListeners.size).toBe(0);
317
+ eth.announce([]);
318
+ expect(changed).toHaveBeenCalledTimes(1);
319
+ });
320
+
321
+ describe("reconnect", () => {
322
+ it("restores the saved connection through the saved family's driver only", async () => {
323
+ const { store, eth, sol, makeEth, seen } = setup(memoryStorage(saved("sol", "phantom")));
324
+ const state = await store.reconnect();
325
+ expect(seen.map((entry) => entry.status)).toEqual(["reconnecting", "connected"]);
326
+ expect(state).toMatchObject({
327
+ status: "connected",
328
+ family: "sol",
329
+ connector: { id: "phantom", name: "Phantom" },
330
+ address: phantomAccount.address,
331
+ });
332
+ expect(sol.reconnect).toHaveBeenCalledWith("phantom");
333
+ expect(sol.connect).not.toHaveBeenCalled();
334
+ expect(makeEth).not.toHaveBeenCalled();
335
+ expect(eth.reconnect).not.toHaveBeenCalled();
336
+ });
337
+
338
+ it("never restores from two drivers: a second driver's account is ignored", async () => {
339
+ const { store, eth } = setup(memoryStorage(saved("sol", "phantom")));
340
+ store.connectors();
341
+ const restoring = store.reconnect();
342
+ eth.emit({ status: "connected", connectorId: "metaMask", account: evmAccount });
343
+ await restoring;
344
+ eth.emit({ status: "connected", connectorId: "metaMask", account: evmAccount });
345
+ expect(store.getState()).toMatchObject({ family: "sol", address: phantomAccount.address });
346
+ expect(eth.reconnect).not.toHaveBeenCalled();
347
+ expect(eth.listeners.size).toBe(0);
348
+ });
349
+
350
+ it("concurrent calls share one attempt", async () => {
351
+ const { store, sol } = setup(memoryStorage(saved("sol", "phantom")));
352
+ const [first, second] = await Promise.all([store.reconnect(), store.reconnect()]);
353
+ expect(first).toBe(second);
354
+ expect(sol.reconnect).toHaveBeenCalledTimes(1);
355
+ expect(await store.reconnect()).toBe(first);
356
+ expect(sol.reconnect).toHaveBeenCalledTimes(1);
357
+ });
358
+
359
+ it("with nothing saved asks no driver", async () => {
360
+ const { store, makeEth, makeSol } = setup(memoryStorage());
361
+ expect((await store.reconnect()).status).toBe("disconnected");
362
+ expect(makeEth).not.toHaveBeenCalled();
363
+ expect(makeSol).not.toHaveBeenCalled();
364
+ });
365
+
366
+ it("forgets a connector that is no longer authorised", async () => {
367
+ const storage = memoryStorage(saved("eth", "metaMask"));
368
+ const { store, eth } = setup(storage);
369
+ eth.reconnect.mockResolvedValueOnce(null);
370
+ expect((await store.reconnect()).status).toBe("disconnected");
371
+ expect(storage.map.has(defaultConnectionStorageKey)).toBe(false);
372
+ });
373
+
374
+ it("keeps the saved connection when the restore fails transiently", async () => {
375
+ const storage = memoryStorage(saved("eth", "metaMask"));
376
+ const { store, eth } = setup(storage);
377
+ eth.reconnect.mockRejectedValueOnce(new Error("provider not ready"));
378
+ expect((await store.reconnect()).status).toBe("disconnected");
379
+ expect(storage.map.has(defaultConnectionStorageKey)).toBe(true);
380
+ });
381
+
382
+ it("leaves alone a saved family this app does not register or cannot connect", async () => {
383
+ for (const family of ["sui", "gpg"]) {
384
+ const storage = memoryStorage(saved(family, "whatever"));
385
+ const { store, makeEth, makeSol } = setup(storage);
386
+ expect((await store.reconnect()).status).toBe("disconnected");
387
+ expect(storage.map.has(defaultConnectionStorageKey)).toBe(true);
388
+ expect(makeEth).not.toHaveBeenCalled();
389
+ expect(makeSol).not.toHaveBeenCalled();
390
+ }
391
+ });
392
+
393
+ it("ignores a malformed saved record", async () => {
394
+ for (const raw of ["not json", "null", '{"family":"sol"}', '["sol","phantom"]']) {
395
+ const { store, makeSol } = setup(memoryStorage({ [defaultConnectionStorageKey]: raw }));
396
+ expect((await store.reconnect()).status).toBe("disconnected");
397
+ expect(makeSol).not.toHaveBeenCalled();
398
+ }
399
+ });
400
+
401
+ it("a restore whose driver cannot be made rejects once, with no unhandled rejection", async () => {
402
+ const unhandled: unknown[] = [];
403
+ const onUnhandled = (reason: unknown) => unhandled.push(reason);
404
+ process.on("unhandledRejection", onUnhandled);
405
+ try {
406
+ const store = createConnectionStore({
407
+ storage: memoryStorage(saved("eth", "metaMask")),
408
+ families: [
409
+ defineWalletFamily({
410
+ id: "eth",
411
+ label: "Ethereum",
412
+ message,
413
+ driver: () => {
414
+ throw new Error("no injected provider");
415
+ },
416
+ }),
417
+ ],
418
+ });
419
+ await expect(store.reconnect()).rejects.toThrow("no injected provider");
420
+ await new Promise((resolve) => setTimeout(resolve, 10));
421
+ expect(unhandled).toEqual([]);
422
+ } finally {
423
+ process.off("unhandledRejection", onUnhandled);
424
+ }
425
+ });
426
+
427
+ it("does nothing while a connection is open", async () => {
428
+ const { store, sol } = setup(memoryStorage(saved("sol", "phantom")));
429
+ await store.connect("eth", "metaMask");
430
+ expect((await store.reconnect()).family).toBe("eth");
431
+ expect(sol.reconnect).not.toHaveBeenCalled();
432
+ });
433
+
434
+ it("a user connect during the restore wins, and the late restore is closed", async () => {
435
+ const storage = memoryStorage(saved("sol", "phantom"));
436
+ const { store, sol } = setup(storage);
437
+ const late = deferred<DriverAccount | null>();
438
+ sol.reconnect.mockReturnValueOnce(late.promise);
439
+ const restoring = store.reconnect();
440
+ await vi.waitFor(() => expect(sol.reconnect).toHaveBeenCalled());
441
+ await store.connect("eth", "metaMask");
442
+ late.resolve(phantomAccount);
443
+ await restoring;
444
+ expect(store.getState()).toMatchObject({ family: "eth", address: evmAccount.address });
445
+ expect(sol.disconnect).toHaveBeenCalledTimes(1);
446
+ expect(storage.map.get(defaultConnectionStorageKey)).toBe(
447
+ JSON.stringify({ family: "eth", connectorId: "metaMask" }),
448
+ );
449
+ });
450
+ });
451
+
452
+ describe("superseded connects", () => {
453
+ it("a disconnect while connecting wins, and the late wallet is closed", async () => {
454
+ const storage = memoryStorage();
455
+ const { store, eth } = setup(storage);
456
+ const late = deferred<DriverAccount>();
457
+ eth.connect.mockReturnValueOnce(late.promise);
458
+ const connecting = store.connect("eth", "metaMask");
459
+ await vi.waitFor(() => expect(eth.connect).toHaveBeenCalled());
460
+ expect(store.getState().status).toBe("connecting");
461
+ await store.disconnect();
462
+ late.resolve(evmAccount);
463
+ await expect(connecting).rejects.toThrow(/superseded/);
464
+ expect(store.getState().status).toBe("disconnected");
465
+ expect(eth.disconnect).toHaveBeenCalledTimes(1);
466
+ expect(storage.map.size).toBe(0);
467
+ });
468
+
469
+ it("the later of two connects on one driver wins, and only the stale connector is closed", async () => {
470
+ const { store, sol } = setup();
471
+ const first = deferred<DriverAccount>();
472
+ sol.connect.mockReturnValueOnce(first.promise);
473
+ const phantom = store.connect("sol", "phantom");
474
+ await vi.waitFor(() => expect(sol.connect).toHaveBeenCalledTimes(1));
475
+ const solflare = store.connect("sol", "solflare");
476
+ first.resolve(phantomAccount);
477
+ await expect(phantom).rejects.toThrow(/superseded/);
478
+ expect((await solflare).connector?.id).toBe("solflare");
479
+ expect(sol.disconnect).toHaveBeenCalledTimes(1);
480
+ expect(sol.disconnect).toHaveBeenCalledWith("phantom");
481
+ expect(store.getState()).toMatchObject({ family: "sol", address: solflareAccount.address });
482
+ expect(sol.getState().connectorId).toBe("solflare");
483
+ });
484
+
485
+ it("a superseded connect's slow close does not wipe the winner's record", async () => {
486
+ const storage = memoryStorage();
487
+ const { store, eth, sol } = setup(storage);
488
+ await store.connect("eth", "metaMask");
489
+ const slowClose = deferred<void>();
490
+ eth.disconnect.mockReturnValueOnce(slowClose.promise);
491
+ const first = store.connect("sol", "phantom");
492
+ const second = store.connect("sol", "solflare");
493
+ expect((await second).connector?.id).toBe("solflare");
494
+ slowClose.resolve();
495
+ await expect(first).rejects.toThrow(/superseded/);
496
+ expect(storage.map.get(defaultConnectionStorageKey)).toBe(
497
+ JSON.stringify({ family: "sol", connectorId: "solflare" }),
498
+ );
499
+ expect(sol.connect).toHaveBeenCalledTimes(1);
500
+ });
501
+
502
+ it("a stale connection left for a winner that is then refused gets closed", async () => {
503
+ const { store, sol } = setup();
504
+ const first = deferred<DriverAccount>();
505
+ const second = deferred<DriverAccount>();
506
+ sol.connect.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise);
507
+ const a = store.connect("sol", "phantom");
508
+ await vi.waitFor(() => expect(sol.connect).toHaveBeenCalledTimes(1));
509
+ const b = store.connect("sol", "phantom");
510
+ await vi.waitFor(() => expect(sol.connect).toHaveBeenCalledTimes(2));
511
+ sol.emit({ status: "connected", connectorId: "phantom", account: phantomAccount });
512
+ first.resolve(phantomAccount);
513
+ await expect(a).rejects.toThrow(/superseded/);
514
+ expect(sol.disconnect).not.toHaveBeenCalled();
515
+ second.reject(new Error("User rejected the request."));
516
+ await expect(b).rejects.toThrow(/User rejected/);
517
+ expect(store.getState().status).toBe("disconnected");
518
+ expect(sol.disconnect).toHaveBeenCalledTimes(1);
519
+ expect(sol.getState().account).toBeNull();
520
+ });
521
+
522
+ it("a connect right after a disconnect waits for the close on that driver", async () => {
523
+ const storage = memoryStorage();
524
+ const { store, eth } = setup(storage);
525
+ await store.connect("eth", "metaMask");
526
+ const slowClose = deferred<void>();
527
+ eth.disconnect.mockImplementationOnce(async () => {
528
+ await slowClose.promise;
529
+ eth.emit({ status: "disconnected", connectorId: null, account: null });
530
+ });
531
+ const closing = store.disconnect();
532
+ const connecting = store.connect("eth", "coinbase");
533
+ await new Promise((resolve) => setTimeout(resolve, 20));
534
+ expect(eth.connect).toHaveBeenCalledTimes(1);
535
+ slowClose.resolve();
536
+ await closing;
537
+ const state = await connecting;
538
+ expect(eth.connect).toHaveBeenCalledTimes(2);
539
+ expect(state).toMatchObject({ status: "connected", connector: { id: "coinbase" } });
540
+ expect(store.getState().status).toBe("connected");
541
+ expect(storage.map.get(defaultConnectionStorageKey)).toBe(
542
+ JSON.stringify({ family: "eth", connectorId: "coinbase" }),
543
+ );
544
+ });
545
+
546
+ it.each([
547
+ ["a disconnect", (store: ReturnType<typeof setup>["store"]) => store.disconnect()],
548
+ [
549
+ "a connect on another family",
550
+ async (store: ReturnType<typeof setup>["store"]) => {
551
+ await store.connect("eth", "metaMask");
552
+ },
553
+ ],
554
+ ])(
555
+ "a stale connection left for a winner that loses to %s and is then refused gets closed",
556
+ async (_, supersede) => {
557
+ const { store, sol } = setup();
558
+ const first = deferred<DriverAccount>();
559
+ const second = deferred<DriverAccount>();
560
+ sol.connect.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise);
561
+ const a = store.connect("sol", "phantom");
562
+ await vi.waitFor(() => expect(sol.connect).toHaveBeenCalledTimes(1));
563
+ const b = store.connect("sol", "phantom");
564
+ await vi.waitFor(() => expect(sol.connect).toHaveBeenCalledTimes(2));
565
+ sol.emit({ status: "connected", connectorId: "phantom", account: phantomAccount });
566
+ first.resolve(phantomAccount);
567
+ await expect(a).rejects.toThrow(/superseded/);
568
+ await supersede(store);
569
+ second.reject(new Error("User rejected the request."));
570
+ await expect(b).rejects.toThrow(/User rejected/);
571
+ expect(sol.disconnect).toHaveBeenCalledTimes(1);
572
+ expect(sol.getState().account).toBeNull();
573
+ },
574
+ );
575
+
576
+ it("a superseded connect approved late is not taken for a connector switch", async () => {
577
+ const storage = memoryStorage();
578
+ const { store, sol } = setup(storage);
579
+ const first = deferred<DriverAccount>();
580
+ sol.connect.mockReturnValueOnce(first.promise);
581
+ const a = store.connect("sol", "phantom");
582
+ await vi.waitFor(() => expect(sol.connect).toHaveBeenCalledTimes(1));
583
+ expect((await store.connect("sol", "solflare")).connector?.id).toBe("solflare");
584
+ sol.emit({ status: "connected", connectorId: "phantom", account: phantomAccount });
585
+ expect(store.getState()).toMatchObject({
586
+ connector: { id: "solflare" },
587
+ address: solflareAccount.address,
588
+ });
589
+ first.resolve(phantomAccount);
590
+ await expect(a).rejects.toThrow(/superseded/);
591
+ expect(sol.disconnect).toHaveBeenCalledWith("phantom");
592
+ // The test driver holds one connection, so approving phantom dropped solflare.
593
+ expect(store.getState().status).toBe("disconnected");
594
+ expect(storage.map.get(defaultConnectionStorageKey)).toBe(
595
+ JSON.stringify({ family: "sol", connectorId: "solflare" }),
596
+ );
597
+ });
598
+
599
+ it("a refused attempt leaves alone a connection it cannot tell apart from the winner's", async () => {
600
+ const { store, sol } = setup();
601
+ const first = deferred<DriverAccount>();
602
+ sol.connect.mockReturnValueOnce(first.promise);
603
+ const a = store.connect("sol", "phantom");
604
+ await vi.waitFor(() => expect(sol.connect).toHaveBeenCalledTimes(1));
605
+ await store.connect("sol", "solflare");
606
+ sol.emit({ status: "connected", connectorId: null, account: solflareAccount });
607
+ first.reject(new Error("User rejected the request."));
608
+ await expect(a).rejects.toThrow(/User rejected/);
609
+ expect(sol.disconnect).not.toHaveBeenCalled();
610
+ expect(store.getState()).toMatchObject({
611
+ status: "connected",
612
+ connector: { id: "solflare" },
613
+ });
614
+ });
615
+
616
+ it("a close that never settles does not wedge the driver", async () => {
617
+ const { store, eth } = setup(memoryStorage(), 20);
618
+ await store.connect("eth", "metaMask");
619
+ eth.disconnect.mockReturnValueOnce(new Promise<void>(() => {}));
620
+ await store.disconnect();
621
+ const state = await store.connect("eth", "coinbase");
622
+ expect(state).toMatchObject({ status: "connected", connector: { id: "coinbase" } });
623
+ expect(eth.connect).toHaveBeenCalledTimes(2);
624
+ });
625
+
626
+ it.each([
627
+ [Number.NaN, false],
628
+ [-5, true],
629
+ [Number.POSITIVE_INFINITY, false],
630
+ ])(
631
+ "closeTimeout %s: a 50 ms disconnect is awaited unless the timeout is negative",
632
+ async (closeTimeout, early) => {
633
+ const { store, eth } = setup(memoryStorage(), closeTimeout);
634
+ await store.connect("eth", "metaMask");
635
+ eth.disconnect.mockReturnValueOnce(new Promise<void>((resolve) => setTimeout(resolve, 50)));
636
+ let done = false;
637
+ const disconnecting = store.disconnect().then(() => {
638
+ done = true;
639
+ });
640
+ await new Promise((resolve) => setTimeout(resolve, 20));
641
+ expect(done).toBe(early);
642
+ await disconnecting;
643
+ },
644
+ );
645
+
646
+ it("a reconnect call while connecting leaves the connect alone", async () => {
647
+ const { store, eth, sol } = setup(memoryStorage(saved("sol", "phantom")));
648
+ const connecting = store.connect("eth", "metaMask");
649
+ expect((await store.reconnect()).status).toBe("connecting");
650
+ await connecting;
651
+ expect(sol.reconnect).not.toHaveBeenCalled();
652
+ expect(eth.connect).toHaveBeenCalledTimes(1);
653
+ expect(store.getState().family).toBe("eth");
654
+ });
655
+ });
656
+
657
+ describe("storage", () => {
658
+ it("uses localStorage when the platform has one", async () => {
659
+ const local = memoryStorage();
660
+ vi.stubGlobal("localStorage", local);
661
+ const { store } = setup("platform");
662
+ await store.connect("sol", "phantom");
663
+ expect(local.setItem).toHaveBeenCalledWith(
664
+ defaultConnectionStorageKey,
665
+ JSON.stringify({ family: "sol", connectorId: "phantom" }),
666
+ );
667
+ });
668
+
669
+ it("null storage keeps nothing and restores nothing", async () => {
670
+ const { store, makeSol } = setup(null);
671
+ await store.connect("eth", "metaMask");
672
+ await store.disconnect();
673
+ expect((await store.reconnect()).status).toBe("disconnected");
674
+ expect(makeSol).not.toHaveBeenCalled();
675
+ });
676
+
677
+ it("a storage that throws costs only the restore", async () => {
678
+ const broken: ConnectionStorage = {
679
+ getItem: () => {
680
+ throw new Error("SecurityError");
681
+ },
682
+ setItem: () => Promise.reject(new Error("QuotaExceededError")),
683
+ removeItem: () => {
684
+ throw new Error("SecurityError");
685
+ },
686
+ };
687
+ const { store } = setup(broken);
688
+ expect((await store.reconnect()).status).toBe("disconnected");
689
+ expect((await store.connect("eth", "metaMask")).status).toBe("connected");
690
+ await store.disconnect();
691
+ expect(store.getState().status).toBe("disconnected");
692
+ });
693
+
694
+ it("honours a custom storage key", async () => {
695
+ const storage = memoryStorage();
696
+ const eth = testDriver("evm", [{ id: "metaMask", name: "MetaMask" }], {
697
+ metaMask: evmAccount,
698
+ });
699
+ const store = createConnectionStore({
700
+ storage,
701
+ storageKey: "app.wallet",
702
+ families: [
703
+ defineWalletFamily({ id: "eth", label: "Ethereum", message, driver: () => eth }),
704
+ ],
705
+ });
706
+ await store.connect("eth", "metaMask");
707
+ expect([...storage.map.keys()]).toEqual(["app.wallet"]);
708
+ });
709
+ });
710
+
711
+ it.each([
712
+ ["phantom", "rejects"],
713
+ [null, "rejects"],
714
+ ["phantom", "resolves"],
715
+ ] as const)(
716
+ "a stale attempt that settles after destroy closes nothing (driver reports %s, attempt %s)",
717
+ async (connectorId, outcome) => {
718
+ const { store, sol } = setup();
719
+ const first = deferred<DriverAccount>();
720
+ sol.connect.mockReturnValueOnce(first.promise);
721
+ const a = store.connect("sol", "phantom");
722
+ await vi.waitFor(() => expect(sol.connect).toHaveBeenCalledTimes(1));
723
+ sol.emit({ status: "connected", connectorId, account: phantomAccount });
724
+ store.destroy();
725
+ if (outcome === "rejects") first.reject(new Error("User rejected the request."));
726
+ else first.resolve(phantomAccount);
727
+ await expect(a).rejects.toThrow(outcome === "rejects" ? /User rejected/ : /superseded/);
728
+ expect(sol.disconnect).not.toHaveBeenCalled();
729
+ },
730
+ );
731
+
732
+ it("an attempt disconnect() cancelled before destroy still closes its own connector", async () => {
733
+ const { store, sol } = setup();
734
+ const first = deferred<DriverAccount>();
735
+ sol.connect.mockReturnValueOnce(first.promise);
736
+ const a = store.connect("sol", "phantom");
737
+ await vi.waitFor(() => expect(sol.connect).toHaveBeenCalledTimes(1));
738
+ await store.disconnect();
739
+ store.destroy();
740
+ sol.emit({ status: "connected", connectorId: "phantom", account: phantomAccount });
741
+ first.resolve(phantomAccount);
742
+ await expect(a).rejects.toThrow(/superseded/);
743
+ expect(sol.disconnect).toHaveBeenCalledWith("phantom");
744
+ expect(sol.getState().account).toBeNull();
745
+ });
746
+
747
+ it("an attempt superseded before destroy leaves the connector the live attempt holds", async () => {
748
+ const { store, sol } = setup();
749
+ const first = deferred<DriverAccount>();
750
+ const second = deferred<DriverAccount>();
751
+ sol.connect.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise);
752
+ const a = store.connect("sol", "phantom");
753
+ await vi.waitFor(() => expect(sol.connect).toHaveBeenCalledTimes(1));
754
+ const b = store.connect("sol", "phantom");
755
+ await vi.waitFor(() => expect(sol.connect).toHaveBeenCalledTimes(2));
756
+ store.destroy();
757
+ sol.emit({ status: "connected", connectorId: "phantom", account: phantomAccount });
758
+ first.resolve(phantomAccount);
759
+ second.resolve(phantomAccount);
760
+ await expect(a).rejects.toThrow(/superseded/);
761
+ await expect(b).rejects.toThrow(/superseded/);
762
+ expect(sol.disconnect).not.toHaveBeenCalled();
763
+ });
764
+
765
+ it("a store remounted on the same driver keeps its connection when the old restore lands", async () => {
766
+ const { sol, make } = sharedDrivers(memoryStorage(saved("sol", "phantom")));
767
+ const first = deferred<DriverAccount | null>();
768
+ sol.reconnect.mockReturnValueOnce(first.promise);
769
+ const unmounted = make();
770
+ const restoring = unmounted.reconnect();
771
+ await vi.waitFor(() => expect(sol.reconnect).toHaveBeenCalledTimes(1));
772
+ unmounted.destroy();
773
+ const remounted = make();
774
+ expect((await remounted.reconnect()).status).toBe("connected");
775
+ first.resolve(phantomAccount);
776
+ await restoring;
777
+ await new Promise((resolve) => setTimeout(resolve, 10));
778
+ expect(sol.disconnect).not.toHaveBeenCalled();
779
+ expect(remounted.getState().status).toBe("connected");
780
+ });
781
+
782
+ it("a remounted store's other connector survives the old restore failing", async () => {
783
+ const { sol, make } = sharedDrivers(memoryStorage(saved("sol", "phantom")));
784
+ const first = deferred<DriverAccount | null>();
785
+ sol.reconnect.mockReturnValueOnce(first.promise);
786
+ const unmounted = make();
787
+ const restoring = unmounted.reconnect();
788
+ await vi.waitFor(() => expect(sol.reconnect).toHaveBeenCalledTimes(1));
789
+ unmounted.destroy();
790
+ const remounted = make();
791
+ expect((await remounted.connect("sol", "solflare")).status).toBe("connected");
792
+ first.reject(new Error("User rejected the request."));
793
+ await restoring;
794
+ expect(sol.disconnect).not.toHaveBeenCalled();
795
+ expect(remounted.getState()).toMatchObject({
796
+ status: "connected",
797
+ connector: { id: "solflare" },
798
+ });
799
+ });
800
+
801
+ describe("a cancelled attempt settling after destroy leaves a remounted store's same connector", () => {
802
+ it.each([false, true])("when it never reached the driver (wrapped: %s)", async (wrap) => {
803
+ const { eth, sol, make } = sharedDrivers(memoryStorage(), wrap);
804
+ const unmounted = make();
805
+ await unmounted.connect("eth", "metaMask");
806
+ const slow = deferred<void>();
807
+ eth.disconnect.mockReturnValueOnce(slow.promise);
808
+ const a = unmounted.connect("sol", "phantom");
809
+ await unmounted.disconnect();
810
+ unmounted.destroy();
811
+ const remounted = make();
812
+ expect((await remounted.connect("sol", "phantom")).status).toBe("connected");
813
+ slow.resolve();
814
+ await expect(a).rejects.toThrow(/superseded/);
815
+ expect(sol.connect).toHaveBeenCalledTimes(1);
816
+ expect(sol.disconnect).not.toHaveBeenCalled();
817
+ expect(remounted.getState().status).toBe("connected");
818
+ });
819
+
820
+ it.each(["resolves", "rejects"] as const)("when its wallet request %s", async (outcome) => {
821
+ const { sol, make } = sharedDrivers();
822
+ const unmounted = make();
823
+ const first = deferred<DriverAccount>();
824
+ sol.connect.mockReturnValueOnce(first.promise);
825
+ const a = unmounted.connect("sol", "phantom");
826
+ await vi.waitFor(() => expect(sol.connect).toHaveBeenCalledTimes(1));
827
+ await unmounted.disconnect();
828
+ unmounted.destroy();
829
+ const remounted = make();
830
+ expect((await remounted.connect("sol", "phantom")).status).toBe("connected");
831
+ if (outcome === "resolves") first.resolve(phantomAccount);
832
+ else first.reject(new Error("User rejected the request."));
833
+ await expect(a).rejects.toThrow(outcome === "resolves" ? /superseded/ : /User rejected/);
834
+ expect(sol.disconnect).not.toHaveBeenCalled();
835
+ expect(remounted.getState().status).toBe("connected");
836
+ });
837
+ });
838
+
839
+ it("a remount on a fresh driver over the same wallet cannot be told from the leftover, so it closes", async () => {
840
+ const { sol, make } = sharedDrivers(memoryStorage(), true);
841
+ const unmounted = make();
842
+ const first = deferred<DriverAccount>();
843
+ sol.connect.mockReturnValueOnce(first.promise);
844
+ const a = unmounted.connect("sol", "phantom");
845
+ await vi.waitFor(() => expect(sol.connect).toHaveBeenCalledTimes(1));
846
+ await unmounted.disconnect();
847
+ unmounted.destroy();
848
+ const remounted = make();
849
+ expect((await remounted.connect("sol", "phantom")).status).toBe("connected");
850
+ first.resolve(phantomAccount);
851
+ await expect(a).rejects.toThrow(/superseded/);
852
+ expect(sol.disconnect).toHaveBeenCalledWith("phantom");
853
+ });
854
+
855
+ it("a destroyed store refuses connect and restores nothing", async () => {
856
+ const { store, sol } = setup(memoryStorage(saved("sol", "phantom")));
857
+ store.destroy();
858
+ await expect(store.connect("sol", "phantom")).rejects.toThrow(/destroyed/);
859
+ expect((await store.reconnect()).status).toBe("disconnected");
860
+ expect(sol.connect).not.toHaveBeenCalled();
861
+ expect(sol.reconnect).not.toHaveBeenCalled();
862
+ expect(sol.listeners.size).toBe(0);
863
+ });
864
+
865
+ it("destroy stops following the active driver and notifying listeners", async () => {
866
+ const { store, eth, seen } = setup();
867
+ await store.connect("eth", "metaMask");
868
+ const count = seen.length;
869
+ store.destroy();
870
+ eth.emit({ status: "disconnected", connectorId: null, account: null });
871
+ expect(seen.length).toBe(count);
872
+ expect(eth.listeners.size).toBe(0);
873
+ });
874
+
875
+ it("a driver whose client throws still connects, with no client", async () => {
876
+ const { store, eth } = setup();
877
+ eth.client.mockImplementation(() => {
878
+ throw new Error("no public client");
879
+ });
880
+ const state = await store.connect("eth", "metaMask");
881
+ expect(state).toMatchObject({ status: "connected", client: null });
882
+ });
883
+
884
+ it("getState keeps its identity between changes", async () => {
885
+ const { store } = setup();
886
+ await store.connect("eth", "metaMask");
887
+ expect(store.getState()).toBe(store.getState());
888
+ expect(Object.isFrozen(store.getState())).toBe(true);
889
+ });
890
+ });