@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.
- package/README.md +48 -0
- package/USAGE_EXAMPLES.md +476 -0
- package/assets/wallets/extension.svg +7 -0
- package/assets/wallets/hashpack.svg +6 -0
- package/assets/wallets/hsuite.svg +11 -0
- package/assets/wallets/kabila.svg +11 -0
- package/assets/wallets/walletconnect.svg +13 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-summary.json +50 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +476 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +476 -0
- package/coverage/lcov-report/lib/components/account-selector/account-actions/account-actions.component.ts.html +868 -0
- package/coverage/lcov-report/lib/components/account-selector/account-actions/index.html +116 -0
- package/coverage/lcov-report/lib/components/account-selector/account-filter/account-filter.component.ts.html +1288 -0
- package/coverage/lcov-report/lib/components/account-selector/account-filter/index.html +116 -0
- package/coverage/lcov-report/lib/components/account-selector/account-formatting.service.ts.html +685 -0
- package/coverage/lcov-report/lib/components/account-selector/account-grouping.service.ts.html +766 -0
- package/coverage/lcov-report/lib/components/account-selector/account-list/account-list.component.ts.html +1495 -0
- package/coverage/lcov-report/lib/components/account-selector/account-list/index.html +116 -0
- package/coverage/lcov-report/lib/components/account-selector/account-selector.component.ts.html +1495 -0
- package/coverage/lcov-report/lib/components/account-selector/account-selector.service.ts.html +1588 -0
- package/coverage/lcov-report/lib/components/account-selector/index.html +161 -0
- package/coverage/lcov-report/lib/components/wallet-account-display/index.html +116 -0
- package/coverage/lcov-report/lib/components/wallet-account-display/wallet-account-display.component.ts.html +505 -0
- package/coverage/lcov-report/lib/components/wallet-connect-button/index.html +116 -0
- package/coverage/lcov-report/lib/components/wallet-connect-button/wallet-connect-button.component.ts.html +805 -0
- package/coverage/lcov-report/lib/components/wallet-connect-prompt/index.html +116 -0
- package/coverage/lcov-report/lib/components/wallet-connect-prompt/wallet-connect-prompt.component.ts.html +409 -0
- package/coverage/lcov-report/lib/components/wallet-connected-guard/index.html +116 -0
- package/coverage/lcov-report/lib/components/wallet-connected-guard/wallet-connected-guard.component.ts.html +304 -0
- package/coverage/lcov-report/lib/components/wallet-connection-modal/connection-method-step/connection-method-step.component.ts.html +436 -0
- package/coverage/lcov-report/lib/components/wallet-connection-modal/connection-method-step/index.html +116 -0
- package/coverage/lcov-report/lib/components/wallet-connection-modal/index.html +116 -0
- package/coverage/lcov-report/lib/components/wallet-connection-modal/qr-pairing-step/index.html +116 -0
- package/coverage/lcov-report/lib/components/wallet-connection-modal/qr-pairing-step/qr-pairing-step.component.ts.html +2287 -0
- package/coverage/lcov-report/lib/components/wallet-connection-modal/wallet-connection-modal.component.ts.html +2275 -0
- package/coverage/lcov-report/lib/components/wallet-session-display/index.html +116 -0
- package/coverage/lcov-report/lib/components/wallet-session-display/wallet-session-display.component.ts.html +676 -0
- package/coverage/lcov-report/lib/components/wallet-transaction-status/index.html +116 -0
- package/coverage/lcov-report/lib/components/wallet-transaction-status/wallet-transaction-status.component.ts.html +703 -0
- package/coverage/lcov-report/lib/directives/index.html +146 -0
- package/coverage/lcov-report/lib/directives/wallet-connected.directive.ts.html +670 -0
- package/coverage/lcov-report/lib/directives/wallet-context.directive.ts.html +547 -0
- package/coverage/lcov-report/lib/directives/wallet-events.directive.ts.html +781 -0
- package/coverage/lcov-report/lib/hsuite-wallet.module.ts.html +715 -0
- package/coverage/lcov-report/lib/index.html +116 -0
- package/coverage/lcov-report/lib/models/connection-config.model.ts.html +280 -0
- package/coverage/lcov-report/lib/models/index.html +131 -0
- package/coverage/lcov-report/lib/models/provider-types.ts.html +577 -0
- package/coverage/lcov-report/lib/providers/base-wallet-provider.ts.html +1138 -0
- package/coverage/lcov-report/lib/providers/hsuite-native/channel-client.service.ts.html +2671 -0
- package/coverage/lcov-report/lib/providers/hsuite-native/index.html +116 -0
- package/coverage/lcov-report/lib/providers/hsuite-native-provider.ts.html +2347 -0
- package/coverage/lcov-report/lib/providers/index.html +146 -0
- package/coverage/lcov-report/lib/providers/p2p-native/index.html +131 -0
- package/coverage/lcov-report/lib/providers/p2p-native/p2p-native.provider.ts.html +2254 -0
- package/coverage/lcov-report/lib/providers/p2p-native/p2p-session-manager.ts.html +2170 -0
- package/coverage/lcov-report/lib/providers/wallet-error-handler.ts.html +1132 -0
- package/coverage/lcov-report/lib/providers/walletconnect/core/index.html +176 -0
- package/coverage/lcov-report/lib/providers/walletconnect/core/session-health.ts.html +673 -0
- package/coverage/lcov-report/lib/providers/walletconnect/core/walletconnect-client-manager.ts.html +1177 -0
- package/coverage/lcov-report/lib/providers/walletconnect/core/walletconnect-provider.ts.html +2563 -0
- package/coverage/lcov-report/lib/providers/walletconnect/core/walletconnect-session-store.ts.html +904 -0
- package/coverage/lcov-report/lib/providers/walletconnect/core/walletconnect-signing-orchestrator.ts.html +982 -0
- package/coverage/lcov-report/lib/providers/walletconnect/signers/hedera-signer.ts.html +1915 -0
- package/coverage/lcov-report/lib/providers/walletconnect/signers/index.html +146 -0
- package/coverage/lcov-report/lib/providers/walletconnect/signers/signer-factory.ts.html +445 -0
- package/coverage/lcov-report/lib/providers/walletconnect/signers/xrpl-signer.ts.html +1519 -0
- package/coverage/lcov-report/lib/services/index.html +191 -0
- package/coverage/lcov-report/lib/services/logger.service.ts.html +463 -0
- package/coverage/lcov-report/lib/services/transaction-builders/base-transaction-builder.service.ts.html +1840 -0
- package/coverage/lcov-report/lib/services/transaction-builders/hedera-amount-utils.ts.html +337 -0
- package/coverage/lcov-report/lib/services/transaction-builders/hedera-transaction-builder.service.ts.html +3940 -0
- package/coverage/lcov-report/lib/services/transaction-builders/index.html +161 -0
- package/coverage/lcov-report/lib/services/transaction-builders/xrpl-transaction-builder.service.ts.html +2581 -0
- package/coverage/lcov-report/lib/services/transaction.service.ts.html +1123 -0
- package/coverage/lcov-report/lib/services/unified-wallet.service.ts.html +2641 -0
- package/coverage/lcov-report/lib/services/wallet-context.service.ts.html +637 -0
- package/coverage/lcov-report/lib/services/wallet-event-bus.service.ts.html +643 -0
- package/coverage/lcov-report/lib/services/wallet-providers.service.ts.html +496 -0
- package/coverage/lcov-report/lib/transports/chrome-extension-transport.ts.html +823 -0
- package/coverage/lcov-report/lib/transports/index.html +116 -0
- package/coverage/lcov-report/lib/utils/index.html +116 -0
- package/coverage/lcov-report/lib/utils/ledger-icons.util.ts.html +319 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +19252 -0
- package/coverage/lib/components/account-selector/account-actions/account-actions.component.ts.html +868 -0
- package/coverage/lib/components/account-selector/account-actions/index.html +116 -0
- package/coverage/lib/components/account-selector/account-filter/account-filter.component.ts.html +1288 -0
- package/coverage/lib/components/account-selector/account-filter/index.html +116 -0
- package/coverage/lib/components/account-selector/account-formatting.service.ts.html +685 -0
- package/coverage/lib/components/account-selector/account-grouping.service.ts.html +766 -0
- package/coverage/lib/components/account-selector/account-list/account-list.component.ts.html +1495 -0
- package/coverage/lib/components/account-selector/account-list/index.html +116 -0
- package/coverage/lib/components/account-selector/account-selector.component.ts.html +1495 -0
- package/coverage/lib/components/account-selector/account-selector.service.ts.html +1588 -0
- package/coverage/lib/components/account-selector/index.html +161 -0
- package/coverage/lib/components/wallet-account-display/index.html +116 -0
- package/coverage/lib/components/wallet-account-display/wallet-account-display.component.ts.html +505 -0
- package/coverage/lib/components/wallet-connect-button/index.html +116 -0
- package/coverage/lib/components/wallet-connect-button/wallet-connect-button.component.ts.html +805 -0
- package/coverage/lib/components/wallet-connect-prompt/index.html +116 -0
- package/coverage/lib/components/wallet-connect-prompt/wallet-connect-prompt.component.ts.html +409 -0
- package/coverage/lib/components/wallet-connected-guard/index.html +116 -0
- package/coverage/lib/components/wallet-connected-guard/wallet-connected-guard.component.ts.html +304 -0
- package/coverage/lib/components/wallet-connection-modal/connection-method-step/connection-method-step.component.ts.html +436 -0
- package/coverage/lib/components/wallet-connection-modal/connection-method-step/index.html +116 -0
- package/coverage/lib/components/wallet-connection-modal/index.html +116 -0
- package/coverage/lib/components/wallet-connection-modal/qr-pairing-step/index.html +116 -0
- package/coverage/lib/components/wallet-connection-modal/qr-pairing-step/qr-pairing-step.component.ts.html +2287 -0
- package/coverage/lib/components/wallet-connection-modal/wallet-connection-modal.component.ts.html +2275 -0
- package/coverage/lib/components/wallet-session-display/index.html +116 -0
- package/coverage/lib/components/wallet-session-display/wallet-session-display.component.ts.html +676 -0
- package/coverage/lib/components/wallet-transaction-status/index.html +116 -0
- package/coverage/lib/components/wallet-transaction-status/wallet-transaction-status.component.ts.html +703 -0
- package/coverage/lib/directives/index.html +146 -0
- package/coverage/lib/directives/wallet-connected.directive.ts.html +670 -0
- package/coverage/lib/directives/wallet-context.directive.ts.html +547 -0
- package/coverage/lib/directives/wallet-events.directive.ts.html +781 -0
- package/coverage/lib/hsuite-wallet.module.ts.html +715 -0
- package/coverage/lib/index.html +116 -0
- package/coverage/lib/models/connection-config.model.ts.html +280 -0
- package/coverage/lib/models/index.html +131 -0
- package/coverage/lib/models/provider-types.ts.html +577 -0
- package/coverage/lib/providers/base-wallet-provider.ts.html +1138 -0
- package/coverage/lib/providers/hsuite-native/channel-client.service.ts.html +2671 -0
- package/coverage/lib/providers/hsuite-native/index.html +116 -0
- package/coverage/lib/providers/hsuite-native-provider.ts.html +2347 -0
- package/coverage/lib/providers/index.html +146 -0
- package/coverage/lib/providers/p2p-native/index.html +131 -0
- package/coverage/lib/providers/p2p-native/p2p-native.provider.ts.html +2254 -0
- package/coverage/lib/providers/p2p-native/p2p-session-manager.ts.html +2170 -0
- package/coverage/lib/providers/wallet-error-handler.ts.html +1132 -0
- package/coverage/lib/providers/walletconnect/core/index.html +176 -0
- package/coverage/lib/providers/walletconnect/core/session-health.ts.html +673 -0
- package/coverage/lib/providers/walletconnect/core/walletconnect-client-manager.ts.html +1177 -0
- package/coverage/lib/providers/walletconnect/core/walletconnect-provider.ts.html +2563 -0
- package/coverage/lib/providers/walletconnect/core/walletconnect-session-store.ts.html +904 -0
- package/coverage/lib/providers/walletconnect/core/walletconnect-signing-orchestrator.ts.html +982 -0
- package/coverage/lib/providers/walletconnect/signers/hedera-signer.ts.html +1915 -0
- package/coverage/lib/providers/walletconnect/signers/index.html +146 -0
- package/coverage/lib/providers/walletconnect/signers/signer-factory.ts.html +445 -0
- package/coverage/lib/providers/walletconnect/signers/xrpl-signer.ts.html +1519 -0
- package/coverage/lib/services/index.html +191 -0
- package/coverage/lib/services/logger.service.ts.html +463 -0
- package/coverage/lib/services/transaction-builders/base-transaction-builder.service.ts.html +1840 -0
- package/coverage/lib/services/transaction-builders/hedera-amount-utils.ts.html +337 -0
- package/coverage/lib/services/transaction-builders/hedera-transaction-builder.service.ts.html +3940 -0
- package/coverage/lib/services/transaction-builders/index.html +161 -0
- package/coverage/lib/services/transaction-builders/xrpl-transaction-builder.service.ts.html +2581 -0
- package/coverage/lib/services/transaction.service.ts.html +1123 -0
- package/coverage/lib/services/unified-wallet.service.ts.html +2641 -0
- package/coverage/lib/services/wallet-context.service.ts.html +637 -0
- package/coverage/lib/services/wallet-event-bus.service.ts.html +643 -0
- package/coverage/lib/services/wallet-providers.service.ts.html +496 -0
- package/coverage/lib/transports/chrome-extension-transport.ts.html +823 -0
- package/coverage/lib/transports/index.html +116 -0
- package/coverage/lib/utils/index.html +116 -0
- package/coverage/lib/utils/ledger-icons.util.ts.html +319 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/README.md +48 -0
- package/dist/fesm2022/hsuite-native-connect-angular.mjs +14592 -0
- package/dist/fesm2022/hsuite-native-connect-angular.mjs.map +1 -0
- package/dist/index.d.ts +6949 -0
- package/examples/minimal-connect.ts +178 -0
- package/examples/multi-protocol.ts +495 -0
- package/examples/transaction-signing.ts +361 -0
- package/jest.config.json +45 -0
- package/karma.conf.js +42 -0
- package/ng-package.json +20 -0
- package/package.json +60 -0
- package/src/index.ts +203 -0
- package/src/lib/components/account-selector/account-actions/account-actions.component.ts +261 -0
- package/src/lib/components/account-selector/account-filter/account-filter.component.ts +401 -0
- package/src/lib/components/account-selector/account-formatting.service.ts +200 -0
- package/src/lib/components/account-selector/account-grouping.service.ts +227 -0
- package/src/lib/components/account-selector/account-list/account-list.component.ts +470 -0
- package/src/lib/components/account-selector/account-selector.component.html +135 -0
- package/src/lib/components/account-selector/account-selector.component.scss +2039 -0
- package/src/lib/components/account-selector/account-selector.component.ts +470 -0
- package/src/lib/components/account-selector/account-selector.service.ts +501 -0
- package/src/lib/components/wallet-account-display/wallet-account-display.component.html +34 -0
- package/src/lib/components/wallet-account-display/wallet-account-display.component.scss +99 -0
- package/src/lib/components/wallet-account-display/wallet-account-display.component.ts +140 -0
- package/src/lib/components/wallet-connect-button/wallet-connect-button.component.html +14 -0
- package/src/lib/components/wallet-connect-button/wallet-connect-button.component.scss +272 -0
- package/src/lib/components/wallet-connect-button/wallet-connect-button.component.ts +240 -0
- package/src/lib/components/wallet-connect-prompt/wallet-connect-prompt.component.html +24 -0
- package/src/lib/components/wallet-connect-prompt/wallet-connect-prompt.component.scss +50 -0
- package/src/lib/components/wallet-connect-prompt/wallet-connect-prompt.component.ts +108 -0
- package/src/lib/components/wallet-connected-guard/wallet-connected-guard.component.html +24 -0
- package/src/lib/components/wallet-connected-guard/wallet-connected-guard.component.ts +73 -0
- package/src/lib/components/wallet-connection-modal/connection-method-step/connection-method-step.component.html +56 -0
- package/src/lib/components/wallet-connection-modal/connection-method-step/connection-method-step.component.scss +218 -0
- package/src/lib/components/wallet-connection-modal/connection-method-step/connection-method-step.component.ts +117 -0
- package/src/lib/components/wallet-connection-modal/qr-pairing-step/qr-pairing-step.component.html +94 -0
- package/src/lib/components/wallet-connection-modal/qr-pairing-step/qr-pairing-step.component.scss +272 -0
- package/src/lib/components/wallet-connection-modal/qr-pairing-step/qr-pairing-step.component.ts +734 -0
- package/src/lib/components/wallet-connection-modal/wallet-connection-modal.component.html +197 -0
- package/src/lib/components/wallet-connection-modal/wallet-connection-modal.component.scss +678 -0
- package/src/lib/components/wallet-connection-modal/wallet-connection-modal.component.ts +730 -0
- package/src/lib/components/wallet-session-display/wallet-session-display.component.html +110 -0
- package/src/lib/components/wallet-session-display/wallet-session-display.component.scss +179 -0
- package/src/lib/components/wallet-session-display/wallet-session-display.component.ts +197 -0
- package/src/lib/components/wallet-transaction-status/wallet-transaction-status.component.html +65 -0
- package/src/lib/components/wallet-transaction-status/wallet-transaction-status.component.scss +254 -0
- package/src/lib/components/wallet-transaction-status/wallet-transaction-status.component.ts +206 -0
- package/src/lib/directives/wallet-connected.directive.ts +195 -0
- package/src/lib/directives/wallet-context.directive.ts +154 -0
- package/src/lib/directives/wallet-events.directive.ts +232 -0
- package/src/lib/hsuite-wallet.module.ts +210 -0
- package/src/lib/models/connection-config.model.ts +65 -0
- package/src/lib/models/provider-types.ts +164 -0
- package/src/lib/models/unified-account.model.ts +76 -0
- package/src/lib/models/wallet-context.model.ts +121 -0
- package/src/lib/models/wallet-events.model.ts +158 -0
- package/src/lib/providers/base-wallet-provider.ts +351 -0
- package/src/lib/providers/hsuite-native/channel-client.service.spec.ts +73 -0
- package/src/lib/providers/hsuite-native/channel-client.service.ts +862 -0
- package/src/lib/providers/hsuite-native/index.ts +8 -0
- package/src/lib/providers/hsuite-native-provider.ts +754 -0
- package/src/lib/providers/mobile-native/mobile-native.provider.spec.ts +19 -0
- package/src/lib/providers/p2p-native/index.ts +30 -0
- package/src/lib/providers/p2p-native/p2p-native.provider.spec.ts +523 -0
- package/src/lib/providers/p2p-native/p2p-native.provider.ts +723 -0
- package/src/lib/providers/p2p-native/p2p-session-manager.ts +695 -0
- package/src/lib/providers/wallet-error-handler.ts +349 -0
- package/src/lib/providers/walletconnect/core/base-signer.interface.ts +122 -0
- package/src/lib/providers/walletconnect/core/session-health.ts +196 -0
- package/src/lib/providers/walletconnect/core/walletconnect-client-manager.ts +364 -0
- package/src/lib/providers/walletconnect/core/walletconnect-provider.integration.spec.ts +348 -0
- package/src/lib/providers/walletconnect/core/walletconnect-provider.ts +826 -0
- package/src/lib/providers/walletconnect/core/walletconnect-session-store.ts +273 -0
- package/src/lib/providers/walletconnect/core/walletconnect-signing-orchestrator.ts +299 -0
- package/src/lib/providers/walletconnect/core/walletconnect-types.ts +48 -0
- package/src/lib/providers/walletconnect/index.ts +33 -0
- package/src/lib/providers/walletconnect/signers/hedera-signer.spec.ts +367 -0
- package/src/lib/providers/walletconnect/signers/hedera-signer.ts +610 -0
- package/src/lib/providers/walletconnect/signers/signer-factory.spec.ts +62 -0
- package/src/lib/providers/walletconnect/signers/signer-factory.ts +120 -0
- package/src/lib/providers/walletconnect/signers/xrpl-signer.spec.ts +296 -0
- package/src/lib/providers/walletconnect/signers/xrpl-signer.ts +478 -0
- package/src/lib/services/logger.service.ts +126 -0
- package/src/lib/services/transaction-builders/base-transaction-builder.service.ts +585 -0
- package/src/lib/services/transaction-builders/hedera-amount-utils.ts +84 -0
- package/src/lib/services/transaction-builders/hedera-transaction-builder.service.spec.ts +741 -0
- package/src/lib/services/transaction-builders/hedera-transaction-builder.service.ts +1285 -0
- package/src/lib/services/transaction-builders/index.ts +54 -0
- package/src/lib/services/transaction-builders/xrpl-transaction-builder.service.spec.ts +937 -0
- package/src/lib/services/transaction-builders/xrpl-transaction-builder.service.ts +832 -0
- package/src/lib/services/transaction.service.ts +346 -0
- package/src/lib/services/unified-wallet.service.spec.ts +1382 -0
- package/src/lib/services/unified-wallet.service.ts +852 -0
- package/src/lib/services/wallet-context.service.ts +184 -0
- package/src/lib/services/wallet-event-bus.service.ts +186 -0
- package/src/lib/services/wallet-providers.service.ts +137 -0
- package/src/lib/transports/chrome-extension-transport.ts +246 -0
- package/src/lib/utils/index.ts +14 -0
- package/src/lib/utils/ledger-icons.util.ts +78 -0
- package/test/test-setup.ts +21 -0
- package/test-setup.ts +63 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +29 -0
- package/tsconfig.spec.json +15 -0
- 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
|
+
}
|