@geeqdev/geeq-sdk 0.1.0 → 0.2.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 (392) hide show
  1. package/.idea/geeq_sdk.iml +12 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/vcs.xml +6 -0
  4. package/.nvmrc +1 -0
  5. package/CONTRIBUTING.md +20 -0
  6. package/dist/src/address/record-type.d.ts +10 -0
  7. package/dist/src/address/record-type.js +59 -0
  8. package/dist/src/address/record-type.js.map +1 -0
  9. package/dist/src/address/wallet-address.d.ts +34 -0
  10. package/dist/src/address/wallet-address.js +90 -0
  11. package/dist/src/address/wallet-address.js.map +1 -0
  12. package/dist/src/amount.d.ts +43 -0
  13. package/dist/src/amount.js +105 -0
  14. package/dist/src/amount.js.map +1 -0
  15. package/dist/src/assert.d.ts +12 -0
  16. package/dist/src/assert.js +24 -0
  17. package/dist/src/assert.js.map +1 -0
  18. package/dist/src/capnp/adminNetTx.capnp.d.ts +75 -0
  19. package/dist/src/capnp/adminNetTx.capnp.js +66 -0
  20. package/dist/src/capnp/adminNetTx.capnp.js.map +1 -0
  21. package/dist/src/capnp/appLayBlock.capnp.d.ts +26 -0
  22. package/dist/src/capnp/appLayBlock.capnp.js +24 -0
  23. package/dist/src/capnp/appLayBlock.capnp.js.map +1 -0
  24. package/dist/src/capnp/appLayLedger.capnp.d.ts +34 -0
  25. package/dist/src/capnp/appLayLedger.capnp.js +32 -0
  26. package/dist/src/capnp/appLayLedger.capnp.js.map +1 -0
  27. package/dist/src/capnp/appLayRec.capnp.d.ts +129 -0
  28. package/dist/src/capnp/appLayRec.capnp.js +156 -0
  29. package/dist/src/capnp/appLayRec.capnp.js.map +1 -0
  30. package/dist/src/capnp/chainStateView.capnp.d.ts +50 -0
  31. package/dist/src/capnp/chainStateView.capnp.js +48 -0
  32. package/dist/src/capnp/chainStateView.capnp.js.map +1 -0
  33. package/dist/src/capnp/coinAcctRec.capnp.d.ts +178 -0
  34. package/dist/src/capnp/coinAcctRec.capnp.js +189 -0
  35. package/dist/src/capnp/coinAcctRec.capnp.js.map +1 -0
  36. package/dist/src/capnp/genesisBlock.capnp.d.ts +61 -0
  37. package/dist/src/capnp/genesisBlock.capnp.js +55 -0
  38. package/dist/src/capnp/genesisBlock.capnp.js.map +1 -0
  39. package/dist/src/capnp/hubContentStmt.capnp.d.ts +63 -0
  40. package/dist/src/capnp/hubContentStmt.capnp.js +57 -0
  41. package/dist/src/capnp/hubContentStmt.capnp.js.map +1 -0
  42. package/dist/src/capnp/misc.capnp.d.ts +20 -0
  43. package/dist/src/capnp/misc.capnp.js +18 -0
  44. package/dist/src/capnp/misc.capnp.js.map +1 -0
  45. package/dist/src/capnp/netActorRec.capnp.d.ts +65 -0
  46. package/dist/src/capnp/netActorRec.capnp.js +65 -0
  47. package/dist/src/capnp/netActorRec.capnp.js.map +1 -0
  48. package/dist/src/capnp/nodeContentStmt.capnp.d.ts +67 -0
  49. package/dist/src/capnp/nodeContentStmt.capnp.js +61 -0
  50. package/dist/src/capnp/nodeContentStmt.capnp.js.map +1 -0
  51. package/dist/src/capnp/nodePayloadList.capnp.d.ts +28 -0
  52. package/dist/src/capnp/nodePayloadList.capnp.js +26 -0
  53. package/dist/src/capnp/nodePayloadList.capnp.js.map +1 -0
  54. package/dist/src/capnp/nodeTxList.capnp.d.ts +36 -0
  55. package/dist/src/capnp/nodeTxList.capnp.js +34 -0
  56. package/dist/src/capnp/nodeTxList.capnp.js.map +1 -0
  57. package/dist/src/capnp/paramAcctRec.capnp.d.ts +338 -0
  58. package/dist/src/capnp/paramAcctRec.capnp.js +669 -0
  59. package/dist/src/capnp/paramAcctRec.capnp.js.map +1 -0
  60. package/dist/src/capnp/payload.capnp.d.ts +237 -0
  61. package/dist/src/capnp/payload.capnp.js +260 -0
  62. package/dist/src/capnp/payload.capnp.js.map +1 -0
  63. package/dist/src/capnp/proxyAcctRec.capnp.d.ts +223 -0
  64. package/dist/src/capnp/proxyAcctRec.capnp.js +255 -0
  65. package/dist/src/capnp/proxyAcctRec.capnp.js.map +1 -0
  66. package/dist/src/capnp/relayContentStmt.capnp.d.ts +82 -0
  67. package/dist/src/capnp/relayContentStmt.capnp.js +85 -0
  68. package/dist/src/capnp/relayContentStmt.capnp.js.map +1 -0
  69. package/dist/src/capnp/sysAcctRec.capnp.d.ts +48 -0
  70. package/dist/src/capnp/sysAcctRec.capnp.js +46 -0
  71. package/dist/src/capnp/sysAcctRec.capnp.js.map +1 -0
  72. package/dist/src/capnp/userAppTx.capnp.d.ts +230 -0
  73. package/dist/src/capnp/userAppTx.capnp.js +270 -0
  74. package/dist/src/capnp/userAppTx.capnp.js.map +1 -0
  75. package/dist/src/capnp/userValTx.capnp.d.ts +1575 -0
  76. package/dist/src/capnp/userValTx.capnp.js +2004 -0
  77. package/dist/src/capnp/userValTx.capnp.js.map +1 -0
  78. package/dist/src/capnp/valLayBlock.capnp.d.ts +88 -0
  79. package/dist/src/capnp/valLayBlock.capnp.js +86 -0
  80. package/dist/src/capnp/valLayBlock.capnp.js.map +1 -0
  81. package/dist/src/capnp/valLayLedger.capnp.d.ts +85 -0
  82. package/dist/src/capnp/valLayLedger.capnp.js +75 -0
  83. package/dist/src/capnp/valLayLedger.capnp.js.map +1 -0
  84. package/dist/src/chain/geeq-chain.d.ts +474 -0
  85. package/dist/src/chain/geeq-chain.js +721 -0
  86. package/dist/src/chain/geeq-chain.js.map +1 -0
  87. package/dist/src/chain/geeq-node.d.ts +57 -0
  88. package/dist/src/chain/geeq-node.js +62 -0
  89. package/dist/src/chain/geeq-node.js.map +1 -0
  90. package/dist/src/chain/nonce-guard.d.ts +42 -0
  91. package/dist/src/chain/nonce-guard.js +68 -0
  92. package/dist/src/chain/nonce-guard.js.map +1 -0
  93. package/dist/src/chain/select.d.ts +40 -0
  94. package/dist/src/chain/select.js +114 -0
  95. package/dist/src/chain/select.js.map +1 -0
  96. package/dist/src/crypto/account-id.d.ts +35 -0
  97. package/dist/src/crypto/account-id.js +58 -0
  98. package/dist/src/crypto/account-id.js.map +1 -0
  99. package/dist/src/crypto/keypair.d.ts +24 -0
  100. package/dist/src/crypto/keypair.js +158 -0
  101. package/dist/src/crypto/keypair.js.map +1 -0
  102. package/dist/src/crypto/mnemonic.d.ts +34 -0
  103. package/dist/src/crypto/mnemonic.js +57 -0
  104. package/dist/src/crypto/mnemonic.js.map +1 -0
  105. package/dist/src/encoding/base64.d.ts +8 -0
  106. package/dist/src/encoding/base64.js +53 -0
  107. package/dist/src/encoding/base64.js.map +1 -0
  108. package/dist/src/encoding/hex.d.ts +2 -0
  109. package/dist/src/encoding/hex.js +35 -0
  110. package/dist/src/encoding/hex.js.map +1 -0
  111. package/dist/src/errors.d.ts +83 -0
  112. package/dist/src/errors.js +49 -0
  113. package/dist/src/errors.js.map +1 -0
  114. package/dist/src/index.d.ts +59 -0
  115. package/dist/src/index.js +53 -0
  116. package/dist/src/index.js.map +1 -0
  117. package/dist/src/ledger/coin-accounts.d.ts +42 -0
  118. package/dist/src/ledger/coin-accounts.js +37 -0
  119. package/dist/src/ledger/coin-accounts.js.map +1 -0
  120. package/dist/src/ledger/decode.d.ts +18 -0
  121. package/dist/src/ledger/decode.js +113 -0
  122. package/dist/src/ledger/decode.js.map +1 -0
  123. package/dist/src/ledger/geeq-ledger.d.ts +99 -0
  124. package/dist/src/ledger/geeq-ledger.js +125 -0
  125. package/dist/src/ledger/geeq-ledger.js.map +1 -0
  126. package/dist/src/ledger/net-actors.d.ts +65 -0
  127. package/dist/src/ledger/net-actors.js +53 -0
  128. package/dist/src/ledger/net-actors.js.map +1 -0
  129. package/dist/src/ledger/nft-records.d.ts +13 -0
  130. package/dist/src/ledger/nft-records.js +41 -0
  131. package/dist/src/ledger/nft-records.js.map +1 -0
  132. package/dist/src/ledger/params.d.ts +34 -0
  133. package/dist/src/ledger/params.js +71 -0
  134. package/dist/src/ledger/params.js.map +1 -0
  135. package/dist/src/ledger/proxy-accounts.d.ts +26 -0
  136. package/dist/src/ledger/proxy-accounts.js +27 -0
  137. package/dist/src/ledger/proxy-accounts.js.map +1 -0
  138. package/dist/src/net/discovery.d.ts +62 -0
  139. package/dist/src/net/discovery.js +136 -0
  140. package/dist/src/net/discovery.js.map +1 -0
  141. package/dist/src/net/explorer.d.ts +57 -0
  142. package/dist/src/net/explorer.js +64 -0
  143. package/dist/src/net/explorer.js.map +1 -0
  144. package/dist/src/net/read-ledger.d.ts +7 -0
  145. package/dist/src/net/read-ledger.js +39 -0
  146. package/dist/src/net/read-ledger.js.map +1 -0
  147. package/dist/src/net/routes.d.ts +49 -0
  148. package/dist/src/net/routes.js +70 -0
  149. package/dist/src/net/routes.js.map +1 -0
  150. package/dist/src/net/transport.d.ts +55 -0
  151. package/dist/src/net/transport.js +168 -0
  152. package/dist/src/net/transport.js.map +1 -0
  153. package/dist/src/net/websocket.d.ts +49 -0
  154. package/dist/src/net/websocket.js +54 -0
  155. package/dist/src/net/websocket.js.map +1 -0
  156. package/dist/src/runtime/bindings.d.ts +9 -0
  157. package/dist/src/runtime/bindings.js +27 -0
  158. package/dist/src/runtime/bindings.js.map +1 -0
  159. package/dist/src/runtime/init.d.ts +13 -0
  160. package/dist/src/runtime/init.js +40 -0
  161. package/dist/src/runtime/init.js.map +1 -0
  162. package/dist/src/runtime/wasm-loader.browser.d.ts +14 -0
  163. package/dist/src/runtime/wasm-loader.browser.js +20 -0
  164. package/dist/src/runtime/wasm-loader.browser.js.map +1 -0
  165. package/dist/src/runtime/wasm-loader.node.d.ts +7 -0
  166. package/dist/src/runtime/wasm-loader.node.js +14 -0
  167. package/dist/src/runtime/wasm-loader.node.js.map +1 -0
  168. package/dist/src/socket/geeq-socket.d.ts +232 -0
  169. package/dist/src/socket/geeq-socket.js +634 -0
  170. package/dist/src/socket/geeq-socket.js.map +1 -0
  171. package/dist/src/socket/tracked-tx.d.ts +148 -0
  172. package/dist/src/socket/tracked-tx.js +212 -0
  173. package/dist/src/socket/tracked-tx.js.map +1 -0
  174. package/dist/src/socket/tx-status.d.ts +69 -0
  175. package/dist/src/socket/tx-status.js +72 -0
  176. package/dist/src/socket/tx-status.js.map +1 -0
  177. package/dist/src/tx/attestation.d.ts +60 -0
  178. package/dist/src/tx/attestation.js +163 -0
  179. package/dist/src/tx/attestation.js.map +1 -0
  180. package/dist/src/tx/content.d.ts +27 -0
  181. package/dist/src/tx/content.js +62 -0
  182. package/dist/src/tx/content.js.map +1 -0
  183. package/dist/src/tx/encode.d.ts +60 -0
  184. package/dist/src/tx/encode.js +82 -0
  185. package/dist/src/tx/encode.js.map +1 -0
  186. package/dist/src/tx/expiry.d.ts +11 -0
  187. package/dist/src/tx/expiry.js +12 -0
  188. package/dist/src/tx/expiry.js.map +1 -0
  189. package/dist/src/tx/fee.d.ts +16 -0
  190. package/dist/src/tx/fee.js +29 -0
  191. package/dist/src/tx/fee.js.map +1 -0
  192. package/dist/src/tx/metadata.d.ts +39 -0
  193. package/dist/src/tx/metadata.js +66 -0
  194. package/dist/src/tx/metadata.js.map +1 -0
  195. package/dist/src/tx/nft.d.ts +16 -0
  196. package/dist/src/tx/nft.js +98 -0
  197. package/dist/src/tx/nft.js.map +1 -0
  198. package/dist/src/tx/proxy.d.ts +37 -0
  199. package/dist/src/tx/proxy.js +134 -0
  200. package/dist/src/tx/proxy.js.map +1 -0
  201. package/dist/src/tx/transfer.d.ts +31 -0
  202. package/dist/src/tx/transfer.js +123 -0
  203. package/dist/src/tx/transfer.js.map +1 -0
  204. package/dist/src/types.d.ts +448 -0
  205. package/dist/src/types.js +2 -0
  206. package/dist/src/types.js.map +1 -0
  207. package/dist/tests/account-id.test.d.ts +1 -0
  208. package/dist/tests/account-id.test.js +27 -0
  209. package/dist/tests/account-id.test.js.map +1 -0
  210. package/dist/tests/amount.test.d.ts +1 -0
  211. package/dist/tests/amount.test.js +113 -0
  212. package/dist/tests/amount.test.js.map +1 -0
  213. package/dist/tests/attestation.test.d.ts +1 -0
  214. package/dist/tests/attestation.test.js +182 -0
  215. package/dist/tests/attestation.test.js.map +1 -0
  216. package/dist/tests/chain.test.d.ts +1 -0
  217. package/dist/tests/chain.test.js +647 -0
  218. package/dist/tests/chain.test.js.map +1 -0
  219. package/dist/tests/content.test.d.ts +1 -0
  220. package/dist/tests/content.test.js +90 -0
  221. package/dist/tests/content.test.js.map +1 -0
  222. package/dist/tests/documented-api.test.d.ts +1 -0
  223. package/dist/tests/documented-api.test.js +229 -0
  224. package/dist/tests/documented-api.test.js.map +1 -0
  225. package/dist/tests/explorer.test.d.ts +1 -0
  226. package/dist/tests/explorer.test.js +95 -0
  227. package/dist/tests/explorer.test.js.map +1 -0
  228. package/dist/tests/fee.test.d.ts +1 -0
  229. package/dist/tests/fee.test.js +179 -0
  230. package/dist/tests/fee.test.js.map +1 -0
  231. package/dist/tests/fixtures/build-vll.d.ts +71 -0
  232. package/dist/tests/fixtures/build-vll.js +141 -0
  233. package/dist/tests/fixtures/build-vll.js.map +1 -0
  234. package/dist/tests/fixtures/fake-socket.d.ts +44 -0
  235. package/dist/tests/fixtures/fake-socket.js +93 -0
  236. package/dist/tests/fixtures/fake-socket.js.map +1 -0
  237. package/dist/tests/fixtures/fake-transport.d.ts +24 -0
  238. package/dist/tests/fixtures/fake-transport.js +30 -0
  239. package/dist/tests/fixtures/fake-transport.js.map +1 -0
  240. package/dist/tests/keypair.test.d.ts +1 -0
  241. package/dist/tests/keypair.test.js +163 -0
  242. package/dist/tests/keypair.test.js.map +1 -0
  243. package/dist/tests/ledger.test.d.ts +1 -0
  244. package/dist/tests/ledger.test.js +234 -0
  245. package/dist/tests/ledger.test.js.map +1 -0
  246. package/dist/tests/mnemonic.test.d.ts +1 -0
  247. package/dist/tests/mnemonic.test.js +97 -0
  248. package/dist/tests/mnemonic.test.js.map +1 -0
  249. package/dist/tests/nft-transfer.test.d.ts +1 -0
  250. package/dist/tests/nft-transfer.test.js +170 -0
  251. package/dist/tests/nft-transfer.test.js.map +1 -0
  252. package/dist/tests/nft.test.d.ts +1 -0
  253. package/dist/tests/nft.test.js +180 -0
  254. package/dist/tests/nft.test.js.map +1 -0
  255. package/dist/tests/offline.test.d.ts +1 -0
  256. package/dist/tests/offline.test.js +95 -0
  257. package/dist/tests/offline.test.js.map +1 -0
  258. package/dist/tests/proxy.test.d.ts +1 -0
  259. package/dist/tests/proxy.test.js +183 -0
  260. package/dist/tests/proxy.test.js.map +1 -0
  261. package/dist/tests/select.test.d.ts +1 -0
  262. package/dist/tests/select.test.js +200 -0
  263. package/dist/tests/select.test.js.map +1 -0
  264. package/dist/tests/socket.test.d.ts +1 -0
  265. package/dist/tests/socket.test.js +607 -0
  266. package/dist/tests/socket.test.js.map +1 -0
  267. package/dist/tests/transfer.test.d.ts +1 -0
  268. package/dist/tests/transfer.test.js +225 -0
  269. package/dist/tests/transfer.test.js.map +1 -0
  270. package/dist/tests/wallet-address.test.d.ts +1 -0
  271. package/dist/tests/wallet-address.test.js +101 -0
  272. package/dist/tests/wallet-address.test.js.map +1 -0
  273. package/dist/tests/wasm-loader.jest.d.ts +13 -0
  274. package/dist/tests/wasm-loader.jest.js +15 -0
  275. package/dist/tests/wasm-loader.jest.js.map +1 -0
  276. package/example/.env.example +4 -0
  277. package/example/README.md +304 -0
  278. package/example/USAGE.md +74 -0
  279. package/example/index.html +12 -0
  280. package/example/package-lock.json +1557 -0
  281. package/example/package.json +25 -0
  282. package/example/scripts/browser-check.mjs +1421 -0
  283. package/example/scripts/match-derivation.mjs +65 -0
  284. package/example/src/App.vue +138 -0
  285. package/example/src/components/AuthorityPanel.vue +277 -0
  286. package/example/src/components/ChainPanel.vue +267 -0
  287. package/example/src/components/ChainState.vue +59 -0
  288. package/example/src/components/CopyButton.vue +25 -0
  289. package/example/src/components/CreateAssetPanel.vue +313 -0
  290. package/example/src/components/DigitalAssetsPanel.vue +68 -0
  291. package/example/src/components/EvidencePanel.vue +396 -0
  292. package/example/src/components/ExplorerPanel.vue +119 -0
  293. package/example/src/components/KeypairPanel.vue +182 -0
  294. package/example/src/components/PhrasePanel.vue +105 -0
  295. package/example/src/components/SocketWarning.vue +18 -0
  296. package/example/src/components/StepGate.vue +24 -0
  297. package/example/src/components/TransferPanel.vue +416 -0
  298. package/example/src/components/TxDeadline.vue +19 -0
  299. package/example/src/components/TxFee.vue +31 -0
  300. package/example/src/components/TxStatus.vue +105 -0
  301. package/example/src/composables/useSocketConnection.ts +80 -0
  302. package/example/src/env.d.ts +12 -0
  303. package/example/src/main.ts +5 -0
  304. package/example/src/pennies.ts +7 -0
  305. package/example/src/style.css +149 -0
  306. package/example/tsconfig.json +19 -0
  307. package/example/vite.config.ts +26 -0
  308. package/jest.config.js +18 -0
  309. package/package.json +2 -6
  310. package/scripts/copy-wasm.mjs +21 -0
  311. package/scripts/smoke.mjs +382 -0
  312. package/src/address/record-type.ts +77 -0
  313. package/src/address/wallet-address.ts +105 -0
  314. package/src/amount.ts +143 -0
  315. package/src/assert.ts +30 -0
  316. package/src/capnp/README.md +15 -0
  317. package/src/capnp/adminNetTx.capnp.ts +66 -0
  318. package/src/capnp/appLayBlock.capnp.ts +26 -0
  319. package/src/capnp/appLayLedger.capnp.ts +35 -0
  320. package/src/capnp/appLayRec.capnp.ts +159 -0
  321. package/src/capnp/chainStateView.capnp.ts +49 -0
  322. package/src/capnp/coinAcctRec.capnp.ts +193 -0
  323. package/src/capnp/genesisBlock.capnp.ts +56 -0
  324. package/src/capnp/hubContentStmt.capnp.ts +59 -0
  325. package/src/capnp/misc.capnp.ts +19 -0
  326. package/src/capnp/netActorRec.capnp.ts +64 -0
  327. package/src/capnp/nodeContentStmt.capnp.ts +62 -0
  328. package/src/capnp/nodePayloadList.capnp.ts +28 -0
  329. package/src/capnp/nodeTxList.capnp.ts +37 -0
  330. package/src/capnp/paramAcctRec.capnp.ts +669 -0
  331. package/src/capnp/payload.capnp.ts +261 -0
  332. package/src/capnp/proxyAcctRec.capnp.ts +259 -0
  333. package/src/capnp/relayContentStmt.capnp.ts +85 -0
  334. package/src/capnp/sysAcctRec.capnp.ts +47 -0
  335. package/src/capnp/userAppTx.capnp.ts +269 -0
  336. package/src/capnp/userValTx.capnp.ts +1999 -0
  337. package/src/capnp/valLayBlock.capnp.ts +93 -0
  338. package/src/capnp/valLayLedger.capnp.ts +80 -0
  339. package/src/chain/geeq-chain.ts +1094 -0
  340. package/src/chain/geeq-node.ts +105 -0
  341. package/src/chain/nonce-guard.ts +73 -0
  342. package/src/chain/select.ts +149 -0
  343. package/src/crypto/account-id.ts +83 -0
  344. package/src/crypto/keypair.ts +221 -0
  345. package/src/crypto/mnemonic.ts +69 -0
  346. package/src/encoding/base64.ts +58 -0
  347. package/src/encoding/hex.ts +42 -0
  348. package/src/errors.ts +122 -0
  349. package/src/index.ts +195 -0
  350. package/src/ledger/coin-accounts.ts +73 -0
  351. package/src/ledger/decode.ts +147 -0
  352. package/src/ledger/geeq-ledger.ts +184 -0
  353. package/src/ledger/net-actors.ts +129 -0
  354. package/src/ledger/nft-records.ts +49 -0
  355. package/src/ledger/params.ts +82 -0
  356. package/src/ledger/proxy-accounts.ts +51 -0
  357. package/src/net/discovery.ts +231 -0
  358. package/src/net/explorer.ts +123 -0
  359. package/src/net/read-ledger.ts +58 -0
  360. package/src/net/routes.ts +79 -0
  361. package/src/net/transport.ts +221 -0
  362. package/src/net/websocket.ts +94 -0
  363. package/src/runtime/bindings.ts +38 -0
  364. package/src/runtime/init.ts +49 -0
  365. package/src/runtime/wasm-loader.browser.ts +22 -0
  366. package/src/runtime/wasm-loader.node.ts +16 -0
  367. package/src/socket/geeq-socket.ts +827 -0
  368. package/src/socket/tracked-tx.ts +319 -0
  369. package/src/socket/tx-status.ts +131 -0
  370. package/src/tx/attestation.ts +222 -0
  371. package/src/tx/content.ts +82 -0
  372. package/src/tx/encode.ts +107 -0
  373. package/src/tx/expiry.ts +11 -0
  374. package/src/tx/fee.ts +31 -0
  375. package/src/tx/metadata.ts +100 -0
  376. package/src/tx/nft.ts +108 -0
  377. package/src/tx/proxy.ts +156 -0
  378. package/src/tx/transfer.ts +162 -0
  379. package/src/types.ts +503 -0
  380. package/src/wasm/README.md +22 -0
  381. package/src/wasm/browser/geeq_core.d.ts +1224 -0
  382. package/src/wasm/browser/geeq_core.js +6014 -0
  383. package/src/wasm/browser/geeq_core_bg.wasm +0 -0
  384. package/src/wasm/browser/geeq_core_bg.wasm.d.ts +447 -0
  385. package/src/wasm/nodejs/geeq_core.d.ts +753 -0
  386. package/src/wasm/nodejs/geeq_core.js +6016 -0
  387. package/src/wasm/nodejs/geeq_core_bg.wasm +0 -0
  388. package/src/wasm/nodejs/geeq_core_bg.wasm.d.ts +447 -0
  389. package/src/wasm/nodejs/package.json +3 -0
  390. package/tsconfig.build.json +8 -0
  391. package/tsconfig.json +21 -0
  392. package/tsup.config.ts +60 -0
@@ -0,0 +1,827 @@
1
+ import { MAX_UINT32, assertDuration } from '../assert'
2
+ import { originOf } from '../chain/select'
3
+ import { resolveAccountId } from '../crypto/account-id'
4
+ import type { AccountRef } from '../crypto/account-id'
5
+ import { fromBase64, toBase64 } from '../encoding/base64'
6
+ import {
7
+ GeeqError,
8
+ invalidArgument,
9
+ invalidKey,
10
+ socketClosed,
11
+ socketUnreachable,
12
+ } from '../errors'
13
+ import { socketRoute } from '../net/routes'
14
+ import {
15
+ WEBSOCKET_OPEN,
16
+ assertSocketSchemeReachable,
17
+ defaultWebSocketFactory,
18
+ opaqueSocketFailureMessage,
19
+ } from '../net/websocket'
20
+ import type { GeeqWebSocketFactory, GeeqWebSocketLike } from '../net/websocket'
21
+ import { GeeqTrackedTx, reportListenerError } from './tracked-tx'
22
+ import { parseTxStatus } from './tx-status'
23
+ import type { GeeqTxStatusEvent } from './tx-status'
24
+
25
+ const DEFAULT_OPEN_TIMEOUT_MS = 15_000
26
+ /** The edge proxy drops a socket idle for 60 s; the production wallet pings every 25. */
27
+ const DEFAULT_HEARTBEAT_MS = 25_000
28
+ const DEFAULT_RECONNECT_DELAY_MS = 1_000
29
+ const DEFAULT_MAX_RECONNECT_DELAY_MS = 30_000
30
+ /**
31
+ * A transaction targets a block no more than ten behind the chain's head, so after about
32
+ * ten block intervals it has either been decided or never will be. Five minutes covers
33
+ * that at the ~20 s interval with room for a slow block.
34
+ */
35
+ const DEFAULT_TRACK_TIMEOUT_MS = 5 * 60_000
36
+ /** Long enough for the next commit to carry a restarted node's buffer forward. */
37
+ const DEFAULT_NONSANE_GRACE_MS = 30_000
38
+ const KEY_BYTES = 32
39
+
40
+ /**
41
+ * - `connecting` — the first handshake, inside {@link GeeqSocket.connect}.
42
+ * - `open` — subscribed and receiving.
43
+ * - `reconnecting` — dropped after being open; retrying with backoff. Anything the node
44
+ * sends meanwhile is lost.
45
+ * - `closed` — disposed. Final.
46
+ */
47
+ export type GeeqSocketState = 'connecting' | 'open' | 'reconnecting' | 'closed'
48
+
49
+ /** One JSON frame from the node, as `{ event, payload }`. */
50
+ export interface GeeqSocketMessage {
51
+ readonly event: string
52
+ readonly payload: unknown
53
+ }
54
+
55
+ export interface GeeqSocketOptions {
56
+ /**
57
+ * The node the socket belongs to — normally `chain.endpoint`, which is also where the
58
+ * transactions go. Its socket is derived: `https://ip:2082` becomes
59
+ * `wss://ip:2082/ws/`.
60
+ *
61
+ * Supply this or {@link url}, not both. Use the node you submit to: only that node
62
+ * reports a `nonsane` refusal, though every node reports the final verdicts.
63
+ */
64
+ readonly endpoint?: string | URL
65
+ /**
66
+ * The socket URL itself, when it is not the one derived from the node — a local
67
+ * harness reaching the socket server directly on its own port, for instance.
68
+ */
69
+ readonly url?: string | URL
70
+ /** Defaults to `new globalThis.WebSocket(url)`. Supply one on Node 20 — see README. */
71
+ readonly webSocket?: GeeqWebSocketFactory
72
+ /**
73
+ * How long a handshake, or a send waiting on one, may take. Defaults to 15000. 0 waits
74
+ * for ever — including a first handshake that neither opens nor fails.
75
+ */
76
+ readonly openTimeoutMs?: number
77
+ /** How often to send the keepalive `'ping'`. Defaults to 25000; 0 disables it. */
78
+ readonly heartbeatMs?: number
79
+ /** Reconnect after a drop. Defaults to true. */
80
+ readonly reconnect?: boolean
81
+ /** The first reconnect delay, doubled on each failure. Defaults to 1000. */
82
+ readonly reconnectDelayMs?: number
83
+ /** The ceiling on that delay. Defaults to 30000. */
84
+ readonly maxReconnectDelayMs?: number
85
+ /** Abandons the first handshake. Once connected, use {@link GeeqSocket.dispose}. */
86
+ readonly signal?: AbortSignal
87
+ }
88
+
89
+ export interface GeeqSendOptions {
90
+ /**
91
+ * Re-send the frame on every reconnect. A node forgets a connection's subscriptions
92
+ * when it drops, so anything that subscribes should set this. Only the latest frame for
93
+ * each event name is kept.
94
+ */
95
+ readonly replay?: boolean
96
+ }
97
+
98
+ export interface GeeqListenOptions {
99
+ readonly blockNumber?: number
100
+ /** Defaults to true. */
101
+ readonly now?: boolean
102
+ }
103
+
104
+ export interface GeeqTrackOptions {
105
+ /** How long to wait for a final status. Defaults to 300000. 0 waits for ever. */
106
+ readonly timeoutMs?: number
107
+ /** How long a `nonsane` may be overturned by a real verdict. Defaults to 30000. */
108
+ readonly nonsaneGraceMs?: number
109
+ /** More accounts to subscribe alongside the signer. */
110
+ readonly accounts?: readonly (AccountRef | string)[]
111
+ }
112
+
113
+ /**
114
+ * Anything with the hash a status event names. Every transaction this SDK builds fits,
115
+ * and brings its signer's `publicKey` for the subscription.
116
+ */
117
+ export interface GeeqTrackable {
118
+ readonly transactionHash: Uint8Array
119
+ readonly publicKey?: Uint8Array
120
+ }
121
+
122
+ interface OpenWaiter {
123
+ resolve(): void
124
+ reject(error: unknown): void
125
+ }
126
+
127
+ type Listener<T> = (value: T) => void
128
+
129
+ function dispatch<T>(listeners: ReadonlySet<Listener<T>> | undefined, value: T): void {
130
+ if (listeners === undefined) return
131
+ // A copy, so a listener that removes itself does not skip its neighbour.
132
+ for (const listener of [...listeners]) {
133
+ try {
134
+ listener(value)
135
+ } catch (error) {
136
+ reportListenerError(error)
137
+ }
138
+ }
139
+ }
140
+
141
+ function socketUrlOf(options: GeeqSocketOptions): string {
142
+ const raw =
143
+ options.url !== undefined
144
+ ? String(options.url)
145
+ : socketRoute(originOf(options.endpoint as string | URL))
146
+ let parsed: URL
147
+ try {
148
+ parsed = new URL(raw)
149
+ } catch (cause) {
150
+ throw invalidArgument(
151
+ 'url must be an absolute socket URL such as wss://1.2.3.4:2082/ws/, received ' +
152
+ `${JSON.stringify(raw)}.`,
153
+ cause,
154
+ )
155
+ }
156
+ if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
157
+ throw invalidArgument(
158
+ `url must use ws:// or wss://, received ${JSON.stringify(raw)}. To derive the socket ` +
159
+ 'from a node URL, pass it as endpoint instead.',
160
+ )
161
+ }
162
+ return parsed.href
163
+ }
164
+
165
+ /**
166
+ * Every key a node might file an account's events under.
167
+ *
168
+ * Nodes have keyed per-account events by the base64 public key and, since 2026-09, by the
169
+ * base64 account id; which one a socket server matches is not visible from here. So an
170
+ * account named by its public key is subscribed under both, as the production wallet does.
171
+ */
172
+ function keysOf(ref: AccountRef | string): string[] {
173
+ if (typeof ref === 'string') {
174
+ const bytes = fromBase64(ref, 'account key')
175
+ if (bytes.length !== KEY_BYTES) {
176
+ throw invalidArgument(
177
+ `An account key must be the base64 of a ${KEY_BYTES}-byte public key or account id, ` +
178
+ `received ${bytes.length} bytes.`,
179
+ )
180
+ }
181
+ return [ref]
182
+ }
183
+
184
+ const accountId = toBase64(resolveAccountId(ref))
185
+ if (ref instanceof Uint8Array) return [accountId]
186
+
187
+ const { publicKey } = ref as { publicKey?: unknown }
188
+ if (publicKey === undefined) return [accountId]
189
+ if (!(publicKey instanceof Uint8Array) || publicKey.length !== KEY_BYTES) {
190
+ throw invalidKey(`publicKey must be ${KEY_BYTES} bytes.`)
191
+ }
192
+ return [toBase64(publicKey), accountId]
193
+ }
194
+
195
+ function closeQuietly(socket: GeeqWebSocketLike | null): void {
196
+ if (socket === null) return
197
+ try {
198
+ socket.close()
199
+ } catch {
200
+ // Already closing, or never opened. Either way there is nothing left to release.
201
+ }
202
+ }
203
+
204
+ /**
205
+ * A node's WebSocket: the channel through which it reports what happened to the
206
+ * transactions you sent, and pushes account and chain updates.
207
+ *
208
+ * One class for every use an application has for the socket:
209
+ *
210
+ * - {@link track} a transaction you are about to submit, and get its verdict;
211
+ * - {@link subscribe} to accounts and read their events with {@link on};
212
+ * - {@link send} any frame the node understands, and receive any it sends.
213
+ *
214
+ * The protocol's hazards are handled here, once: subscriptions replace rather than add,
215
+ * so the full set of keys is always sent; the node forgets a connection's subscriptions
216
+ * when it drops, so they are replayed on every reconnect before anything is announced;
217
+ * the edge proxy drops an idle socket, so it is kept alive; a transaction's first status
218
+ * is not always its last, so {@link GeeqTrackedTx} decides when it is final.
219
+ *
220
+ * ```ts
221
+ * const socket = await GeeqSocket.connect({ endpoint: chain.endpoint })
222
+ * const tx = await chain.createTransfer({ from, to, amount })
223
+ * const tracked = await socket.track(tx) // before submitting — always
224
+ * const response = await fetch(submitRoute(tx.nodeUrl), { … })
225
+ * if (!response.ok) tracked.refused({ status: response.status })
226
+ * const outcome = await tracked.settled()
227
+ * ```
228
+ *
229
+ * In a reactive framework, mark an instance raw (Vue's `markRaw`) rather than letting it
230
+ * be proxied — a proxied native WebSocket throws on every call.
231
+ */
232
+ export class GeeqSocket {
233
+ /** The socket URL, normalised. */
234
+ readonly url: string
235
+
236
+ // TS-private and non-enumerable, never `#private` — see GeeqNode for why.
237
+ private readonly factory!: GeeqWebSocketFactory
238
+ private readonly openTimeoutMs!: number
239
+ private readonly heartbeatMs!: number
240
+ private readonly reconnects!: boolean
241
+ private readonly reconnectDelayMs!: number
242
+ private readonly maxReconnectDelayMs!: number
243
+ /** The frames in force, by event name, replayed on every connection. */
244
+ private readonly persisted!: Map<string, string>
245
+ /** What this connection has already been told, so a repeat is not sent twice. */
246
+ private readonly told!: Map<string, string>
247
+ private readonly keys!: Set<string>
248
+ private readonly tracked!: Map<string, GeeqTrackedTx>
249
+ private readonly eventListeners!: Map<string, Set<Listener<unknown>>>
250
+ private readonly messageListeners!: Set<Listener<GeeqSocketMessage>>
251
+ private readonly statusListeners!: Set<Listener<GeeqTxStatusEvent>>
252
+ private readonly stateListeners!: Set<Listener<GeeqSocketState>>
253
+ private readonly waiters!: Set<OpenWaiter>
254
+ private socket!: GeeqWebSocketLike | null
255
+ private current!: GeeqSocketState
256
+ private everOpened!: boolean
257
+ private attempts!: number
258
+ private openTimer!: ReturnType<typeof setTimeout> | null
259
+ private reconnectTimer!: ReturnType<typeof setTimeout> | null
260
+ private heartbeatTimer!: ReturnType<typeof setInterval> | null
261
+
262
+ /** @internal */
263
+ constructor(init: {
264
+ url: string
265
+ factory: GeeqWebSocketFactory
266
+ openTimeoutMs: number
267
+ heartbeatMs: number
268
+ reconnect: boolean
269
+ reconnectDelayMs: number
270
+ maxReconnectDelayMs: number
271
+ }) {
272
+ this.url = init.url
273
+
274
+ const hidden = (key: string, value: unknown, writable = false): void => {
275
+ Object.defineProperty(this, key, { value, enumerable: false, writable })
276
+ }
277
+ hidden('factory', init.factory)
278
+ hidden('openTimeoutMs', init.openTimeoutMs)
279
+ hidden('heartbeatMs', init.heartbeatMs)
280
+ hidden('reconnects', init.reconnect)
281
+ hidden('reconnectDelayMs', init.reconnectDelayMs)
282
+ hidden('maxReconnectDelayMs', init.maxReconnectDelayMs)
283
+ hidden('persisted', new Map())
284
+ hidden('told', new Map())
285
+ hidden('keys', new Set())
286
+ hidden('tracked', new Map())
287
+ hidden('eventListeners', new Map())
288
+ hidden('messageListeners', new Set())
289
+ hidden('statusListeners', new Set())
290
+ hidden('stateListeners', new Set())
291
+ hidden('waiters', new Set())
292
+ hidden('socket', null, true)
293
+ hidden('current', 'connecting', true)
294
+ hidden('everOpened', false, true)
295
+ hidden('attempts', 0, true)
296
+ hidden('openTimer', null, true)
297
+ hidden('reconnectTimer', null, true)
298
+ hidden('heartbeatTimer', null, true)
299
+ }
300
+
301
+ /**
302
+ * Open a node's socket and resolve once it is open.
303
+ *
304
+ * Throws `socket-unreachable` if the first handshake fails, rather than retrying
305
+ * quietly: a socket that never opened is almost always a certificate, a mixed-content
306
+ * page or a wrong URL, and retrying hides which. After that first open, a drop is
307
+ * retried automatically and reported through {@link onConnectionChange}.
308
+ */
309
+ static async connect(options: GeeqSocketOptions): Promise<GeeqSocket> {
310
+ if ((options.endpoint === undefined) === (options.url === undefined)) {
311
+ throw invalidArgument(
312
+ 'Provide exactly one of endpoint (the node you submit to — its socket URL is ' +
313
+ 'derived from it) or url (a socket URL, such as ws://10.0.0.1:3100/ws/ for a ' +
314
+ 'local harness).',
315
+ )
316
+ }
317
+ assertDuration(options.openTimeoutMs, 'openTimeoutMs')
318
+ assertDuration(options.heartbeatMs, 'heartbeatMs')
319
+ assertDuration(options.reconnectDelayMs, 'reconnectDelayMs')
320
+ assertDuration(options.maxReconnectDelayMs, 'maxReconnectDelayMs')
321
+
322
+ const url = socketUrlOf(options)
323
+ assertSocketSchemeReachable(url)
324
+
325
+ const { signal } = options
326
+ if (signal?.aborted === true) throw signal.reason
327
+
328
+ const socket = new GeeqSocket({
329
+ url,
330
+ factory: options.webSocket ?? defaultWebSocketFactory,
331
+ openTimeoutMs: options.openTimeoutMs ?? DEFAULT_OPEN_TIMEOUT_MS,
332
+ heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,
333
+ reconnect: options.reconnect ?? true,
334
+ reconnectDelayMs: options.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS,
335
+ maxReconnectDelayMs: options.maxReconnectDelayMs ?? DEFAULT_MAX_RECONNECT_DELAY_MS,
336
+ })
337
+
338
+ let onAbort: (() => void) | undefined
339
+ try {
340
+ await new Promise<void>((resolve, reject) => {
341
+ // No timer of its own: the handshake's timeout is the deadline.
342
+ socket.waiters.add({ resolve, reject })
343
+ if (signal !== undefined) {
344
+ onAbort = () => reject(signal.reason)
345
+ signal.addEventListener('abort', onAbort)
346
+ }
347
+ socket.dial()
348
+ })
349
+ } catch (error) {
350
+ socket.dispose()
351
+ throw error
352
+ } finally {
353
+ if (onAbort !== undefined) signal?.removeEventListener('abort', onAbort)
354
+ }
355
+ return socket
356
+ }
357
+
358
+ /** Where the connection stands. See {@link GeeqSocketState}. */
359
+ get state(): GeeqSocketState {
360
+ return this.current
361
+ }
362
+
363
+ /** Every account key subscribed so far, base64. The set only grows. */
364
+ get subscriptions(): readonly string[] {
365
+ return [...this.keys]
366
+ }
367
+
368
+ /**
369
+ * Subscribe to accounts' events: their transactions' statuses and record updates.
370
+ *
371
+ * Takes a keypair, a `{ publicKey }`, a 32-byte account id, a base64 key, or a list of
372
+ * them. The node has no unsubscribe and treats every subscription as a replacement, so
373
+ * this adds to the set this socket holds and sends all of it — calling it for one
374
+ * account never silences another. Resolves once the node has been told.
375
+ */
376
+ async subscribe(
377
+ accounts: AccountRef | string | readonly (AccountRef | string)[],
378
+ ): Promise<void> {
379
+ this.assertOpenable('subscribe')
380
+ const list = (Array.isArray(accounts) ? accounts : [accounts]) as readonly (
381
+ | AccountRef
382
+ | string
383
+ )[]
384
+ // Resolve every key before adding any, so one bad entry does not half-apply the list.
385
+ const resolved = list.flatMap(keysOf)
386
+ for (const key of resolved) this.keys.add(key)
387
+ return this.send('client', { publicKeys: [...this.keys] }, { replay: true })
388
+ }
389
+
390
+ /** Ask for events from now, or from a block. Replayed on reconnect. */
391
+ async listen(options: GeeqListenOptions = {}): Promise<void> {
392
+ const { blockNumber } = options
393
+ if (
394
+ blockNumber !== undefined &&
395
+ (!Number.isInteger(blockNumber) || blockNumber < 0 || blockNumber > MAX_UINT32)
396
+ ) {
397
+ throw invalidArgument(
398
+ `blockNumber must be a block number, received ${String(blockNumber)}.`,
399
+ )
400
+ }
401
+ return this.send(
402
+ 'listen',
403
+ { ...(blockNumber === undefined ? {} : { blockNumber }), now: options.now ?? true },
404
+ { replay: true },
405
+ )
406
+ }
407
+
408
+ /**
409
+ * Subscribe to the whole chain's record feed — every account, fee and parameter update.
410
+ * What a block explorer or an indexer wants; an ordinary application does not.
411
+ */
412
+ async subscribeBlockExplorer(): Promise<void> {
413
+ return this.send('block-explorer', undefined, { replay: true })
414
+ }
415
+
416
+ /**
417
+ * Send a frame, `{ event, payload }`, waiting for the connection if it is down.
418
+ *
419
+ * The escape hatch for anything the node understands that this class has no method
420
+ * for. Rejects with `socket-unreachable` if the socket does not reopen within
421
+ * `openTimeoutMs` — a replayed frame is still kept, and goes out when it does.
422
+ */
423
+ async send(event: string, payload?: unknown, options: GeeqSendOptions = {}): Promise<void> {
424
+ this.assertOpenable('send')
425
+ if (typeof event !== 'string' || event === '') {
426
+ throw invalidArgument('event must be a non-empty string.')
427
+ }
428
+ const frame = JSON.stringify(payload === undefined ? { event } : { event, payload })
429
+ const replay = options.replay === true
430
+ if (replay) this.persisted.set(event, frame)
431
+
432
+ // Resolves only once the frame has actually gone out. A socket can be open by our
433
+ // books and already closing by its own — its close event not yet delivered — and a
434
+ // subscription reported as sent when it was not would break track()'s promise.
435
+ for (;;) {
436
+ await this.whenOpen()
437
+ // A newer frame for the same event may have replaced this one while we waited; the
438
+ // node must end up with the newest, and a replacement makes the older one moot.
439
+ const latest = replay ? (this.persisted.get(event) ?? frame) : frame
440
+ if (this.transmit(event, latest, replay)) return
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Follow a transaction to its verdict. Call it **before** submitting.
446
+ *
447
+ * Subscribes the signer — the node pushes a transaction's statuses only to connections
448
+ * subscribed to its account — and resolves once the node has been told, so a status
449
+ * cannot arrive before anyone is listening for it. If this throws, do not submit: a
450
+ * transaction sent with no way to hear its verdict leaves the user guessing.
451
+ *
452
+ * Tracking the same transaction twice returns the same handle.
453
+ */
454
+ async track(tx: GeeqTrackable, options: GeeqTrackOptions = {}): Promise<GeeqTrackedTx> {
455
+ this.assertOpenable('track')
456
+ const hash = (tx as { transactionHash?: unknown } | null)?.transactionHash
457
+ if (!(hash instanceof Uint8Array) || hash.length !== KEY_BYTES) {
458
+ throw invalidArgument(
459
+ `tx must carry a ${KEY_BYTES}-byte transactionHash — pass the transaction the SDK ` +
460
+ 'built.',
461
+ )
462
+ }
463
+ assertDuration(options.timeoutMs, 'timeoutMs')
464
+ assertDuration(options.nonsaneGraceMs, 'nonsaneGraceMs')
465
+
466
+ const accounts: (AccountRef | string)[] = [
467
+ ...(tx.publicKey === undefined ? [] : [{ publicKey: tx.publicKey }]),
468
+ ...(options.accounts ?? []),
469
+ ]
470
+ if (accounts.length === 0) {
471
+ throw invalidArgument(
472
+ 'This transaction does not say who signed it, so there is no account to subscribe ' +
473
+ 'to for its status. Pass the signer in accounts.',
474
+ )
475
+ }
476
+
477
+ const id = toBase64(hash)
478
+ const existing = this.tracked.get(id)
479
+ const tracked =
480
+ existing ??
481
+ new GeeqTrackedTx({
482
+ id,
483
+ timeoutMs: options.timeoutMs ?? DEFAULT_TRACK_TIMEOUT_MS,
484
+ nonsaneGraceMs: options.nonsaneGraceMs ?? DEFAULT_NONSANE_GRACE_MS,
485
+ detach: () => {
486
+ if (this.tracked.get(id) === tracked) this.tracked.delete(id)
487
+ },
488
+ })
489
+ // Registered before the subscription goes out, so nothing can slip between them.
490
+ this.tracked.set(id, tracked)
491
+
492
+ try {
493
+ await this.subscribe(accounts)
494
+ } catch (error) {
495
+ if (existing === undefined) tracked.release()
496
+ throw error
497
+ }
498
+ return tracked
499
+ }
500
+
501
+ /**
502
+ * Call `listener` with the payload of every frame named `event`, whatever it is —
503
+ * `'uut_status'`, `'coin-account-update'`, `'fee-update'`, or one this SDK has never
504
+ * heard of. Returns the function that removes it.
505
+ */
506
+ on(event: string, listener: (payload: unknown) => void): () => void {
507
+ let listeners = this.eventListeners.get(event)
508
+ if (listeners === undefined) {
509
+ listeners = new Set()
510
+ this.eventListeners.set(event, listeners)
511
+ }
512
+ const set = listeners
513
+ set.add(listener)
514
+ return () => {
515
+ set.delete(listener)
516
+ if (set.size === 0 && this.eventListeners.get(event) === set) {
517
+ this.eventListeners.delete(event)
518
+ }
519
+ }
520
+ }
521
+
522
+ /** Call `listener` with every JSON frame the node sends. */
523
+ onMessage(listener: (message: GeeqSocketMessage) => void): () => void {
524
+ this.messageListeners.add(listener)
525
+ return () => {
526
+ this.messageListeners.delete(listener)
527
+ }
528
+ }
529
+
530
+ /** Call `listener` with every transaction status, tracked or not, already parsed. */
531
+ onTxStatus(listener: (event: GeeqTxStatusEvent) => void): () => void {
532
+ this.statusListeners.add(listener)
533
+ return () => {
534
+ this.statusListeners.delete(listener)
535
+ }
536
+ }
537
+
538
+ /** Call `listener` whenever {@link state} changes. */
539
+ onConnectionChange(listener: (state: GeeqSocketState) => void): () => void {
540
+ this.stateListeners.add(listener)
541
+ return () => {
542
+ this.stateListeners.delete(listener)
543
+ }
544
+ }
545
+
546
+ /**
547
+ * Close for good. Pending sends reject with `socket-closed`, and every transaction still
548
+ * being tracked settles `unknown` with reason `closed` — it may yet be decided, just
549
+ * not heard about here.
550
+ */
551
+ dispose(): void {
552
+ if (this.current === 'closed') return
553
+ this.setState('closed')
554
+
555
+ for (const timer of [this.openTimer, this.reconnectTimer]) {
556
+ if (timer !== null) clearTimeout(timer)
557
+ }
558
+ this.openTimer = null
559
+ this.reconnectTimer = null
560
+ this.stopHeartbeat()
561
+
562
+ const socket = this.socket
563
+ this.socket = null
564
+ closeQuietly(socket)
565
+
566
+ const closed = socketClosed(`The socket at ${this.url} was disposed.`)
567
+ for (const waiter of [...this.waiters]) waiter.reject(closed)
568
+ this.waiters.clear()
569
+
570
+ for (const tracked of [...this.tracked.values()]) {
571
+ tracked.abandon(
572
+ 'closed',
573
+ `The socket was closed while transaction ${tracked.id} was pending. It may still be ` +
574
+ 'decided; check the ledger before sending it again.',
575
+ )
576
+ }
577
+ this.tracked.clear()
578
+
579
+ this.eventListeners.clear()
580
+ this.messageListeners.clear()
581
+ this.statusListeners.clear()
582
+ this.stateListeners.clear()
583
+ }
584
+
585
+ toJSON(): Record<string, unknown> {
586
+ return {
587
+ url: this.url,
588
+ state: this.current,
589
+ subscriptions: this.keys.size,
590
+ tracking: this.tracked.size,
591
+ }
592
+ }
593
+
594
+ toString(): string {
595
+ return `GeeqSocket(${this.url}, ${this.current})`
596
+ }
597
+
598
+ private assertOpenable(what: string): void {
599
+ if (this.current === 'closed') {
600
+ throw socketClosed(
601
+ `${what}() was called on a disposed socket (${this.url}). Connect a new one.`,
602
+ )
603
+ }
604
+ }
605
+
606
+ private setState(next: GeeqSocketState): void {
607
+ if (this.current === next) return
608
+ this.current = next
609
+ dispatch(this.stateListeners, next)
610
+ }
611
+
612
+ /** Resolves when the socket is open, or rejects after `openTimeoutMs` or on dispose. */
613
+ private whenOpen(): Promise<void> {
614
+ if (this.current === 'open') return Promise.resolve()
615
+ if (this.current === 'closed') {
616
+ return Promise.reject(socketClosed(`The socket at ${this.url} was disposed.`))
617
+ }
618
+
619
+ return new Promise<void>((resolve, reject) => {
620
+ let timer: ReturnType<typeof setTimeout> | null = null
621
+ const waiter: OpenWaiter = {
622
+ resolve: () => {
623
+ if (timer !== null) clearTimeout(timer)
624
+ resolve()
625
+ },
626
+ reject: (error) => {
627
+ if (timer !== null) clearTimeout(timer)
628
+ reject(error)
629
+ },
630
+ }
631
+ if (this.openTimeoutMs > 0) {
632
+ timer = setTimeout(() => {
633
+ this.waiters.delete(waiter)
634
+ reject(
635
+ socketUnreachable(
636
+ `The socket at ${this.url} is ${this.current} and did not reopen within ` +
637
+ `${this.openTimeoutMs}ms. It keeps retrying; anything sent with replay ` +
638
+ 'will go out when it does.',
639
+ ),
640
+ )
641
+ }, this.openTimeoutMs)
642
+ }
643
+ this.waiters.add(waiter)
644
+ })
645
+ }
646
+
647
+ private dial(): void {
648
+ let socket: GeeqWebSocketLike
649
+ try {
650
+ socket = this.factory(this.url)
651
+ } catch (cause) {
652
+ this.attemptFailed(
653
+ cause instanceof GeeqError
654
+ ? cause
655
+ : socketUnreachable(opaqueSocketFailureMessage(this.url), cause),
656
+ )
657
+ return
658
+ }
659
+
660
+ this.socket = socket
661
+ // A new connection has been told nothing, so nothing may be suppressed as a repeat.
662
+ this.told.clear()
663
+
664
+ if (this.openTimeoutMs > 0) {
665
+ this.openTimer = setTimeout(() => {
666
+ if (this.socket !== socket) return
667
+ this.socket = null
668
+ closeQuietly(socket)
669
+ this.attemptFailed(
670
+ socketUnreachable(
671
+ `The socket at ${this.url} did not open within ${this.openTimeoutMs}ms. ` +
672
+ opaqueSocketFailureMessage(this.url),
673
+ ),
674
+ )
675
+ }, this.openTimeoutMs)
676
+ }
677
+
678
+ // A superseded socket can still fire; only the current one may drive state.
679
+ socket.addEventListener('open', () => {
680
+ if (this.socket === socket) this.opened()
681
+ })
682
+ socket.addEventListener('message', (event) => {
683
+ if (this.socket === socket) this.receive(event.data)
684
+ })
685
+ socket.addEventListener('close', () => {
686
+ if (this.socket !== socket) return
687
+ this.socket = null
688
+ this.attemptFailed(socketUnreachable(opaqueSocketFailureMessage(this.url)))
689
+ })
690
+ socket.addEventListener('error', () => {
691
+ // A 'close' always follows, and that is where the drop is handled. The listener
692
+ // must still exist: the `ws` package throws an unhandled 'error' otherwise.
693
+ })
694
+ }
695
+
696
+ private opened(): void {
697
+ if (this.openTimer !== null) clearTimeout(this.openTimer)
698
+ this.openTimer = null
699
+ this.everOpened = true
700
+
701
+ // Restore everything this connection should hold before announcing it, so a listener
702
+ // reacting to 'open' finds the subscriptions already in force. If it drops part way,
703
+ // the next connection replays the lot.
704
+ for (const [event, frame] of this.persisted) {
705
+ if (!this.transmit(event, frame, true)) return
706
+ }
707
+ // Only now is it a working connection. Resetting the backoff any earlier would let a
708
+ // connection that dies while its subscriptions are replayed be redialled every second
709
+ // for ever.
710
+ this.attempts = 0
711
+ this.startHeartbeat()
712
+ this.setState('open')
713
+
714
+ for (const waiter of [...this.waiters]) waiter.resolve()
715
+ this.waiters.clear()
716
+ }
717
+
718
+ /** A handshake failed, or an open socket dropped. */
719
+ private attemptFailed(error: GeeqError): void {
720
+ if (this.openTimer !== null) clearTimeout(this.openTimer)
721
+ this.openTimer = null
722
+ this.stopHeartbeat()
723
+ if (this.current === 'closed') return
724
+
725
+ if (!this.everOpened) {
726
+ // Still inside connect(): report it, and let connect() dispose.
727
+ for (const waiter of [...this.waiters]) waiter.reject(error)
728
+ this.waiters.clear()
729
+ return
730
+ }
731
+
732
+ if (!this.reconnects) {
733
+ this.dispose()
734
+ return
735
+ }
736
+
737
+ this.setState('reconnecting')
738
+ if (this.reconnectTimer !== null) return
739
+ const delay = Math.min(this.reconnectDelayMs * 2 ** this.attempts, this.maxReconnectDelayMs)
740
+ this.attempts += 1
741
+ this.reconnectTimer = setTimeout(() => {
742
+ this.reconnectTimer = null
743
+ if (this.current !== 'closed') this.dial()
744
+ }, delay)
745
+ }
746
+
747
+ /**
748
+ * Put one frame on the current connection. True once it is there — or already was.
749
+ *
750
+ * False means the connection is gone. If our books still said it was open, it is
751
+ * treated as the drop it is, here and now, so the caller waits for the next connection
752
+ * instead of spinning on this one.
753
+ */
754
+ private transmit(event: string, frame: string, dedupe: boolean): boolean {
755
+ // The node treats every subscription frame as a fresh subscribe, so an identical
756
+ // repeat on the same connection is dropped here instead.
757
+ if (dedupe && this.told.get(event) === frame) return true
758
+
759
+ const socket = this.socket
760
+ if (socket === null) return false
761
+ if (socket.readyState === WEBSOCKET_OPEN) {
762
+ try {
763
+ socket.send(frame)
764
+ if (dedupe) this.told.set(event, frame)
765
+ return true
766
+ } catch {
767
+ // Closing underneath us. Handled below, as for any other drop.
768
+ }
769
+ }
770
+
771
+ this.socket = null
772
+ closeQuietly(socket)
773
+ this.attemptFailed(socketUnreachable(opaqueSocketFailureMessage(this.url)))
774
+ return false
775
+ }
776
+
777
+ private receive(data: unknown): void {
778
+ const text =
779
+ typeof data === 'string'
780
+ ? data
781
+ : data instanceof ArrayBuffer || ArrayBuffer.isView(data)
782
+ ? new TextDecoder().decode(data)
783
+ : undefined
784
+ if (text === undefined) return
785
+
786
+ let parsed: unknown
787
+ try {
788
+ parsed = JSON.parse(text)
789
+ } catch {
790
+ // Heartbeat replies and other non-JSON frames are not events.
791
+ return
792
+ }
793
+ if (parsed === null || typeof parsed !== 'object') return
794
+ const { event, payload } = parsed as { event?: unknown; payload?: unknown }
795
+ if (typeof event !== 'string') return
796
+
797
+ dispatch(this.messageListeners, Object.freeze({ event, payload }))
798
+ dispatch(this.eventListeners.get(event), payload)
799
+
800
+ if (event !== 'uut_status') return
801
+ const status = parseTxStatus(payload)
802
+ if (status === undefined) return
803
+ dispatch(this.statusListeners, status)
804
+ this.tracked.get(status.id)?.receive(status)
805
+ }
806
+
807
+ private startHeartbeat(): void {
808
+ this.stopHeartbeat()
809
+ if (this.heartbeatMs <= 0) return
810
+ this.heartbeatTimer = setInterval(() => {
811
+ const socket = this.socket
812
+ if (socket === null || socket.readyState !== WEBSOCKET_OPEN) return
813
+ try {
814
+ // A raw text frame, not JSON: there is no protocol ping, and a browser cannot send
815
+ // a control frame. Any reply is non-JSON and ignored.
816
+ socket.send('ping')
817
+ } catch {
818
+ // The close handler reconnects.
819
+ }
820
+ }, this.heartbeatMs)
821
+ }
822
+
823
+ private stopHeartbeat(): void {
824
+ if (this.heartbeatTimer !== null) clearInterval(this.heartbeatTimer)
825
+ this.heartbeatTimer = null
826
+ }
827
+ }