@hsuite/native-connect-angular 1.0.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 (276) hide show
  1. package/README.md +48 -0
  2. package/USAGE_EXAMPLES.md +476 -0
  3. package/assets/wallets/extension.svg +7 -0
  4. package/assets/wallets/hashpack.svg +6 -0
  5. package/assets/wallets/hsuite.svg +11 -0
  6. package/assets/wallets/kabila.svg +11 -0
  7. package/assets/wallets/walletconnect.svg +13 -0
  8. package/coverage/base.css +224 -0
  9. package/coverage/block-navigation.js +87 -0
  10. package/coverage/coverage-summary.json +50 -0
  11. package/coverage/favicon.png +0 -0
  12. package/coverage/index.html +476 -0
  13. package/coverage/lcov-report/base.css +224 -0
  14. package/coverage/lcov-report/block-navigation.js +87 -0
  15. package/coverage/lcov-report/favicon.png +0 -0
  16. package/coverage/lcov-report/index.html +476 -0
  17. package/coverage/lcov-report/lib/components/account-selector/account-actions/account-actions.component.ts.html +868 -0
  18. package/coverage/lcov-report/lib/components/account-selector/account-actions/index.html +116 -0
  19. package/coverage/lcov-report/lib/components/account-selector/account-filter/account-filter.component.ts.html +1288 -0
  20. package/coverage/lcov-report/lib/components/account-selector/account-filter/index.html +116 -0
  21. package/coverage/lcov-report/lib/components/account-selector/account-formatting.service.ts.html +685 -0
  22. package/coverage/lcov-report/lib/components/account-selector/account-grouping.service.ts.html +766 -0
  23. package/coverage/lcov-report/lib/components/account-selector/account-list/account-list.component.ts.html +1495 -0
  24. package/coverage/lcov-report/lib/components/account-selector/account-list/index.html +116 -0
  25. package/coverage/lcov-report/lib/components/account-selector/account-selector.component.ts.html +1495 -0
  26. package/coverage/lcov-report/lib/components/account-selector/account-selector.service.ts.html +1588 -0
  27. package/coverage/lcov-report/lib/components/account-selector/index.html +161 -0
  28. package/coverage/lcov-report/lib/components/wallet-account-display/index.html +116 -0
  29. package/coverage/lcov-report/lib/components/wallet-account-display/wallet-account-display.component.ts.html +505 -0
  30. package/coverage/lcov-report/lib/components/wallet-connect-button/index.html +116 -0
  31. package/coverage/lcov-report/lib/components/wallet-connect-button/wallet-connect-button.component.ts.html +805 -0
  32. package/coverage/lcov-report/lib/components/wallet-connect-prompt/index.html +116 -0
  33. package/coverage/lcov-report/lib/components/wallet-connect-prompt/wallet-connect-prompt.component.ts.html +409 -0
  34. package/coverage/lcov-report/lib/components/wallet-connected-guard/index.html +116 -0
  35. package/coverage/lcov-report/lib/components/wallet-connected-guard/wallet-connected-guard.component.ts.html +304 -0
  36. package/coverage/lcov-report/lib/components/wallet-connection-modal/connection-method-step/connection-method-step.component.ts.html +436 -0
  37. package/coverage/lcov-report/lib/components/wallet-connection-modal/connection-method-step/index.html +116 -0
  38. package/coverage/lcov-report/lib/components/wallet-connection-modal/index.html +116 -0
  39. package/coverage/lcov-report/lib/components/wallet-connection-modal/qr-pairing-step/index.html +116 -0
  40. package/coverage/lcov-report/lib/components/wallet-connection-modal/qr-pairing-step/qr-pairing-step.component.ts.html +2287 -0
  41. package/coverage/lcov-report/lib/components/wallet-connection-modal/wallet-connection-modal.component.ts.html +2275 -0
  42. package/coverage/lcov-report/lib/components/wallet-session-display/index.html +116 -0
  43. package/coverage/lcov-report/lib/components/wallet-session-display/wallet-session-display.component.ts.html +676 -0
  44. package/coverage/lcov-report/lib/components/wallet-transaction-status/index.html +116 -0
  45. package/coverage/lcov-report/lib/components/wallet-transaction-status/wallet-transaction-status.component.ts.html +703 -0
  46. package/coverage/lcov-report/lib/directives/index.html +146 -0
  47. package/coverage/lcov-report/lib/directives/wallet-connected.directive.ts.html +670 -0
  48. package/coverage/lcov-report/lib/directives/wallet-context.directive.ts.html +547 -0
  49. package/coverage/lcov-report/lib/directives/wallet-events.directive.ts.html +781 -0
  50. package/coverage/lcov-report/lib/hsuite-wallet.module.ts.html +715 -0
  51. package/coverage/lcov-report/lib/index.html +116 -0
  52. package/coverage/lcov-report/lib/models/connection-config.model.ts.html +280 -0
  53. package/coverage/lcov-report/lib/models/index.html +131 -0
  54. package/coverage/lcov-report/lib/models/provider-types.ts.html +577 -0
  55. package/coverage/lcov-report/lib/providers/base-wallet-provider.ts.html +1138 -0
  56. package/coverage/lcov-report/lib/providers/hsuite-native/channel-client.service.ts.html +2671 -0
  57. package/coverage/lcov-report/lib/providers/hsuite-native/index.html +116 -0
  58. package/coverage/lcov-report/lib/providers/hsuite-native-provider.ts.html +2347 -0
  59. package/coverage/lcov-report/lib/providers/index.html +146 -0
  60. package/coverage/lcov-report/lib/providers/p2p-native/index.html +131 -0
  61. package/coverage/lcov-report/lib/providers/p2p-native/p2p-native.provider.ts.html +2254 -0
  62. package/coverage/lcov-report/lib/providers/p2p-native/p2p-session-manager.ts.html +2170 -0
  63. package/coverage/lcov-report/lib/providers/wallet-error-handler.ts.html +1132 -0
  64. package/coverage/lcov-report/lib/providers/walletconnect/core/index.html +176 -0
  65. package/coverage/lcov-report/lib/providers/walletconnect/core/session-health.ts.html +673 -0
  66. package/coverage/lcov-report/lib/providers/walletconnect/core/walletconnect-client-manager.ts.html +1177 -0
  67. package/coverage/lcov-report/lib/providers/walletconnect/core/walletconnect-provider.ts.html +2563 -0
  68. package/coverage/lcov-report/lib/providers/walletconnect/core/walletconnect-session-store.ts.html +904 -0
  69. package/coverage/lcov-report/lib/providers/walletconnect/core/walletconnect-signing-orchestrator.ts.html +982 -0
  70. package/coverage/lcov-report/lib/providers/walletconnect/signers/hedera-signer.ts.html +1915 -0
  71. package/coverage/lcov-report/lib/providers/walletconnect/signers/index.html +146 -0
  72. package/coverage/lcov-report/lib/providers/walletconnect/signers/signer-factory.ts.html +445 -0
  73. package/coverage/lcov-report/lib/providers/walletconnect/signers/xrpl-signer.ts.html +1519 -0
  74. package/coverage/lcov-report/lib/services/index.html +191 -0
  75. package/coverage/lcov-report/lib/services/logger.service.ts.html +463 -0
  76. package/coverage/lcov-report/lib/services/transaction-builders/base-transaction-builder.service.ts.html +1840 -0
  77. package/coverage/lcov-report/lib/services/transaction-builders/hedera-amount-utils.ts.html +337 -0
  78. package/coverage/lcov-report/lib/services/transaction-builders/hedera-transaction-builder.service.ts.html +3940 -0
  79. package/coverage/lcov-report/lib/services/transaction-builders/index.html +161 -0
  80. package/coverage/lcov-report/lib/services/transaction-builders/xrpl-transaction-builder.service.ts.html +2581 -0
  81. package/coverage/lcov-report/lib/services/transaction.service.ts.html +1123 -0
  82. package/coverage/lcov-report/lib/services/unified-wallet.service.ts.html +2641 -0
  83. package/coverage/lcov-report/lib/services/wallet-context.service.ts.html +637 -0
  84. package/coverage/lcov-report/lib/services/wallet-event-bus.service.ts.html +643 -0
  85. package/coverage/lcov-report/lib/services/wallet-providers.service.ts.html +496 -0
  86. package/coverage/lcov-report/lib/transports/chrome-extension-transport.ts.html +823 -0
  87. package/coverage/lcov-report/lib/transports/index.html +116 -0
  88. package/coverage/lcov-report/lib/utils/index.html +116 -0
  89. package/coverage/lcov-report/lib/utils/ledger-icons.util.ts.html +319 -0
  90. package/coverage/lcov-report/prettify.css +1 -0
  91. package/coverage/lcov-report/prettify.js +2 -0
  92. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  93. package/coverage/lcov-report/sorter.js +210 -0
  94. package/coverage/lcov.info +19252 -0
  95. package/coverage/lib/components/account-selector/account-actions/account-actions.component.ts.html +868 -0
  96. package/coverage/lib/components/account-selector/account-actions/index.html +116 -0
  97. package/coverage/lib/components/account-selector/account-filter/account-filter.component.ts.html +1288 -0
  98. package/coverage/lib/components/account-selector/account-filter/index.html +116 -0
  99. package/coverage/lib/components/account-selector/account-formatting.service.ts.html +685 -0
  100. package/coverage/lib/components/account-selector/account-grouping.service.ts.html +766 -0
  101. package/coverage/lib/components/account-selector/account-list/account-list.component.ts.html +1495 -0
  102. package/coverage/lib/components/account-selector/account-list/index.html +116 -0
  103. package/coverage/lib/components/account-selector/account-selector.component.ts.html +1495 -0
  104. package/coverage/lib/components/account-selector/account-selector.service.ts.html +1588 -0
  105. package/coverage/lib/components/account-selector/index.html +161 -0
  106. package/coverage/lib/components/wallet-account-display/index.html +116 -0
  107. package/coverage/lib/components/wallet-account-display/wallet-account-display.component.ts.html +505 -0
  108. package/coverage/lib/components/wallet-connect-button/index.html +116 -0
  109. package/coverage/lib/components/wallet-connect-button/wallet-connect-button.component.ts.html +805 -0
  110. package/coverage/lib/components/wallet-connect-prompt/index.html +116 -0
  111. package/coverage/lib/components/wallet-connect-prompt/wallet-connect-prompt.component.ts.html +409 -0
  112. package/coverage/lib/components/wallet-connected-guard/index.html +116 -0
  113. package/coverage/lib/components/wallet-connected-guard/wallet-connected-guard.component.ts.html +304 -0
  114. package/coverage/lib/components/wallet-connection-modal/connection-method-step/connection-method-step.component.ts.html +436 -0
  115. package/coverage/lib/components/wallet-connection-modal/connection-method-step/index.html +116 -0
  116. package/coverage/lib/components/wallet-connection-modal/index.html +116 -0
  117. package/coverage/lib/components/wallet-connection-modal/qr-pairing-step/index.html +116 -0
  118. package/coverage/lib/components/wallet-connection-modal/qr-pairing-step/qr-pairing-step.component.ts.html +2287 -0
  119. package/coverage/lib/components/wallet-connection-modal/wallet-connection-modal.component.ts.html +2275 -0
  120. package/coverage/lib/components/wallet-session-display/index.html +116 -0
  121. package/coverage/lib/components/wallet-session-display/wallet-session-display.component.ts.html +676 -0
  122. package/coverage/lib/components/wallet-transaction-status/index.html +116 -0
  123. package/coverage/lib/components/wallet-transaction-status/wallet-transaction-status.component.ts.html +703 -0
  124. package/coverage/lib/directives/index.html +146 -0
  125. package/coverage/lib/directives/wallet-connected.directive.ts.html +670 -0
  126. package/coverage/lib/directives/wallet-context.directive.ts.html +547 -0
  127. package/coverage/lib/directives/wallet-events.directive.ts.html +781 -0
  128. package/coverage/lib/hsuite-wallet.module.ts.html +715 -0
  129. package/coverage/lib/index.html +116 -0
  130. package/coverage/lib/models/connection-config.model.ts.html +280 -0
  131. package/coverage/lib/models/index.html +131 -0
  132. package/coverage/lib/models/provider-types.ts.html +577 -0
  133. package/coverage/lib/providers/base-wallet-provider.ts.html +1138 -0
  134. package/coverage/lib/providers/hsuite-native/channel-client.service.ts.html +2671 -0
  135. package/coverage/lib/providers/hsuite-native/index.html +116 -0
  136. package/coverage/lib/providers/hsuite-native-provider.ts.html +2347 -0
  137. package/coverage/lib/providers/index.html +146 -0
  138. package/coverage/lib/providers/p2p-native/index.html +131 -0
  139. package/coverage/lib/providers/p2p-native/p2p-native.provider.ts.html +2254 -0
  140. package/coverage/lib/providers/p2p-native/p2p-session-manager.ts.html +2170 -0
  141. package/coverage/lib/providers/wallet-error-handler.ts.html +1132 -0
  142. package/coverage/lib/providers/walletconnect/core/index.html +176 -0
  143. package/coverage/lib/providers/walletconnect/core/session-health.ts.html +673 -0
  144. package/coverage/lib/providers/walletconnect/core/walletconnect-client-manager.ts.html +1177 -0
  145. package/coverage/lib/providers/walletconnect/core/walletconnect-provider.ts.html +2563 -0
  146. package/coverage/lib/providers/walletconnect/core/walletconnect-session-store.ts.html +904 -0
  147. package/coverage/lib/providers/walletconnect/core/walletconnect-signing-orchestrator.ts.html +982 -0
  148. package/coverage/lib/providers/walletconnect/signers/hedera-signer.ts.html +1915 -0
  149. package/coverage/lib/providers/walletconnect/signers/index.html +146 -0
  150. package/coverage/lib/providers/walletconnect/signers/signer-factory.ts.html +445 -0
  151. package/coverage/lib/providers/walletconnect/signers/xrpl-signer.ts.html +1519 -0
  152. package/coverage/lib/services/index.html +191 -0
  153. package/coverage/lib/services/logger.service.ts.html +463 -0
  154. package/coverage/lib/services/transaction-builders/base-transaction-builder.service.ts.html +1840 -0
  155. package/coverage/lib/services/transaction-builders/hedera-amount-utils.ts.html +337 -0
  156. package/coverage/lib/services/transaction-builders/hedera-transaction-builder.service.ts.html +3940 -0
  157. package/coverage/lib/services/transaction-builders/index.html +161 -0
  158. package/coverage/lib/services/transaction-builders/xrpl-transaction-builder.service.ts.html +2581 -0
  159. package/coverage/lib/services/transaction.service.ts.html +1123 -0
  160. package/coverage/lib/services/unified-wallet.service.ts.html +2641 -0
  161. package/coverage/lib/services/wallet-context.service.ts.html +637 -0
  162. package/coverage/lib/services/wallet-event-bus.service.ts.html +643 -0
  163. package/coverage/lib/services/wallet-providers.service.ts.html +496 -0
  164. package/coverage/lib/transports/chrome-extension-transport.ts.html +823 -0
  165. package/coverage/lib/transports/index.html +116 -0
  166. package/coverage/lib/utils/index.html +116 -0
  167. package/coverage/lib/utils/ledger-icons.util.ts.html +319 -0
  168. package/coverage/prettify.css +1 -0
  169. package/coverage/prettify.js +2 -0
  170. package/coverage/sort-arrow-sprite.png +0 -0
  171. package/coverage/sorter.js +210 -0
  172. package/dist/README.md +48 -0
  173. package/dist/fesm2022/hsuite-native-connect-angular.mjs +14592 -0
  174. package/dist/fesm2022/hsuite-native-connect-angular.mjs.map +1 -0
  175. package/dist/index.d.ts +6949 -0
  176. package/examples/minimal-connect.ts +178 -0
  177. package/examples/multi-protocol.ts +495 -0
  178. package/examples/transaction-signing.ts +361 -0
  179. package/jest.config.json +45 -0
  180. package/karma.conf.js +42 -0
  181. package/ng-package.json +20 -0
  182. package/package.json +60 -0
  183. package/src/index.ts +203 -0
  184. package/src/lib/components/account-selector/account-actions/account-actions.component.ts +261 -0
  185. package/src/lib/components/account-selector/account-filter/account-filter.component.ts +401 -0
  186. package/src/lib/components/account-selector/account-formatting.service.ts +200 -0
  187. package/src/lib/components/account-selector/account-grouping.service.ts +227 -0
  188. package/src/lib/components/account-selector/account-list/account-list.component.ts +470 -0
  189. package/src/lib/components/account-selector/account-selector.component.html +135 -0
  190. package/src/lib/components/account-selector/account-selector.component.scss +2039 -0
  191. package/src/lib/components/account-selector/account-selector.component.ts +470 -0
  192. package/src/lib/components/account-selector/account-selector.service.ts +501 -0
  193. package/src/lib/components/wallet-account-display/wallet-account-display.component.html +34 -0
  194. package/src/lib/components/wallet-account-display/wallet-account-display.component.scss +99 -0
  195. package/src/lib/components/wallet-account-display/wallet-account-display.component.ts +140 -0
  196. package/src/lib/components/wallet-connect-button/wallet-connect-button.component.html +14 -0
  197. package/src/lib/components/wallet-connect-button/wallet-connect-button.component.scss +272 -0
  198. package/src/lib/components/wallet-connect-button/wallet-connect-button.component.ts +240 -0
  199. package/src/lib/components/wallet-connect-prompt/wallet-connect-prompt.component.html +24 -0
  200. package/src/lib/components/wallet-connect-prompt/wallet-connect-prompt.component.scss +50 -0
  201. package/src/lib/components/wallet-connect-prompt/wallet-connect-prompt.component.ts +108 -0
  202. package/src/lib/components/wallet-connected-guard/wallet-connected-guard.component.html +24 -0
  203. package/src/lib/components/wallet-connected-guard/wallet-connected-guard.component.ts +73 -0
  204. package/src/lib/components/wallet-connection-modal/connection-method-step/connection-method-step.component.html +56 -0
  205. package/src/lib/components/wallet-connection-modal/connection-method-step/connection-method-step.component.scss +218 -0
  206. package/src/lib/components/wallet-connection-modal/connection-method-step/connection-method-step.component.ts +117 -0
  207. package/src/lib/components/wallet-connection-modal/qr-pairing-step/qr-pairing-step.component.html +94 -0
  208. package/src/lib/components/wallet-connection-modal/qr-pairing-step/qr-pairing-step.component.scss +272 -0
  209. package/src/lib/components/wallet-connection-modal/qr-pairing-step/qr-pairing-step.component.ts +734 -0
  210. package/src/lib/components/wallet-connection-modal/wallet-connection-modal.component.html +197 -0
  211. package/src/lib/components/wallet-connection-modal/wallet-connection-modal.component.scss +678 -0
  212. package/src/lib/components/wallet-connection-modal/wallet-connection-modal.component.ts +730 -0
  213. package/src/lib/components/wallet-session-display/wallet-session-display.component.html +110 -0
  214. package/src/lib/components/wallet-session-display/wallet-session-display.component.scss +179 -0
  215. package/src/lib/components/wallet-session-display/wallet-session-display.component.ts +197 -0
  216. package/src/lib/components/wallet-transaction-status/wallet-transaction-status.component.html +65 -0
  217. package/src/lib/components/wallet-transaction-status/wallet-transaction-status.component.scss +254 -0
  218. package/src/lib/components/wallet-transaction-status/wallet-transaction-status.component.ts +206 -0
  219. package/src/lib/directives/wallet-connected.directive.ts +195 -0
  220. package/src/lib/directives/wallet-context.directive.ts +154 -0
  221. package/src/lib/directives/wallet-events.directive.ts +232 -0
  222. package/src/lib/hsuite-wallet.module.ts +210 -0
  223. package/src/lib/models/connection-config.model.ts +65 -0
  224. package/src/lib/models/provider-types.ts +164 -0
  225. package/src/lib/models/unified-account.model.ts +76 -0
  226. package/src/lib/models/wallet-context.model.ts +121 -0
  227. package/src/lib/models/wallet-events.model.ts +158 -0
  228. package/src/lib/providers/base-wallet-provider.ts +351 -0
  229. package/src/lib/providers/hsuite-native/channel-client.service.spec.ts +73 -0
  230. package/src/lib/providers/hsuite-native/channel-client.service.ts +862 -0
  231. package/src/lib/providers/hsuite-native/index.ts +8 -0
  232. package/src/lib/providers/hsuite-native-provider.ts +754 -0
  233. package/src/lib/providers/mobile-native/mobile-native.provider.spec.ts +19 -0
  234. package/src/lib/providers/p2p-native/index.ts +30 -0
  235. package/src/lib/providers/p2p-native/p2p-native.provider.spec.ts +523 -0
  236. package/src/lib/providers/p2p-native/p2p-native.provider.ts +723 -0
  237. package/src/lib/providers/p2p-native/p2p-session-manager.ts +695 -0
  238. package/src/lib/providers/wallet-error-handler.ts +349 -0
  239. package/src/lib/providers/walletconnect/core/base-signer.interface.ts +122 -0
  240. package/src/lib/providers/walletconnect/core/session-health.ts +196 -0
  241. package/src/lib/providers/walletconnect/core/walletconnect-client-manager.ts +364 -0
  242. package/src/lib/providers/walletconnect/core/walletconnect-provider.integration.spec.ts +348 -0
  243. package/src/lib/providers/walletconnect/core/walletconnect-provider.ts +826 -0
  244. package/src/lib/providers/walletconnect/core/walletconnect-session-store.ts +273 -0
  245. package/src/lib/providers/walletconnect/core/walletconnect-signing-orchestrator.ts +299 -0
  246. package/src/lib/providers/walletconnect/core/walletconnect-types.ts +48 -0
  247. package/src/lib/providers/walletconnect/index.ts +33 -0
  248. package/src/lib/providers/walletconnect/signers/hedera-signer.spec.ts +367 -0
  249. package/src/lib/providers/walletconnect/signers/hedera-signer.ts +610 -0
  250. package/src/lib/providers/walletconnect/signers/signer-factory.spec.ts +62 -0
  251. package/src/lib/providers/walletconnect/signers/signer-factory.ts +120 -0
  252. package/src/lib/providers/walletconnect/signers/xrpl-signer.spec.ts +296 -0
  253. package/src/lib/providers/walletconnect/signers/xrpl-signer.ts +478 -0
  254. package/src/lib/services/logger.service.ts +126 -0
  255. package/src/lib/services/transaction-builders/base-transaction-builder.service.ts +585 -0
  256. package/src/lib/services/transaction-builders/hedera-amount-utils.ts +84 -0
  257. package/src/lib/services/transaction-builders/hedera-transaction-builder.service.spec.ts +741 -0
  258. package/src/lib/services/transaction-builders/hedera-transaction-builder.service.ts +1285 -0
  259. package/src/lib/services/transaction-builders/index.ts +54 -0
  260. package/src/lib/services/transaction-builders/xrpl-transaction-builder.service.spec.ts +937 -0
  261. package/src/lib/services/transaction-builders/xrpl-transaction-builder.service.ts +832 -0
  262. package/src/lib/services/transaction.service.ts +346 -0
  263. package/src/lib/services/unified-wallet.service.spec.ts +1382 -0
  264. package/src/lib/services/unified-wallet.service.ts +852 -0
  265. package/src/lib/services/wallet-context.service.ts +184 -0
  266. package/src/lib/services/wallet-event-bus.service.ts +186 -0
  267. package/src/lib/services/wallet-providers.service.ts +137 -0
  268. package/src/lib/transports/chrome-extension-transport.ts +246 -0
  269. package/src/lib/utils/index.ts +14 -0
  270. package/src/lib/utils/ledger-icons.util.ts +78 -0
  271. package/test/test-setup.ts +21 -0
  272. package/test-setup.ts +63 -0
  273. package/tsconfig.build.json +11 -0
  274. package/tsconfig.json +29 -0
  275. package/tsconfig.spec.json +15 -0
  276. package/vitest.config.ts +48 -0
@@ -0,0 +1,862 @@
1
+ /**
2
+ * HSuite Native Connect
3
+ * Copyright 2024-2025 HSuite (https://hsuite.finance)
4
+ *
5
+ * SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
6
+ *
7
+ * This file is part of HSuite Native Connect. For commercial licensing,
8
+ * visit https://hsuite.finance/licensing
9
+ */
10
+
11
+ /**
12
+ * @file channel-client.service.ts
13
+ * @description Angular wrapper for the unified ChannelClient.
14
+ *
15
+ * ChannelClientService
16
+ *
17
+ * Angular service that wraps the ChannelClient from native-wallet-sdk,
18
+ * providing reactive signals and Angular-friendly APIs for dApp-wallet
19
+ * communication using the new unified channel protocol.
20
+ *
21
+ * Key features:
22
+ * - Reactive state via Angular signals
23
+ * - NgZone integration for proper change detection
24
+ * - Automatic session persistence
25
+ * - Support for both session (1:1) and party (N:N) channels
26
+ *
27
+ * This service is designed to replace the existing NostrSessionClient
28
+ * as part of the protocol simplification effort. It can coexist with
29
+ * the legacy implementation during migration.
30
+ *
31
+ * @Component({ ... })
32
+ * export class AppComponent {
33
+ * private channelService = inject(ChannelClientService);
34
+ *
35
+ * readonly state = this.channelService.state;
36
+ * readonly accounts = this.channelService.accounts;
37
+ *
38
+ * async connect() {
39
+ * const invite = await this.channelService.connect({
40
+ * type: 'session',
41
+ * appId: 'my-dapp',
42
+ * appName: 'My dApp',
43
+ * ledgerId: 'hedera',
44
+ * networkId: 'mainnet',
45
+ * });
46
+ * // Open wallet with invite URL
47
+ * window.open(this.channelService.getWalletInviteUrl(invite, walletUrl));
48
+ * }
49
+ * }
50
+ * ```
51
+ */
52
+
53
+ import {
54
+ Injectable,
55
+ inject,
56
+ signal,
57
+ computed,
58
+ NgZone,
59
+ type Signal,
60
+ type WritableSignal,
61
+ } from '@angular/core';
62
+ import {
63
+ ChannelClient,
64
+ type ChannelInvite,
65
+ type ChannelState,
66
+ type ChannelType,
67
+ type ChannelAccount,
68
+ type TransportState,
69
+ type PersistedChannel,
70
+ encodeChannelInvite,
71
+ getLogger,
72
+ } from '@hsuite/native-connect-sdk';
73
+
74
+ import { DEFAULT_WALLET_URL } from '../../models/provider-types';
75
+ import type { UnifiedAccount } from '../../models/unified-account.model';
76
+
77
+ const logger = getLogger().scoped?.('ChannelClientService') ?? getLogger();
78
+
79
+ /**
80
+ * ChannelConnectConfig
81
+ *
82
+ * Configuration for establishing a new channel.
83
+ */
84
+ export interface ChannelConnectConfig {
85
+ /** Channel type: 'session' for 1:1, 'party' for multisig */
86
+ type: ChannelType;
87
+ /** dApp identifier */
88
+ appId: string;
89
+ /** dApp display name */
90
+ appName: string;
91
+ /** dApp icon URL (optional) */
92
+ appIcon?: string;
93
+ /**
94
+ * dApp origin URL (optional). When omitted, the SDK falls back to
95
+ * `window.location.origin` so the wallet can always show the user where the
96
+ * connection request is coming from. Override this only if your dApp serves
97
+ * the wallet integration from a different origin than the page hosting it.
98
+ */
99
+ appOrigin?: string;
100
+ /** Target ledger (e.g., 'hedera', 'xrpl') */
101
+ ledgerId: string;
102
+ /** Target network (e.g., 'mainnet', 'testnet') */
103
+ networkId: string;
104
+ /** Requested permissions */
105
+ permissions?: string[];
106
+ /** Nostr relay URLs (optional, uses defaults) */
107
+ relays?: string[];
108
+ /** Wallet URL for generating invite links */
109
+ walletUrl?: string;
110
+ }
111
+
112
+ /**
113
+ * ChannelClientService
114
+ *
115
+ * Angular service for unified channel-based dApp-wallet communication.
116
+ */
117
+ @Injectable({ providedIn: 'root' })
118
+ export class ChannelClientService {
119
+ private readonly zone = inject(NgZone);
120
+ private client: ChannelClient | null = null;
121
+
122
+ // ============================================================================
123
+ // Reactive State
124
+ // ============================================================================
125
+
126
+ /** Current channel state */
127
+ private readonly _state: WritableSignal<ChannelState> = signal('idle');
128
+ readonly state: Signal<ChannelState> = this._state.asReadonly();
129
+
130
+ /** Current transport state (nostr-only, p2p-connected, etc.) */
131
+ private readonly _transportState: WritableSignal<TransportState> = signal('nostr-only');
132
+ readonly transportState: Signal<TransportState> = this._transportState.asReadonly();
133
+
134
+ /** Approved accounts from the wallet */
135
+ private readonly _accounts: WritableSignal<ChannelAccount[]> = signal([]);
136
+ readonly accounts: Signal<ChannelAccount[]> = this._accounts.asReadonly();
137
+
138
+ /** Current channel invite (if connected) */
139
+ private readonly _currentInvite: WritableSignal<ChannelInvite | null> = signal(null);
140
+ readonly currentInvite: Signal<ChannelInvite | null> = this._currentInvite.asReadonly();
141
+
142
+ /** Error message (if any) */
143
+ private readonly _error: WritableSignal<string | null> = signal(null);
144
+ readonly error: Signal<string | null> = this._error.asReadonly();
145
+
146
+ /** Flag to prevent auto-restore from interfering with new connections */
147
+ private connectingNewSession = false;
148
+
149
+ // ============================================================================
150
+ // Computed State
151
+ // ============================================================================
152
+
153
+ /** Whether the channel is currently connected and active */
154
+ readonly isConnected = computed(() => this._state() === 'active');
155
+
156
+ /** Whether we're currently connecting */
157
+ readonly isConnecting = computed(() => {
158
+ const s = this._state();
159
+ return s === 'connecting' || s === 'pending';
160
+ });
161
+
162
+ /** Accounts formatted as UnifiedAccount for compatibility */
163
+ readonly unifiedAccounts = computed<UnifiedAccount[]>(() => {
164
+ const accounts = this._accounts();
165
+ const invite = this._currentInvite();
166
+ if (!invite) return [];
167
+
168
+ return accounts.map((account, index) => {
169
+ const accountMetadata = (account as { metadata?: Record<string, unknown> }).metadata;
170
+ return {
171
+ id: `channel-${account.address}`,
172
+ address: account.address,
173
+ label: account.alias ?? `Account ${index + 1}`,
174
+ ledgerId: account.ledgerId,
175
+ networkId: account.networkId,
176
+ providerId: 'hsuite-native',
177
+ providerType: 'hsuite-native' as const,
178
+ metadata: {
179
+ ...(accountMetadata ?? {}),
180
+ channelId: invite.id,
181
+ channelType: invite.type,
182
+ isMultisig: account.isMultisig === true,
183
+ multisigThreshold: account.multisigThreshold,
184
+ multisigTotal: account.multisigTotal,
185
+ publicKey: account.publicKey ?? '',
186
+ },
187
+ };
188
+ });
189
+ });
190
+
191
+ /**
192
+ *
193
+ */
194
+ constructor() {
195
+ // Note: Auto-restore is intentionally NOT called here.
196
+ // Session lifecycle (connect/disconnect/restore) is controlled by
197
+ // HsuiteNativeProvider, which calls attemptRestore() as needed.
198
+ // This keeps ChannelClientService as a passive wrapper.
199
+ }
200
+
201
+ // ============================================================================
202
+ // Public API
203
+ // ============================================================================
204
+
205
+ /**
206
+ * Establish a new channel connection.
207
+ * Returns the invite to share with the wallet (via QR code or deep link).
208
+ *
209
+ * @param config - Channel configuration
210
+ * @returns The channel invite for wallet scanning
211
+ */
212
+ async connect(config: ChannelConnectConfig): Promise<ChannelInvite> {
213
+ if (this._state() === 'connecting' || this._state() === 'pending') {
214
+ logger.warn('Connection already in progress');
215
+ const existing = this._currentInvite();
216
+ if (existing) return existing;
217
+ throw new Error('Connection already in progress');
218
+ }
219
+
220
+ // Mark that we're initiating a new session to prevent auto-restore interference
221
+ this.connectingNewSession = true;
222
+
223
+ // CRITICAL: Disconnect any existing client before creating a new one
224
+ // This prevents the old client from interfering with the new connection
225
+ if (this.client) {
226
+ try {
227
+ await this.client.disconnect();
228
+ } catch {
229
+ // Ignore disconnect errors
230
+ }
231
+ this.client = null;
232
+ }
233
+
234
+ // Clear any old stored session before starting a new connection
235
+ // This prevents restoration of old sessions during the connection process
236
+ this.clearStoredChannel();
237
+
238
+ this.runInZone(() => {
239
+ this._state.set('connecting');
240
+ this._error.set(null);
241
+ this._currentInvite.set(null);
242
+ this._accounts.set([]);
243
+ });
244
+
245
+ try {
246
+ // Create new client with callbacks for immediate state updates
247
+ // This ensures rejection/disconnection is detected immediately, not just via polling
248
+ this.client = new ChannelClient({
249
+ onStateChange: (state) => {
250
+ // Log immediately before runInZone for debugging
251
+ console.log('[ChannelClientService] onStateChange callback received:', {
252
+ newState: state,
253
+ previousState: this._state(),
254
+ timestamp: Date.now(),
255
+ });
256
+
257
+ this.runInZone(() => {
258
+ const previousState = this._state();
259
+ if (state !== previousState) {
260
+ logger.debug('State change via callback', { from: previousState, to: state });
261
+ this._state.set(state);
262
+
263
+ // Handle error state - this is triggered on rejection
264
+ if (state === 'error') {
265
+ console.log(
266
+ '[ChannelClientService] Error state detected - connection rejected or failed',
267
+ );
268
+ const errorMsg = 'Connection rejected or failed';
269
+ this._error.set(errorMsg);
270
+ }
271
+
272
+ // Handle disconnected state explicitly
273
+ if (state === 'disconnected') {
274
+ console.log('[ChannelClientService] Disconnected state detected');
275
+ }
276
+
277
+ // Reset connecting flag when connection is complete or failed
278
+ if (state === 'active' || state === 'error' || state === 'disconnected') {
279
+ this.connectingNewSession = false;
280
+ }
281
+
282
+ // Persist when active
283
+ if (state === 'active' && previousState !== 'active') {
284
+ this.persistCurrentState();
285
+ }
286
+ }
287
+ });
288
+ },
289
+ onAccountsChange: (accounts) => {
290
+ this.runInZone(() => {
291
+ logger.debug('Accounts change via callback', { count: accounts.length });
292
+ this._accounts.set(accounts);
293
+ // Re-persist when accounts change
294
+ if (this._state() === 'active') {
295
+ this.persistCurrentState();
296
+ }
297
+ });
298
+ },
299
+ onTransportChange: (transport) => {
300
+ this.runInZone(() => {
301
+ if (transport !== this._transportState()) {
302
+ logger.debug('Transport change via callback', { state: transport });
303
+ this._transportState.set(transport);
304
+ }
305
+ });
306
+ },
307
+ });
308
+
309
+ // Connect using proper ChannelConfig format. Default `origin` to
310
+ // `window.location.origin` so wallets always have a URL to display in
311
+ // the approval prompt — without this, dApps that forget to pass an
312
+ // origin show up as anonymous, hurting trust signals.
313
+ const resolvedOrigin =
314
+ config.appOrigin ??
315
+ (typeof window !== 'undefined' && window.location ? window.location.origin : undefined);
316
+
317
+ const invite = await this.client.connect({
318
+ type: config.type,
319
+ app: {
320
+ id: config.appId,
321
+ name: config.appName,
322
+ icon: config.appIcon,
323
+ origin: resolvedOrigin,
324
+ },
325
+ context: {
326
+ ledgerId: config.ledgerId,
327
+ networkId: config.networkId,
328
+ },
329
+ permissions: config.permissions,
330
+ relays: config.relays,
331
+ });
332
+
333
+ // Set up state synchronization
334
+ this.setupStateSync();
335
+
336
+ this.runInZone(() => {
337
+ this._currentInvite.set(invite);
338
+ this._state.set('pending');
339
+ });
340
+
341
+ // Store invite for reconnection
342
+ this.storeInvite(invite);
343
+
344
+ logger.info('Channel connection initiated', {
345
+ channelId: invite.id.slice(0, 8),
346
+ type: invite.type,
347
+ });
348
+
349
+ return invite;
350
+ } catch (error) {
351
+ const message = error instanceof Error ? error.message : String(error);
352
+ logger.error('Connection failed', { error: message });
353
+
354
+ // Reset connecting flag on error
355
+ this.connectingNewSession = false;
356
+
357
+ this.runInZone(() => {
358
+ this._state.set('error');
359
+ this._error.set(message);
360
+ });
361
+
362
+ throw error;
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Generate a wallet invite URL for the current channel.
368
+ *
369
+ * @param invite - The channel invite (defaults to current)
370
+ * @param walletUrl - Base URL of the wallet
371
+ * @returns Full URL with invite parameter
372
+ */
373
+ getWalletInviteUrl(invite?: ChannelInvite, walletUrl = DEFAULT_WALLET_URL): string {
374
+ const inv = invite ?? this._currentInvite();
375
+ if (!inv) {
376
+ throw new Error('No active invite');
377
+ }
378
+
379
+ const encoded = encodeChannelInvite(inv);
380
+ return `${walletUrl}?hsuite_invite=${encodeURIComponent(encoded)}`;
381
+ }
382
+
383
+ /**
384
+ * Send an RPC request to the wallet.
385
+ *
386
+ * @param method - RPC method name
387
+ * @param params - Method parameters
388
+ * @param timeoutMs - Request timeout (default: 60s)
389
+ * @returns The RPC response
390
+ */
391
+ async request<T = unknown>(
392
+ method: string,
393
+ params: Record<string, unknown>,
394
+ timeoutMs = 60000,
395
+ ): Promise<T> {
396
+ if (!this.client) {
397
+ throw new Error('Not connected - no active channel client. Please reconnect to the wallet.');
398
+ }
399
+
400
+ const state = this._state();
401
+ if (state !== 'active' && state !== 'approved') {
402
+ throw new Error(
403
+ `Not connected - channel state is '${state}'. Please reconnect to the wallet.`,
404
+ );
405
+ }
406
+
407
+ logger.debug('Sending RPC request', { method, state });
408
+
409
+ return this.client.request<T>(method, params, timeoutMs);
410
+ }
411
+
412
+ /**
413
+ * Sign a transaction via the wallet.
414
+ *
415
+ * @param options
416
+ * @param accountAddress - Signer account address
417
+ * @param options.accountAddress
418
+ * @param options.payload
419
+ * @param options.ledgerId
420
+ * @param options.networkId
421
+ * @param payload - Transaction payload (base64 or hex)
422
+ * @param ledgerId - Optional ledger override
423
+ * @param networkId - Optional network override
424
+ * @returns Sign result with signature
425
+ */
426
+ async signTransaction(options: {
427
+ accountAddress: string;
428
+ payload: string;
429
+ ledgerId?: string;
430
+ networkId?: string;
431
+ }): Promise<{ signature: string; signedPayload?: string; metadata?: Record<string, unknown> }> {
432
+ const response = await this.request<{
433
+ signature?: string;
434
+ signedPayload?: string;
435
+ signedTransaction?: string;
436
+ metadata?: Record<string, unknown>;
437
+ }>('ledger/sign', {
438
+ accountAddress: options.accountAddress,
439
+ payload: options.payload,
440
+ ledgerId: options.ledgerId,
441
+ networkId: options.networkId,
442
+ });
443
+
444
+ return {
445
+ signature: response.signature ?? '',
446
+ signedPayload: response.signedPayload ?? response.signedTransaction,
447
+ metadata: response.metadata,
448
+ };
449
+ }
450
+
451
+ /**
452
+ * Sign and submit a transaction in one call (single prompt).
453
+ *
454
+ * @param options
455
+ * @param accountAddress - Signer account address
456
+ * @param options.accountAddress
457
+ * @param options.payload
458
+ * @param options.ledgerId
459
+ * @param options.networkId
460
+ * @param options.isBatch
461
+ * @param options.batchKey
462
+ * @param options.innerTransactions
463
+ * @param payload - Transaction payload
464
+ * @param ledgerId - Optional ledger override
465
+ * @param networkId - Optional network override
466
+ * @returns Submit result with transaction ID
467
+ */
468
+ async signAndSubmitTransaction(options: {
469
+ accountAddress: string;
470
+ payload: string;
471
+ ledgerId?: string;
472
+ networkId?: string;
473
+ // Batch transaction fields
474
+ isBatch?: boolean;
475
+ batchKey?: string;
476
+ innerTransactions?: Array<{ payload: string; description?: string }>;
477
+ }): Promise<{
478
+ transactionId: string;
479
+ transactionHash?: string;
480
+ metadata?: Record<string, unknown>;
481
+ }> {
482
+ const response = await this.request<{
483
+ transactionId?: string;
484
+ transactionHash?: string;
485
+ metadata?: Record<string, unknown>;
486
+ }>('ledger/signAndSubmit', {
487
+ accountAddress: options.accountAddress,
488
+ payload: options.payload,
489
+ ledgerId: options.ledgerId,
490
+ networkId: options.networkId,
491
+ // Pass batch transaction fields if present
492
+ ...(options.isBatch && {
493
+ isBatch: options.isBatch,
494
+ batchKey: options.batchKey,
495
+ innerTransactions: options.innerTransactions,
496
+ }),
497
+ });
498
+
499
+ return {
500
+ transactionId: response.transactionId ?? '',
501
+ transactionHash: response.transactionHash,
502
+ metadata: response.metadata,
503
+ };
504
+ }
505
+
506
+ /**
507
+ * Disconnect from the wallet.
508
+ */
509
+ async disconnect(): Promise<void> {
510
+ // Stop state polling first
511
+ this.stopStateSync();
512
+
513
+ if (this.client) {
514
+ try {
515
+ await this.client.disconnect();
516
+ } catch (error) {
517
+ logger.warn('Disconnect error', { error });
518
+ }
519
+ this.client = null;
520
+ }
521
+
522
+ this.clearStoredChannel();
523
+
524
+ this.runInZone(() => {
525
+ this._state.set('disconnected');
526
+ this._transportState.set('nostr-only');
527
+ this._accounts.set([]);
528
+ this._currentInvite.set(null);
529
+ this._error.set(null);
530
+ });
531
+
532
+ logger.info('Disconnected from channel');
533
+ }
534
+
535
+ /**
536
+ * Attempt to restore a previous session.
537
+ *
538
+ * Note: This method is called by HsuiteNativeProvider as the single point
539
+ * of session lifecycle control. Do not call from multiple places.
540
+ *
541
+ * @returns True if restoration was successful
542
+ */
543
+ async attemptRestore(): Promise<boolean> {
544
+ // Skip restore if a new connection is being initiated
545
+ if (this.connectingNewSession) {
546
+ logger.debug('Skipping restore - new connection in progress');
547
+ return false;
548
+ }
549
+
550
+ const stored = this.getStoredChannel();
551
+ if (!stored) {
552
+ return false;
553
+ }
554
+
555
+ // §21.1 Option A: legacy persisted channels (pre-localContext-persistence)
556
+ // cannot be safely restored — reconnecting re-derives the Ed25519 signing
557
+ // keypair with empty context, producing a different pubKey than the one
558
+ // the peer TOFU-bound at initial connect, and the next signed message
559
+ // floods HWA-04 impersonation errors. Rather than silently fall through
560
+ // to that broken path, clear the stored channel and force a fresh scan.
561
+ // Safe to do unconditionally at this point in the project's lifecycle:
562
+ // no dApp sessions are live in production yet.
563
+ if (stored.localContext === undefined) {
564
+ logger.warn(
565
+ 'Clearing legacy persisted channel without localContext — user must re-approve the dApp connection (§21.1)',
566
+ { channelId: stored.id.slice(0, 8) },
567
+ );
568
+ this.clearStoredChannel();
569
+ return false;
570
+ }
571
+
572
+ logger.info('Attempting session restore', {
573
+ channelId: stored.id.slice(0, 8),
574
+ });
575
+
576
+ this.runInZone(() => {
577
+ this._state.set('connecting');
578
+ });
579
+
580
+ try {
581
+ // Create client with callbacks for immediate state updates
582
+ this.client = new ChannelClient({
583
+ onStateChange: (state) => {
584
+ this.runInZone(() => {
585
+ const previousState = this._state();
586
+ if (state !== previousState) {
587
+ logger.debug('State change via callback (reconnect)', {
588
+ from: previousState,
589
+ to: state,
590
+ });
591
+ this._state.set(state);
592
+ if (state === 'error') {
593
+ const errorMsg =
594
+ this.client?.state === 'error' ? 'Connection rejected or failed' : null;
595
+ this._error.set(errorMsg);
596
+ }
597
+ if (state === 'active' || state === 'error' || state === 'disconnected') {
598
+ this.connectingNewSession = false;
599
+ }
600
+ if (state === 'active' && previousState !== 'active') {
601
+ this.persistCurrentState();
602
+ }
603
+ }
604
+ });
605
+ },
606
+ onAccountsChange: (accounts) => {
607
+ this.runInZone(() => {
608
+ logger.debug('Accounts change via callback (reconnect)', { count: accounts.length });
609
+ this._accounts.set(accounts);
610
+ if (this._state() === 'active') {
611
+ this.persistCurrentState();
612
+ }
613
+ });
614
+ },
615
+ onTransportChange: (transport) => {
616
+ this.runInZone(() => {
617
+ if (transport !== this._transportState()) {
618
+ logger.debug('Transport change via callback (reconnect)', { state: transport });
619
+ this._transportState.set(transport);
620
+ }
621
+ });
622
+ },
623
+ });
624
+ // Pass the known remote peer fingerprint for P2P restoration
625
+ const remotePeer = stored.knownPeers?.[0];
626
+ await this.client.reconnect(stored, remotePeer);
627
+
628
+ this.setupStateSync();
629
+
630
+ // Restore invite and accounts from persisted/client data
631
+ const restoredInvite = this.client.currentInvite;
632
+ const restoredAccounts = this.client.accounts;
633
+ this.runInZone(() => {
634
+ if (restoredInvite) {
635
+ this._currentInvite.set(restoredInvite);
636
+ }
637
+ // Immediately sync accounts from the client
638
+ if (restoredAccounts.length > 0) {
639
+ this._accounts.set(restoredAccounts);
640
+ }
641
+ this._state.set('active');
642
+ });
643
+
644
+ logger.info('Session restored successfully', {
645
+ channelId: stored.id.slice(0, 8),
646
+ accounts: restoredAccounts.length,
647
+ });
648
+ return true;
649
+ } catch (error) {
650
+ logger.warn('Session restore failed', {
651
+ error: error instanceof Error ? error.message : String(error),
652
+ });
653
+
654
+ // Clean up the partially initialized client
655
+ this.client = null;
656
+
657
+ this.runInZone(() => {
658
+ this._state.set('disconnected');
659
+ });
660
+
661
+ this.clearStoredChannel();
662
+ return false;
663
+ }
664
+ }
665
+
666
+ // ============================================================================
667
+ // Private Methods
668
+ // ============================================================================
669
+
670
+ /**
671
+ * Run a function inside NgZone for proper change detection.
672
+ * @param fn
673
+ */
674
+ private runInZone(fn: () => void): void {
675
+ if (this.zone) {
676
+ this.zone.run(fn);
677
+ } else {
678
+ fn();
679
+ }
680
+ }
681
+
682
+ /** Current polling timeout ID for cleanup */
683
+ private pollingTimeoutId?: ReturnType<typeof setTimeout>;
684
+
685
+ /**
686
+ * Set up state synchronization from the underlying client.
687
+ * Polling continues as long as the client exists - it doesn't stop on disconnect/error
688
+ * to properly track state changes during reconnection.
689
+ */
690
+ private setupStateSync(): void {
691
+ if (!this.client) return;
692
+
693
+ // Clear any existing polling
694
+ this.stopStateSync();
695
+
696
+ // Poll for state changes (ChannelClient uses getters, not signals)
697
+ const checkState = () => {
698
+ if (!this.client) {
699
+ this.pollingTimeoutId = undefined;
700
+ return;
701
+ }
702
+
703
+ // Use getters directly (not function calls)
704
+ const clientState = this.client.state;
705
+ const clientAccounts = this.client.accounts;
706
+ const clientTransport = this.client.transportState;
707
+
708
+ this.runInZone(() => {
709
+ const previousState = this._state();
710
+ if (clientState !== previousState) {
711
+ this._state.set(clientState);
712
+ // Reset the connecting flag when connection is complete (active) or failed (error, disconnected)
713
+ if (
714
+ clientState === 'active' ||
715
+ clientState === 'error' ||
716
+ clientState === 'disconnected'
717
+ ) {
718
+ this.connectingNewSession = false;
719
+ }
720
+ // Re-store session when it becomes active (after approval with accounts)
721
+ if (clientState === 'active' && previousState !== 'active') {
722
+ this.persistCurrentState();
723
+ }
724
+ }
725
+ // Deep compare accounts array
726
+ const currentAccounts = this._accounts();
727
+ if (
728
+ clientAccounts.length !== currentAccounts.length ||
729
+ clientAccounts.some((acc, i) => acc.address !== currentAccounts[i]?.address)
730
+ ) {
731
+ this._accounts.set(clientAccounts);
732
+ // Re-persist when accounts change
733
+ if (clientState === 'active') {
734
+ this.persistCurrentState();
735
+ }
736
+ }
737
+ if (clientTransport !== this._transportState()) {
738
+ this._transportState.set(clientTransport);
739
+ logger.debug('Transport state updated', { state: clientTransport });
740
+ }
741
+ });
742
+
743
+ // Continue polling as long as client exists
744
+ // Use faster polling for active states, slower for idle/disconnected
745
+ if (this.client) {
746
+ const pollInterval =
747
+ clientState === 'active' ||
748
+ clientState === 'pending' ||
749
+ clientState === 'connecting' ||
750
+ clientState === 'approved'
751
+ ? 100 // Fast polling during active states
752
+ : 500; // Slower polling during idle/disconnected (save CPU)
753
+ this.pollingTimeoutId = setTimeout(checkState, pollInterval);
754
+ } else {
755
+ this.pollingTimeoutId = undefined;
756
+ }
757
+ };
758
+
759
+ this.pollingTimeoutId = setTimeout(checkState, 100);
760
+ }
761
+
762
+ /**
763
+ * Stop state synchronization polling.
764
+ */
765
+ private stopStateSync(): void {
766
+ if (this.pollingTimeoutId) {
767
+ clearTimeout(this.pollingTimeoutId);
768
+ this.pollingTimeoutId = undefined;
769
+ }
770
+ }
771
+
772
+ // ============================================================================
773
+ // Storage
774
+ // ============================================================================
775
+
776
+ private static readonly STORAGE_KEY = 'hsuite_channel_persisted';
777
+
778
+ /**
779
+ * Persist current state for reconnection after approval.
780
+ */
781
+ private persistCurrentState(): void {
782
+ if (!this.client) return;
783
+ try {
784
+ const persisted = this.client.exportState();
785
+ if (persisted) {
786
+ localStorage.setItem(ChannelClientService.STORAGE_KEY, JSON.stringify(persisted));
787
+ logger.debug('Channel state persisted', {
788
+ channelId: persisted.id.slice(0, 8),
789
+ accounts: persisted.accounts?.length ?? 0,
790
+ });
791
+ }
792
+ } catch (error) {
793
+ logger.warn('Failed to persist channel state', { error });
794
+ }
795
+ }
796
+
797
+ /**
798
+ * Store channel state for reconnection.
799
+ * Uses client's exportState() to get proper PersistedChannel format.
800
+ * @param invite
801
+ */
802
+ private storeInvite(invite: ChannelInvite): void {
803
+ try {
804
+ // Use the client's exportState if available, otherwise create minimal persisted data
805
+ const persisted = this.client?.exportState();
806
+ if (persisted) {
807
+ localStorage.setItem(ChannelClientService.STORAGE_KEY, JSON.stringify(persisted));
808
+ } else {
809
+ // Fallback: create minimal persisted channel.
810
+ // C-1 / SOC2 CC6.6 — `createdAt` is required on PersistedChannel
811
+ // so the absolute-lifetime reaper can survive a reconnect. In
812
+ // this fallback path we don't know the original creation time
813
+ // (the client didn't expose its exportState), so seed it to
814
+ // "now" — the worst case is the reaper waits a full lifetime
815
+ // from this point, which is no weaker than the previous
816
+ // behaviour where it had no anchor at all.
817
+ const now = Date.now();
818
+ const minimal: PersistedChannel = {
819
+ version: 1,
820
+ id: invite.id,
821
+ type: invite.type,
822
+ invitation: encodeChannelInvite(invite),
823
+ fingerprint: 'unknown',
824
+ accounts: this._accounts(),
825
+ knownPeers: [],
826
+ createdAt: now,
827
+ lastActivity: now,
828
+ isHost: false,
829
+ };
830
+ localStorage.setItem(ChannelClientService.STORAGE_KEY, JSON.stringify(minimal));
831
+ }
832
+ } catch {
833
+ logger.warn('Failed to store channel');
834
+ }
835
+ }
836
+
837
+ /**
838
+ * Retrieve stored channel for reconnection.
839
+ */
840
+ private getStoredChannel(): PersistedChannel | null {
841
+ try {
842
+ const stored = localStorage.getItem(ChannelClientService.STORAGE_KEY);
843
+ if (stored) {
844
+ return JSON.parse(stored) as PersistedChannel;
845
+ }
846
+ } catch {
847
+ logger.warn('Failed to retrieve stored channel');
848
+ }
849
+ return null;
850
+ }
851
+
852
+ /**
853
+ * Clear stored channel data.
854
+ */
855
+ private clearStoredChannel(): void {
856
+ try {
857
+ localStorage.removeItem(ChannelClientService.STORAGE_KEY);
858
+ } catch {
859
+ // Ignore
860
+ }
861
+ }
862
+ }