@bopen-io/wallet-toolbox 1.7.18
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/.claude/settings.local.json +10 -0
- package/.env.template +22 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- package/.github/ISSUE_TEMPLATE/discussion.md +24 -0
- package/.github/pull_request_template.md +22 -0
- package/.github/workflows/push.yaml +145 -0
- package/.prettierrc +10 -0
- package/CHANGELOG.md +280 -0
- package/CONTRIBUTING.md +89 -0
- package/README.md +43 -0
- package/docs/README.md +85 -0
- package/docs/client.md +19627 -0
- package/docs/monitor.md +953 -0
- package/docs/open-rpc/index.html +46 -0
- package/docs/services.md +6377 -0
- package/docs/setup.md +1268 -0
- package/docs/storage.md +5367 -0
- package/docs/wallet.md +19626 -0
- package/jest.config.ts +25 -0
- package/license.md +28 -0
- package/out/tsconfig.all.tsbuildinfo +1 -0
- package/package.json +63 -0
- package/src/CWIStyleWalletManager.ts +1999 -0
- package/src/Setup.ts +579 -0
- package/src/SetupClient.ts +322 -0
- package/src/SetupWallet.ts +108 -0
- package/src/SimpleWalletManager.ts +526 -0
- package/src/Wallet.ts +1169 -0
- package/src/WalletAuthenticationManager.ts +153 -0
- package/src/WalletLogger.ts +213 -0
- package/src/WalletPermissionsManager.ts +3660 -0
- package/src/WalletSettingsManager.ts +114 -0
- package/src/__tests/CWIStyleWalletManager.test.d.ts.map +1 -0
- package/src/__tests/CWIStyleWalletManager.test.js.map +1 -0
- package/src/__tests/CWIStyleWalletManager.test.ts +675 -0
- package/src/__tests/WalletPermissionsManager.callbacks.test.ts +323 -0
- package/src/__tests/WalletPermissionsManager.checks.test.ts +844 -0
- package/src/__tests/WalletPermissionsManager.encryption.test.ts +412 -0
- package/src/__tests/WalletPermissionsManager.fixtures.ts +307 -0
- package/src/__tests/WalletPermissionsManager.flows.test.ts +462 -0
- package/src/__tests/WalletPermissionsManager.initialization.test.ts +300 -0
- package/src/__tests/WalletPermissionsManager.pmodules.test.ts +798 -0
- package/src/__tests/WalletPermissionsManager.proxying.test.ts +724 -0
- package/src/__tests/WalletPermissionsManager.tokens.test.ts +503 -0
- package/src/index.all.ts +27 -0
- package/src/index.client.ts +25 -0
- package/src/index.mobile.ts +21 -0
- package/src/index.ts +1 -0
- package/src/monitor/Monitor.ts +412 -0
- package/src/monitor/MonitorDaemon.ts +188 -0
- package/src/monitor/README.md +3 -0
- package/src/monitor/__test/MonitorDaemon.man.test.ts +45 -0
- package/src/monitor/tasks/TaskCheckForProofs.ts +243 -0
- package/src/monitor/tasks/TaskCheckNoSends.ts +73 -0
- package/src/monitor/tasks/TaskClock.ts +33 -0
- package/src/monitor/tasks/TaskFailAbandoned.ts +54 -0
- package/src/monitor/tasks/TaskMonitorCallHistory.ts +26 -0
- package/src/monitor/tasks/TaskNewHeader.ts +93 -0
- package/src/monitor/tasks/TaskPurge.ts +68 -0
- package/src/monitor/tasks/TaskReorg.ts +89 -0
- package/src/monitor/tasks/TaskReviewStatus.ts +48 -0
- package/src/monitor/tasks/TaskSendWaiting.ts +122 -0
- package/src/monitor/tasks/TaskSyncWhenIdle.ts +26 -0
- package/src/monitor/tasks/TaskUnFail.ts +151 -0
- package/src/monitor/tasks/WalletMonitorTask.ts +47 -0
- package/src/sdk/CertOpsWallet.ts +18 -0
- package/src/sdk/PrivilegedKeyManager.ts +372 -0
- package/src/sdk/README.md +13 -0
- package/src/sdk/WERR_errors.ts +234 -0
- package/src/sdk/WalletError.ts +170 -0
- package/src/sdk/WalletErrorFromJson.ts +80 -0
- package/src/sdk/WalletServices.interfaces.ts +700 -0
- package/src/sdk/WalletSigner.interfaces.ts +11 -0
- package/src/sdk/WalletStorage.interfaces.ts +606 -0
- package/src/sdk/__test/CertificateLifeCycle.test.ts +131 -0
- package/src/sdk/__test/PrivilegedKeyManager.test.ts +738 -0
- package/src/sdk/__test/WalletError.test.ts +318 -0
- package/src/sdk/__test/validationHelpers.test.ts +21 -0
- package/src/sdk/index.ts +10 -0
- package/src/sdk/types.ts +226 -0
- package/src/services/README.md +11 -0
- package/src/services/ServiceCollection.ts +248 -0
- package/src/services/Services.ts +603 -0
- package/src/services/__tests/ARC.man.test.ts +123 -0
- package/src/services/__tests/ARC.timeout.man.test.ts +79 -0
- package/src/services/__tests/ArcGorillaPool.man.test.ts +108 -0
- package/src/services/__tests/arcServices.test.ts +8 -0
- package/src/services/__tests/bitrails.test.ts +56 -0
- package/src/services/__tests/getMerklePath.test.ts +15 -0
- package/src/services/__tests/getRawTx.test.ts +13 -0
- package/src/services/__tests/postBeef.test.ts +104 -0
- package/src/services/__tests/verifyBeef.test.ts +50 -0
- package/src/services/chaintracker/BHServiceClient.ts +212 -0
- package/src/services/chaintracker/ChaintracksChainTracker.ts +71 -0
- package/src/services/chaintracker/__tests/ChaintracksChainTracker.test.ts +33 -0
- package/src/services/chaintracker/__tests/ChaintracksServiceClient.test.ts +29 -0
- package/src/services/chaintracker/chaintracks/Api/BlockHeaderApi.ts +72 -0
- package/src/services/chaintracker/chaintracks/Api/BulkIngestorApi.ts +83 -0
- package/src/services/chaintracker/chaintracks/Api/BulkStorageApi.ts +92 -0
- package/src/services/chaintracker/chaintracks/Api/ChaintracksApi.ts +64 -0
- package/src/services/chaintracker/chaintracks/Api/ChaintracksClientApi.ts +189 -0
- package/src/services/chaintracker/chaintracks/Api/ChaintracksFetchApi.ts +18 -0
- package/src/services/chaintracker/chaintracks/Api/ChaintracksFsApi.ts +58 -0
- package/src/services/chaintracker/chaintracks/Api/ChaintracksStorageApi.ts +386 -0
- package/src/services/chaintracker/chaintracks/Api/LiveIngestorApi.ts +25 -0
- package/src/services/chaintracker/chaintracks/Chaintracks.ts +609 -0
- package/src/services/chaintracker/chaintracks/ChaintracksService.ts +199 -0
- package/src/services/chaintracker/chaintracks/ChaintracksServiceClient.ts +154 -0
- package/src/services/chaintracker/chaintracks/Ingest/BulkIngestorBase.ts +176 -0
- package/src/services/chaintracker/chaintracks/Ingest/BulkIngestorCDN.ts +174 -0
- package/src/services/chaintracker/chaintracks/Ingest/BulkIngestorCDNBabbage.ts +18 -0
- package/src/services/chaintracker/chaintracks/Ingest/BulkIngestorWhatsOnChainCdn.ts +113 -0
- package/src/services/chaintracker/chaintracks/Ingest/BulkIngestorWhatsOnChainWs.ts +81 -0
- package/src/services/chaintracker/chaintracks/Ingest/LiveIngestorBase.ts +86 -0
- package/src/services/chaintracker/chaintracks/Ingest/LiveIngestorTeranodeP2P.ts +59 -0
- package/src/services/chaintracker/chaintracks/Ingest/LiveIngestorWhatsOnChainPoll.ts +104 -0
- package/src/services/chaintracker/chaintracks/Ingest/LiveIngestorWhatsOnChainWs.ts +66 -0
- package/src/services/chaintracker/chaintracks/Ingest/WhatsOnChainIngestorWs.ts +566 -0
- package/src/services/chaintracker/chaintracks/Ingest/WhatsOnChainServices.ts +219 -0
- package/src/services/chaintracker/chaintracks/Ingest/__tests/BulkIngestorCDNBabbage.test.ts +54 -0
- package/src/services/chaintracker/chaintracks/Ingest/__tests/LiveIngestorWhatsOnChainPoll.test.ts +33 -0
- package/src/services/chaintracker/chaintracks/Ingest/__tests/WhatsOnChainServices.test.ts +124 -0
- package/src/services/chaintracker/chaintracks/Storage/BulkStorageBase.ts +92 -0
- package/src/services/chaintracker/chaintracks/Storage/ChaintracksKnexMigrations.ts +104 -0
- package/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.ts +382 -0
- package/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageIdb.ts +574 -0
- package/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageKnex.ts +438 -0
- package/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageMemory.ts +29 -0
- package/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageNoDb.ts +304 -0
- package/src/services/chaintracker/chaintracks/Storage/__tests/ChaintracksStorageIdb.test.ts +102 -0
- package/src/services/chaintracker/chaintracks/Storage/__tests/ChaintracksStorageKnex.test.ts +45 -0
- package/src/services/chaintracker/chaintracks/__tests/Chaintracks.test.ts +77 -0
- package/src/services/chaintracker/chaintracks/__tests/ChaintracksClientApi.test.ts +192 -0
- package/src/services/chaintracker/chaintracks/__tests/LocalCdnServer.ts +75 -0
- package/src/services/chaintracker/chaintracks/__tests/createIdbChaintracks.test.ts +62 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest349/mainNetBlockHeaders.json +1 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest349/mainNet_0.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest349/mainNet_1.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest349/mainNet_2.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest349/mainNet_3.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest379/mainNetBlockHeaders.json +1 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest379/mainNet_0.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest379/mainNet_1.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest379/mainNet_2.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest379/mainNet_3.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest399/mainNetBlockHeaders.json +1 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest399/mainNet_0.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest399/mainNet_1.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest399/mainNet_2.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest399/mainNet_3.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest402/mainNetBlockHeaders.json +1 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest402/mainNet_0.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest402/mainNet_1.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest402/mainNet_2.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest402/mainNet_3.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest402/mainNet_4.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest499/mainNetBlockHeaders.json +1 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest499/mainNet_0.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest499/mainNet_1.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest499/mainNet_2.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest499/mainNet_3.headers +0 -0
- package/src/services/chaintracker/chaintracks/__tests/data/cdnTest499/mainNet_4.headers +0 -0
- package/src/services/chaintracker/chaintracks/createDefaultIdbChaintracksOptions.ts +92 -0
- package/src/services/chaintracker/chaintracks/createDefaultKnexChaintracksOptions.ts +111 -0
- package/src/services/chaintracker/chaintracks/createDefaultNoDbChaintracksOptions.ts +91 -0
- package/src/services/chaintracker/chaintracks/createIdbChaintracks.ts +60 -0
- package/src/services/chaintracker/chaintracks/createKnexChaintracks.ts +65 -0
- package/src/services/chaintracker/chaintracks/createNoDbChaintracks.ts +60 -0
- package/src/services/chaintracker/chaintracks/index.all.ts +12 -0
- package/src/services/chaintracker/chaintracks/index.client.ts +4 -0
- package/src/services/chaintracker/chaintracks/index.mobile.ts +37 -0
- package/src/services/chaintracker/chaintracks/util/BulkFileDataManager.ts +975 -0
- package/src/services/chaintracker/chaintracks/util/BulkFileDataReader.ts +60 -0
- package/src/services/chaintracker/chaintracks/util/BulkFilesReader.ts +336 -0
- package/src/services/chaintracker/chaintracks/util/BulkHeaderFile.ts +247 -0
- package/src/services/chaintracker/chaintracks/util/ChaintracksFetch.ts +69 -0
- package/src/services/chaintracker/chaintracks/util/ChaintracksFs.ts +141 -0
- package/src/services/chaintracker/chaintracks/util/HeightRange.ts +153 -0
- package/src/services/chaintracker/chaintracks/util/SingleWriterMultiReaderLock.ts +76 -0
- package/src/services/chaintracker/chaintracks/util/__tests/BulkFileDataManager.test.ts +304 -0
- package/src/services/chaintracker/chaintracks/util/__tests/ChaintracksFetch.test.ts +60 -0
- package/src/services/chaintracker/chaintracks/util/__tests/HeightRange.test.ts +67 -0
- package/src/services/chaintracker/chaintracks/util/__tests/SingleWriterMultiReaderLock.test.ts +49 -0
- package/src/services/chaintracker/chaintracks/util/blockHeaderUtilities.ts +573 -0
- package/src/services/chaintracker/chaintracks/util/dirtyHashes.ts +29 -0
- package/src/services/chaintracker/chaintracks/util/validBulkHeaderFilesByFileHash.ts +432 -0
- package/src/services/chaintracker/index.all.ts +4 -0
- package/src/services/chaintracker/index.client.ts +4 -0
- package/src/services/chaintracker/index.mobile.ts +4 -0
- package/src/services/createDefaultWalletServicesOptions.ts +77 -0
- package/src/services/index.ts +1 -0
- package/src/services/processingErrors/arcSuccessError.json +76 -0
- package/src/services/providers/ARC.ts +350 -0
- package/src/services/providers/Bitails.ts +256 -0
- package/src/services/providers/SdkWhatsOnChain.ts +83 -0
- package/src/services/providers/WhatsOnChain.ts +883 -0
- package/src/services/providers/__tests/WhatsOnChain.test.ts +242 -0
- package/src/services/providers/__tests/exchangeRates.test.ts +18 -0
- package/src/services/providers/exchangeRates.ts +265 -0
- package/src/services/providers/getBeefForTxid.ts +369 -0
- package/src/signer/README.md +5 -0
- package/src/signer/WalletSigner.ts +17 -0
- package/src/signer/methods/acquireDirectCertificate.ts +52 -0
- package/src/signer/methods/buildSignableTransaction.ts +183 -0
- package/src/signer/methods/completeSignedTransaction.ts +117 -0
- package/src/signer/methods/createAction.ts +172 -0
- package/src/signer/methods/internalizeAction.ts +106 -0
- package/src/signer/methods/proveCertificate.ts +43 -0
- package/src/signer/methods/signAction.ts +54 -0
- package/src/storage/README.md +14 -0
- package/src/storage/StorageIdb.ts +2304 -0
- package/src/storage/StorageKnex.ts +1425 -0
- package/src/storage/StorageProvider.ts +810 -0
- package/src/storage/StorageReader.ts +194 -0
- package/src/storage/StorageReaderWriter.ts +432 -0
- package/src/storage/StorageSyncReader.ts +34 -0
- package/src/storage/WalletStorageManager.ts +943 -0
- package/src/storage/__test/StorageIdb.test.ts +43 -0
- package/src/storage/__test/WalletStorageManager.test.ts +275 -0
- package/src/storage/__test/adminStats.man.test.ts +89 -0
- package/src/storage/__test/getBeefForTransaction.test.ts +385 -0
- package/src/storage/index.all.ts +11 -0
- package/src/storage/index.client.ts +7 -0
- package/src/storage/index.mobile.ts +6 -0
- package/src/storage/methods/ListActionsSpecOp.ts +70 -0
- package/src/storage/methods/ListOutputsSpecOp.ts +129 -0
- package/src/storage/methods/__test/GenerateChange/generateChangeSdk.test.ts +1057 -0
- package/src/storage/methods/__test/GenerateChange/randomValsUsed1.ts +20 -0
- package/src/storage/methods/__test/offsetKey.test.ts +274 -0
- package/src/storage/methods/attemptToPostReqsToNetwork.ts +389 -0
- package/src/storage/methods/createAction.ts +947 -0
- package/src/storage/methods/generateChange.ts +556 -0
- package/src/storage/methods/getBeefForTransaction.ts +139 -0
- package/src/storage/methods/getSyncChunk.ts +293 -0
- package/src/storage/methods/internalizeAction.ts +562 -0
- package/src/storage/methods/listActionsIdb.ts +183 -0
- package/src/storage/methods/listActionsKnex.ts +226 -0
- package/src/storage/methods/listCertificates.ts +73 -0
- package/src/storage/methods/listOutputsIdb.ts +203 -0
- package/src/storage/methods/listOutputsKnex.ts +263 -0
- package/src/storage/methods/offsetKey.ts +89 -0
- package/src/storage/methods/processAction.ts +420 -0
- package/src/storage/methods/purgeData.ts +251 -0
- package/src/storage/methods/purgeDataIdb.ts +10 -0
- package/src/storage/methods/reviewStatus.ts +101 -0
- package/src/storage/methods/reviewStatusIdb.ts +43 -0
- package/src/storage/methods/utils.Buffer.ts +33 -0
- package/src/storage/methods/utils.ts +56 -0
- package/src/storage/remoting/StorageClient.ts +567 -0
- package/src/storage/remoting/StorageMobile.ts +544 -0
- package/src/storage/remoting/StorageServer.ts +291 -0
- package/src/storage/remoting/__test/StorageClient.test.ts +113 -0
- package/src/storage/schema/KnexMigrations.ts +489 -0
- package/src/storage/schema/StorageIdbSchema.ts +150 -0
- package/src/storage/schema/entities/EntityBase.ts +210 -0
- package/src/storage/schema/entities/EntityCertificate.ts +188 -0
- package/src/storage/schema/entities/EntityCertificateField.ts +136 -0
- package/src/storage/schema/entities/EntityCommission.ts +148 -0
- package/src/storage/schema/entities/EntityOutput.ts +290 -0
- package/src/storage/schema/entities/EntityOutputBasket.ts +153 -0
- package/src/storage/schema/entities/EntityOutputTag.ts +121 -0
- package/src/storage/schema/entities/EntityOutputTagMap.ts +123 -0
- package/src/storage/schema/entities/EntityProvenTx.ts +319 -0
- package/src/storage/schema/entities/EntityProvenTxReq.ts +580 -0
- package/src/storage/schema/entities/EntitySyncState.ts +389 -0
- package/src/storage/schema/entities/EntityTransaction.ts +306 -0
- package/src/storage/schema/entities/EntityTxLabel.ts +121 -0
- package/src/storage/schema/entities/EntityTxLabelMap.ts +123 -0
- package/src/storage/schema/entities/EntityUser.ts +112 -0
- package/src/storage/schema/entities/MergeEntity.ts +73 -0
- package/src/storage/schema/entities/__tests/CertificateFieldTests.test.ts +353 -0
- package/src/storage/schema/entities/__tests/CertificateTests.test.ts +354 -0
- package/src/storage/schema/entities/__tests/CommissionTests.test.ts +371 -0
- package/src/storage/schema/entities/__tests/OutputBasketTests.test.ts +278 -0
- package/src/storage/schema/entities/__tests/OutputTagMapTests.test.ts +242 -0
- package/src/storage/schema/entities/__tests/OutputTagTests.test.ts +288 -0
- package/src/storage/schema/entities/__tests/OutputTests.test.ts +464 -0
- package/src/storage/schema/entities/__tests/ProvenTxReqTests.test.ts +340 -0
- package/src/storage/schema/entities/__tests/ProvenTxTests.test.ts +504 -0
- package/src/storage/schema/entities/__tests/SyncStateTests.test.ts +288 -0
- package/src/storage/schema/entities/__tests/TransactionTests.test.ts +604 -0
- package/src/storage/schema/entities/__tests/TxLabelMapTests.test.ts +361 -0
- package/src/storage/schema/entities/__tests/TxLabelTests.test.ts +198 -0
- package/src/storage/schema/entities/__tests/stampLogTests.test.ts +90 -0
- package/src/storage/schema/entities/__tests/usersTests.test.ts +340 -0
- package/src/storage/schema/entities/index.ts +16 -0
- package/src/storage/schema/tables/TableCertificate.ts +21 -0
- package/src/storage/schema/tables/TableCertificateField.ts +12 -0
- package/src/storage/schema/tables/TableCommission.ts +13 -0
- package/src/storage/schema/tables/TableMonitorEvent.ts +9 -0
- package/src/storage/schema/tables/TableOutput.ts +64 -0
- package/src/storage/schema/tables/TableOutputBasket.ts +12 -0
- package/src/storage/schema/tables/TableOutputTag.ts +10 -0
- package/src/storage/schema/tables/TableOutputTagMap.ts +9 -0
- package/src/storage/schema/tables/TableProvenTx.ts +14 -0
- package/src/storage/schema/tables/TableProvenTxReq.ts +65 -0
- package/src/storage/schema/tables/TableSettings.ts +17 -0
- package/src/storage/schema/tables/TableSyncState.ts +18 -0
- package/src/storage/schema/tables/TableTransaction.ts +54 -0
- package/src/storage/schema/tables/TableTxLabel.ts +10 -0
- package/src/storage/schema/tables/TableTxLabelMap.ts +9 -0
- package/src/storage/schema/tables/TableUser.ts +16 -0
- package/src/storage/schema/tables/index.ts +16 -0
- package/src/storage/sync/StorageMySQLDojoReader.ts +696 -0
- package/src/storage/sync/index.ts +1 -0
- package/src/utility/Format.ts +133 -0
- package/src/utility/README.md +3 -0
- package/src/utility/ReaderUint8Array.ts +187 -0
- package/src/utility/ScriptTemplateBRC29.ts +73 -0
- package/src/utility/__tests/utilityHelpers.noBuffer.test.ts +109 -0
- package/src/utility/aggregateResults.ts +68 -0
- package/src/utility/identityUtils.ts +159 -0
- package/src/utility/index.all.ts +7 -0
- package/src/utility/index.client.ts +7 -0
- package/src/utility/parseTxScriptOffsets.ts +29 -0
- package/src/utility/stampLog.ts +69 -0
- package/src/utility/tscProofToMerklePath.ts +48 -0
- package/src/utility/utilityHelpers.buffer.ts +34 -0
- package/src/utility/utilityHelpers.noBuffer.ts +60 -0
- package/src/utility/utilityHelpers.ts +275 -0
- package/src/wab-client/WABClient.ts +94 -0
- package/src/wab-client/__tests/WABClient.man.test.ts +59 -0
- package/src/wab-client/auth-method-interactors/AuthMethodInteractor.ts +47 -0
- package/src/wab-client/auth-method-interactors/DevConsoleInteractor.ts +73 -0
- package/src/wab-client/auth-method-interactors/PersonaIDInteractor.ts +35 -0
- package/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.ts +72 -0
- package/syncVersions.js +71 -0
- package/test/Wallet/StorageClient/storageClient.man.test.ts +75 -0
- package/test/Wallet/action/abortAction.test.ts +47 -0
- package/test/Wallet/action/createAction.test.ts +299 -0
- package/test/Wallet/action/createAction2.test.ts +1273 -0
- package/test/Wallet/action/createActionToGenerateBeefs.man.test.ts +293 -0
- package/test/Wallet/action/internalizeAction.a.test.ts +286 -0
- package/test/Wallet/action/internalizeAction.test.ts +682 -0
- package/test/Wallet/action/relinquishOutput.test.ts +37 -0
- package/test/Wallet/certificate/acquireCertificate.test.ts +298 -0
- package/test/Wallet/certificate/listCertificates.test.ts +346 -0
- package/test/Wallet/construct/Wallet.constructor.test.ts +57 -0
- package/test/Wallet/get/getHeaderForHeight.test.ts +82 -0
- package/test/Wallet/get/getHeight.test.ts +52 -0
- package/test/Wallet/get/getKnownTxids.test.ts +86 -0
- package/test/Wallet/get/getNetwork.test.ts +27 -0
- package/test/Wallet/get/getVersion.test.ts +27 -0
- package/test/Wallet/list/listActions.test.ts +279 -0
- package/test/Wallet/list/listActions2.test.ts +1381 -0
- package/test/Wallet/list/listCertificates.test.ts +118 -0
- package/test/Wallet/list/listOutputs.test.ts +447 -0
- package/test/Wallet/live/walletLive.man.test.ts +521 -0
- package/test/Wallet/local/localWallet.man.test.ts +93 -0
- package/test/Wallet/local/localWallet2.man.test.ts +277 -0
- package/test/Wallet/signAction/mountaintop.man.test.ts +130 -0
- package/test/Wallet/specOps/specOps.man.test.ts +220 -0
- package/test/Wallet/support/janitor.man.test.ts +40 -0
- package/test/Wallet/support/operations.man.test.ts +407 -0
- package/test/Wallet/support/reqErrorReview.2025.05.06.man.test.ts +347 -0
- package/test/Wallet/sync/Wallet.sync.test.ts +215 -0
- package/test/Wallet/sync/Wallet.updateWalletLegacyTestData.man.test.ts +203 -0
- package/test/Wallet/sync/setActive.test.ts +170 -0
- package/test/WalletClient/LocalKVStore.man.test.ts +114 -0
- package/test/WalletClient/WERR.man.test.ts +35 -0
- package/test/bsv-ts-sdk/LocalKVStore.test.ts +102 -0
- package/test/checkDB.ts +57 -0
- package/test/checkdb +0 -0
- package/test/examples/backup.man.test.ts +59 -0
- package/test/examples/pushdrop.test.ts +282 -0
- package/test/monitor/Monitor.test.ts +620 -0
- package/test/services/Services.test.ts +263 -0
- package/test/storage/KnexMigrations.test.ts +86 -0
- package/test/storage/StorageMySQLDojoReader.man.test.ts +60 -0
- package/test/storage/count.test.ts +177 -0
- package/test/storage/find.test.ts +195 -0
- package/test/storage/findLegacy.test.ts +67 -0
- package/test/storage/idb/allocateChange.test.ts +251 -0
- package/test/storage/idb/count.test.ts +158 -0
- package/test/storage/idb/find.test.ts +177 -0
- package/test/storage/idb/idbSpeed.test.ts +36 -0
- package/test/storage/idb/insert.test.ts +268 -0
- package/test/storage/idb/transactionAbort.test.ts +108 -0
- package/test/storage/idb/update.test.ts +999 -0
- package/test/storage/insert.test.ts +278 -0
- package/test/storage/update.test.ts +1021 -0
- package/test/storage/update2.test.ts +897 -0
- package/test/utils/TestUtilsWalletStorage.ts +2526 -0
- package/test/utils/localWalletMethods.ts +363 -0
- package/test/utils/removeFailedFromDatabase.sql +17 -0
- package/ts2md.json +44 -0
- package/tsconfig.all.json +31 -0
- package/tsconfig.client.json +29 -0
- package/tsconfig.json +17 -0
- package/tsconfig.mobile.json +28 -0
|
@@ -0,0 +1,3660 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WalletInterface,
|
|
3
|
+
Utils,
|
|
4
|
+
PushDrop,
|
|
5
|
+
LockingScript,
|
|
6
|
+
Transaction,
|
|
7
|
+
WalletProtocol,
|
|
8
|
+
Base64String,
|
|
9
|
+
PubKeyHex,
|
|
10
|
+
Beef,
|
|
11
|
+
Validation,
|
|
12
|
+
WalletEncryptArgs,
|
|
13
|
+
WalletDecryptArgs,
|
|
14
|
+
CreateHmacArgs,
|
|
15
|
+
VerifyHmacArgs,
|
|
16
|
+
CreateSignatureArgs,
|
|
17
|
+
VerifySignatureArgs,
|
|
18
|
+
InternalizeActionArgs,
|
|
19
|
+
ListOutputsArgs,
|
|
20
|
+
RelinquishOutputArgs,
|
|
21
|
+
GetPublicKeyArgs,
|
|
22
|
+
CreateActionArgs,
|
|
23
|
+
ListOutputsResult
|
|
24
|
+
} from '@bsv/sdk'
|
|
25
|
+
|
|
26
|
+
////// TODO: ADD SUPPORT FOR ADMIN COUNTERPARTIES BASED ON WALLET STORAGE
|
|
27
|
+
////// PROHIBITION OF SPECIAL OPERATIONS IS ALSO CRITICAL.
|
|
28
|
+
////// !!!!!!!! SECURITY-CRITICAL ADDITION — DO NOT USE UNTIL IMPLEMENTED.
|
|
29
|
+
|
|
30
|
+
function deepEqual(object1: any, object2: any): boolean {
|
|
31
|
+
if (object1 === null || object1 === undefined || object2 === null || object2 === undefined) {
|
|
32
|
+
return object1 === object2
|
|
33
|
+
}
|
|
34
|
+
const keys1 = Object.keys(object1)
|
|
35
|
+
const keys2 = Object.keys(object2)
|
|
36
|
+
|
|
37
|
+
if (keys1.length !== keys2.length) {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const key of keys1) {
|
|
42
|
+
const val1 = object1[key]
|
|
43
|
+
const val2 = object2[key]
|
|
44
|
+
const areObjects = isObject(val1) && isObject(val2)
|
|
45
|
+
if ((areObjects && !deepEqual(val1, val2)) || (!areObjects && val1 !== val2)) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isObject(object: any): boolean {
|
|
54
|
+
return object != null && typeof object === 'object'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A permissions module handles request/response transformation for a specific P-protocol or P-basket scheme under BRC-98/99.
|
|
59
|
+
* Modules are registered in the config mapped by their scheme ID.
|
|
60
|
+
*/
|
|
61
|
+
export interface PermissionsModule {
|
|
62
|
+
/**
|
|
63
|
+
* Transforms the request before it's passed to the underlying wallet.
|
|
64
|
+
* Can check and enforce permissions, throw errors, or modify any arguments as needed prior to invocation.
|
|
65
|
+
*
|
|
66
|
+
* @param req - The incoming request with method, args, and originator
|
|
67
|
+
* @returns Transformed arguments that will be passed to the underlying wallet
|
|
68
|
+
*/
|
|
69
|
+
onRequest(req: { method: string; args: object; originator: string }): Promise<{ args: object }>
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Transforms the response from the underlying wallet before returning to caller.
|
|
73
|
+
*
|
|
74
|
+
* @param res - The response from the underlying wallet
|
|
75
|
+
* @param context - Metadata about the original request (method, originator)
|
|
76
|
+
* @returns Transformed response to return to the caller
|
|
77
|
+
*/
|
|
78
|
+
onResponse(
|
|
79
|
+
res: any,
|
|
80
|
+
context: {
|
|
81
|
+
method: string
|
|
82
|
+
originator: string
|
|
83
|
+
}
|
|
84
|
+
): Promise<any>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Describes a group of permissions that can be requested together.
|
|
89
|
+
* This structure is based on BRC-73.
|
|
90
|
+
*/
|
|
91
|
+
export interface GroupedPermissions {
|
|
92
|
+
description?: string
|
|
93
|
+
spendingAuthorization?: {
|
|
94
|
+
amount: number
|
|
95
|
+
description: string
|
|
96
|
+
}
|
|
97
|
+
protocolPermissions?: Array<{
|
|
98
|
+
protocolID: WalletProtocol
|
|
99
|
+
counterparty?: string
|
|
100
|
+
description: string
|
|
101
|
+
}>
|
|
102
|
+
basketAccess?: Array<{
|
|
103
|
+
basket: string
|
|
104
|
+
description: string
|
|
105
|
+
}>
|
|
106
|
+
certificateAccess?: Array<{
|
|
107
|
+
type: string
|
|
108
|
+
fields: string[]
|
|
109
|
+
verifierPublicKey: string
|
|
110
|
+
description: string
|
|
111
|
+
}>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The object passed to the UI when a grouped permission is requested.
|
|
116
|
+
*/
|
|
117
|
+
export interface GroupedPermissionRequest {
|
|
118
|
+
originator: string
|
|
119
|
+
requestID: string
|
|
120
|
+
permissions: GroupedPermissions
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Signature for functions that handle a grouped permission request event.
|
|
125
|
+
*/
|
|
126
|
+
export type GroupedPermissionEventHandler = (request: GroupedPermissionRequest) => void | Promise<void>
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Describes a single requested permission that the user must either grant or deny.
|
|
130
|
+
*
|
|
131
|
+
* Four categories of permission are supported, each with a unique protocol:
|
|
132
|
+
* 1) protocol - "DPACP" (Domain Protocol Access Control Protocol)
|
|
133
|
+
* 2) basket - "DBAP" (Domain Basket Access Protocol)
|
|
134
|
+
* 3) certificate - "DCAP" (Domain Certificate Access Protocol)
|
|
135
|
+
* 4) spending - "DSAP" (Domain Spending Authorization Protocol)
|
|
136
|
+
*
|
|
137
|
+
* This model underpins "requests" made to the user for permission, which the user can
|
|
138
|
+
* either grant or deny. The manager can then create on-chain tokens (PushDrop outputs)
|
|
139
|
+
* if permission is granted. Denying requests cause the underlying operation to throw,
|
|
140
|
+
* and no token is created. An "ephemeral" grant is also possible, denoting a one-time
|
|
141
|
+
* authorization without an associated persistent on-chain token.
|
|
142
|
+
*/
|
|
143
|
+
export interface PermissionRequest {
|
|
144
|
+
type: 'protocol' | 'basket' | 'certificate' | 'spending'
|
|
145
|
+
originator: string // The domain or FQDN of the requesting application
|
|
146
|
+
displayOriginator?: string // Optional raw/original originator string for UI purposes
|
|
147
|
+
privileged?: boolean // For "protocol" or "certificate" usage, indicating privileged key usage
|
|
148
|
+
protocolID?: WalletProtocol // For type='protocol': BRC-43 style (securityLevel, protocolName)
|
|
149
|
+
counterparty?: string // For type='protocol': e.g. target public key or "self"/"anyone"
|
|
150
|
+
|
|
151
|
+
basket?: string // For type='basket': the basket name being requested
|
|
152
|
+
|
|
153
|
+
certificate?: {
|
|
154
|
+
// For type='certificate': details about the cert usage
|
|
155
|
+
verifier: string
|
|
156
|
+
certType: string
|
|
157
|
+
fields: string[]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
spending?: {
|
|
161
|
+
// For type='spending': details about the requested spend
|
|
162
|
+
satoshis: number
|
|
163
|
+
lineItems?: Array<{
|
|
164
|
+
type: 'input' | 'output' | 'fee'
|
|
165
|
+
description: string
|
|
166
|
+
satoshis: number
|
|
167
|
+
}>
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
reason?: string // Human-readable explanation for requesting permission
|
|
171
|
+
renewal?: boolean // Whether this request is for renewing an expired token
|
|
172
|
+
previousToken?: PermissionToken // If renewing an expired permission, reference to the old token
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Signature for functions that handle a permission request event, e.g. "Please ask the user to allow basket X".
|
|
177
|
+
*/
|
|
178
|
+
export type PermissionEventHandler = (request: PermissionRequest & { requestID: string }) => void | Promise<void>
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Data structure representing an on-chain permission token.
|
|
182
|
+
* It is typically stored as a single unspent PushDrop output in a special "internal" admin basket belonging to
|
|
183
|
+
* the user, held in their underlying wallet.
|
|
184
|
+
*
|
|
185
|
+
* It can represent any of the four permission categories by having the relevant fields:
|
|
186
|
+
* - DPACP: originator, privileged, protocol, securityLevel, counterparty
|
|
187
|
+
* - DBAP: originator, basketName
|
|
188
|
+
* - DCAP: originator, privileged, verifier, certType, certFields
|
|
189
|
+
* - DSAP: originator, authorizedAmount
|
|
190
|
+
*/
|
|
191
|
+
export interface PermissionToken {
|
|
192
|
+
/** The transaction ID where this token resides. */
|
|
193
|
+
txid: string
|
|
194
|
+
|
|
195
|
+
/** The current transaction encapsulating the token. */
|
|
196
|
+
tx: number[]
|
|
197
|
+
|
|
198
|
+
/** The output index within that transaction. */
|
|
199
|
+
outputIndex: number
|
|
200
|
+
|
|
201
|
+
/** The exact script hex for the locking script. */
|
|
202
|
+
outputScript: string
|
|
203
|
+
|
|
204
|
+
/** The amount of satoshis assigned to the permission output (often 1). */
|
|
205
|
+
satoshis: number
|
|
206
|
+
|
|
207
|
+
/** The originator domain or FQDN that is allowed to use this permission. */
|
|
208
|
+
originator: string
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The raw, unnormalized originator string captured at the time the permission
|
|
212
|
+
* token was created. This is preserved so we can continue to recognize legacy
|
|
213
|
+
* permissions that were stored with different casing or explicit default ports.
|
|
214
|
+
*/
|
|
215
|
+
rawOriginator?: string
|
|
216
|
+
|
|
217
|
+
/** The expiration time for this token in UNIX epoch seconds. (0 or omitted for spending authorizations, which are indefinite) */
|
|
218
|
+
expiry: number
|
|
219
|
+
|
|
220
|
+
/** Whether this token grants privileged usage (for protocol or certificate). */
|
|
221
|
+
privileged?: boolean
|
|
222
|
+
|
|
223
|
+
/** The protocol name, if this is a DPACP token. */
|
|
224
|
+
protocol?: string
|
|
225
|
+
|
|
226
|
+
/** The security level (0,1,2) for DPACP. */
|
|
227
|
+
securityLevel?: 0 | 1 | 2
|
|
228
|
+
|
|
229
|
+
/** The counterparty, for DPACP. */
|
|
230
|
+
counterparty?: string
|
|
231
|
+
|
|
232
|
+
/** The name of a basket, if this is a DBAP token. */
|
|
233
|
+
basketName?: string
|
|
234
|
+
|
|
235
|
+
/** The certificate type, if this is a DCAP token. */
|
|
236
|
+
certType?: string
|
|
237
|
+
|
|
238
|
+
/** The certificate fields that this token covers, if DCAP token. */
|
|
239
|
+
certFields?: string[]
|
|
240
|
+
|
|
241
|
+
/** The "verifier" public key string, if DCAP. */
|
|
242
|
+
verifier?: string
|
|
243
|
+
|
|
244
|
+
/** For DSAP, the maximum authorized spending for the month. */
|
|
245
|
+
authorizedAmount?: number
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* A map from each permission type to a special "admin basket" name used for storing
|
|
250
|
+
* the tokens. The tokens themselves are unspent transaction outputs (UTXOs) with a
|
|
251
|
+
* specialized PushDrop script that references the originator, expiry, etc.
|
|
252
|
+
*/
|
|
253
|
+
const BASKET_MAP = {
|
|
254
|
+
protocol: 'admin protocol-permission',
|
|
255
|
+
basket: 'admin basket-access',
|
|
256
|
+
certificate: 'admin certificate-access',
|
|
257
|
+
spending: 'admin spending-authorization'
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The set of callbacks that external code can bind to, e.g. to display UI prompts or logs
|
|
262
|
+
* when a permission is requested.
|
|
263
|
+
*/
|
|
264
|
+
export interface WalletPermissionsManagerCallbacks {
|
|
265
|
+
onProtocolPermissionRequested?: PermissionEventHandler[]
|
|
266
|
+
onBasketAccessRequested?: PermissionEventHandler[]
|
|
267
|
+
onCertificateAccessRequested?: PermissionEventHandler[]
|
|
268
|
+
onSpendingAuthorizationRequested?: PermissionEventHandler[]
|
|
269
|
+
onGroupedPermissionRequested?: GroupedPermissionEventHandler[]
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Configuration object for the WalletPermissionsManager. If a given option is `false`,
|
|
274
|
+
* the manager will skip or alter certain permission checks or behaviors.
|
|
275
|
+
*
|
|
276
|
+
* By default, all of these are `true` unless specified otherwise. This is the most secure configuration.
|
|
277
|
+
*/
|
|
278
|
+
export interface PermissionsManagerConfig {
|
|
279
|
+
/**
|
|
280
|
+
* A map of P-basket/protocol permission scheme modules.
|
|
281
|
+
*
|
|
282
|
+
* Keys are scheme IDs (e.g., "btms"), values are PermissionsModule instances.
|
|
283
|
+
*
|
|
284
|
+
* Each module handles basket/protocol names of the form: `p <schemeID> <rest...>`
|
|
285
|
+
*
|
|
286
|
+
* The WalletPermissionManager detects P-prefix baskets/protocols and delegates
|
|
287
|
+
* request/response transformation to the corresponding module.
|
|
288
|
+
*
|
|
289
|
+
* If no module exists for a given schemeID, the wallet will reject access.
|
|
290
|
+
*/
|
|
291
|
+
permissionModules?: Record<string, PermissionsModule>
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* For `createSignature` and `verifySignature`,
|
|
295
|
+
* require a "protocol usage" permission check?
|
|
296
|
+
*/
|
|
297
|
+
seekProtocolPermissionsForSigning?: boolean
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* For methods that perform encryption (encrypt/decrypt), require
|
|
301
|
+
* a "protocol usage" permission check?
|
|
302
|
+
*/
|
|
303
|
+
seekProtocolPermissionsForEncrypting?: boolean
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* For methods that perform HMAC creation or verification (createHmac, verifyHmac),
|
|
307
|
+
* require a "protocol usage" permission check?
|
|
308
|
+
*/
|
|
309
|
+
seekProtocolPermissionsForHMAC?: boolean
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* For revealing counterparty-level or specific key linkage revelation information,
|
|
313
|
+
* should we require permission?
|
|
314
|
+
*/
|
|
315
|
+
seekPermissionsForKeyLinkageRevelation?: boolean
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* For revealing any user public key (getPublicKey) **other** than the identity key,
|
|
319
|
+
* should we require permission?
|
|
320
|
+
*/
|
|
321
|
+
seekPermissionsForPublicKeyRevelation?: boolean
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* If getPublicKey is requested with `identityKey=true`, do we require permission?
|
|
325
|
+
*/
|
|
326
|
+
seekPermissionsForIdentityKeyRevelation?: boolean
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* If discoverByIdentityKey / discoverByAttributes are called, do we require permission
|
|
330
|
+
* for "identity resolution" usage?
|
|
331
|
+
*/
|
|
332
|
+
seekPermissionsForIdentityResolution?: boolean
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* When we do internalizeAction with `basket insertion`, or include outputs in baskets
|
|
336
|
+
* with `createAction, do we ask for basket permission?
|
|
337
|
+
*/
|
|
338
|
+
seekBasketInsertionPermissions?: boolean
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* When relinquishOutput is called, do we ask for basket permission?
|
|
342
|
+
*/
|
|
343
|
+
seekBasketRemovalPermissions?: boolean
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* When listOutputs is called, do we ask for basket permission?
|
|
347
|
+
*/
|
|
348
|
+
seekBasketListingPermissions?: boolean
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* When createAction is called with labels, do we ask for "label usage" permission?
|
|
352
|
+
*/
|
|
353
|
+
seekPermissionWhenApplyingActionLabels?: boolean
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* When listActions is called with labels, do we ask for "label usage" permission?
|
|
357
|
+
*/
|
|
358
|
+
seekPermissionWhenListingActionsByLabel?: boolean
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* If proving a certificate (proveCertificate) or revealing certificate fields,
|
|
362
|
+
* do we require a "certificate access" permission?
|
|
363
|
+
*/
|
|
364
|
+
seekCertificateDisclosurePermissions?: boolean
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* If acquiring a certificate (acquireCertificate), do we require a permission check?
|
|
368
|
+
*/
|
|
369
|
+
seekCertificateAcquisitionPermissions?: boolean
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* If relinquishing a certificate (relinquishCertificate), do we require a permission check?
|
|
373
|
+
*/
|
|
374
|
+
seekCertificateRelinquishmentPermissions?: boolean
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* If listing a user's certificates (listCertificates), do we require a permission check?
|
|
378
|
+
*/
|
|
379
|
+
seekCertificateListingPermissions?: boolean
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Should transaction descriptions, input descriptions, and output descriptions be encrypted
|
|
383
|
+
* when before they are passed to the underlying wallet, and transparently decrypted when retrieved?
|
|
384
|
+
*/
|
|
385
|
+
encryptWalletMetadata?: boolean
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* If the originator tries to spend wallet funds (netSpent > 0 in createAction),
|
|
389
|
+
* do we seek spending authorization?
|
|
390
|
+
*/
|
|
391
|
+
seekSpendingPermissions?: boolean
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* If true, triggers a grouped permission request flow based on the originator's `manifest.json`.
|
|
395
|
+
*/
|
|
396
|
+
seekGroupedPermission?: boolean
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* If false, permissions are checked without regard for whether we are in
|
|
400
|
+
* privileged mode. Privileged status is ignored with respect to whether
|
|
401
|
+
* permissions are granted. Internally, they are always sought and checked
|
|
402
|
+
* with privileged=false, regardless of the actual value.
|
|
403
|
+
*/
|
|
404
|
+
differentiatePrivilegedOperations?: boolean
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* @class WalletPermissionsManager
|
|
409
|
+
*
|
|
410
|
+
* Wraps an underlying BRC-100 `Wallet` implementation with permissions management capabilities.
|
|
411
|
+
* The manager intercepts calls from external applications (identified by originators), checks if the request is allowed,
|
|
412
|
+
* and if not, orchestrates user permission flows. It creates or renews on-chain tokens in special
|
|
413
|
+
* admin baskets to track these authorizations. Finally, it proxies the actual call to the underlying wallet.
|
|
414
|
+
*
|
|
415
|
+
* ### Key Responsibilities:
|
|
416
|
+
* - **Permission Checking**: Before standard wallet operations (e.g. `encrypt`),
|
|
417
|
+
* the manager checks if a valid permission token exists. If not, it attempts to request permission from the user.
|
|
418
|
+
* - **On-Chain Tokens**: When permission is granted, the manager stores it as an unspent "PushDrop" output.
|
|
419
|
+
* This can be spent later to revoke or renew the permission.
|
|
420
|
+
* - **Callbacks**: The manager triggers user-defined callbacks on permission requests (to show a UI prompt),
|
|
421
|
+
* on grants/denials, and on internal processes.
|
|
422
|
+
*
|
|
423
|
+
* ### Implementation Notes:
|
|
424
|
+
* - The manager follows the BRC-100 `createAction` + `signAction` pattern for building or spending these tokens.
|
|
425
|
+
* - Token revocation or renewal uses standard BRC-100 flows: we build a transaction that consumes
|
|
426
|
+
* the old token UTXO and outputs a new one (or none, if fully revoked).
|
|
427
|
+
*/
|
|
428
|
+
export class WalletPermissionsManager implements WalletInterface {
|
|
429
|
+
/** A reference to the BRC-100 wallet instance. */
|
|
430
|
+
private underlying: WalletInterface
|
|
431
|
+
|
|
432
|
+
/** The "admin" domain or FQDN that is implicitly allowed to do everything. */
|
|
433
|
+
private adminOriginator: string
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Event callbacks that external code can subscribe to, e.g. to show a UI prompt
|
|
437
|
+
* or log events. Each event can have multiple handlers.
|
|
438
|
+
*/
|
|
439
|
+
private callbacks: WalletPermissionsManagerCallbacks = {
|
|
440
|
+
onProtocolPermissionRequested: [],
|
|
441
|
+
onBasketAccessRequested: [],
|
|
442
|
+
onCertificateAccessRequested: [],
|
|
443
|
+
onSpendingAuthorizationRequested: [],
|
|
444
|
+
onGroupedPermissionRequested: []
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* We queue parallel requests for the same resource so that only one
|
|
449
|
+
* user prompt is created for a single resource. If multiple calls come
|
|
450
|
+
* in at once for the same "protocol:domain:privileged:counterparty" etc.,
|
|
451
|
+
* they get merged.
|
|
452
|
+
*
|
|
453
|
+
* The key is a string derived from the operation; the value is an object with a reference to the
|
|
454
|
+
* associated request and an array of pending promise resolve/reject pairs, one for each active
|
|
455
|
+
* operation that's waiting on the particular resource described by the key.
|
|
456
|
+
*/
|
|
457
|
+
private activeRequests: Map<
|
|
458
|
+
string,
|
|
459
|
+
{
|
|
460
|
+
request: PermissionRequest | { originator: string; permissions: GroupedPermissions }
|
|
461
|
+
pending: Array<{
|
|
462
|
+
resolve: (val: any) => void
|
|
463
|
+
reject: (err: any) => void
|
|
464
|
+
}>
|
|
465
|
+
}
|
|
466
|
+
> = new Map()
|
|
467
|
+
|
|
468
|
+
/** Cache recently confirmed permissions to avoid repeated lookups. */
|
|
469
|
+
private permissionCache: Map<string, { expiry: number; cachedAt: number }> = new Map()
|
|
470
|
+
private recentGrants: Map<string, number> = new Map()
|
|
471
|
+
|
|
472
|
+
/** How long a cached permission remains valid (5 minutes). */
|
|
473
|
+
private static readonly CACHE_TTL_MS = 5 * 60 * 1000
|
|
474
|
+
/** Window during which freshly granted permissions are auto-allowed (except spending). */
|
|
475
|
+
private static readonly RECENT_GRANT_COVER_MS = 15 * 1000
|
|
476
|
+
|
|
477
|
+
/** Default ports used when normalizing originator values. */
|
|
478
|
+
private static readonly DEFAULT_PORTS: Record<string, string> = {
|
|
479
|
+
'http:': '80',
|
|
480
|
+
'https:': '443'
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Configuration that determines whether to skip or apply various checks and encryption.
|
|
485
|
+
*/
|
|
486
|
+
private config: PermissionsManagerConfig
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Constructs a new Permissions Manager instance.
|
|
490
|
+
*
|
|
491
|
+
* @param underlyingWallet The underlying BRC-100 wallet, where requests are forwarded after permission is granted
|
|
492
|
+
* @param adminOriginator The domain or FQDN that is automatically allowed everything
|
|
493
|
+
* @param config A set of boolean flags controlling how strictly permissions are enforced
|
|
494
|
+
*/
|
|
495
|
+
constructor(underlyingWallet: WalletInterface, adminOriginator: string, config: PermissionsManagerConfig = {}) {
|
|
496
|
+
this.underlying = underlyingWallet
|
|
497
|
+
this.adminOriginator = this.normalizeOriginator(adminOriginator) || adminOriginator
|
|
498
|
+
|
|
499
|
+
// Default all config options to true unless specified
|
|
500
|
+
this.config = {
|
|
501
|
+
seekProtocolPermissionsForSigning: true,
|
|
502
|
+
seekProtocolPermissionsForEncrypting: true,
|
|
503
|
+
seekProtocolPermissionsForHMAC: true,
|
|
504
|
+
seekPermissionsForKeyLinkageRevelation: true,
|
|
505
|
+
seekPermissionsForPublicKeyRevelation: true,
|
|
506
|
+
seekPermissionsForIdentityKeyRevelation: true,
|
|
507
|
+
seekPermissionsForIdentityResolution: true,
|
|
508
|
+
seekBasketInsertionPermissions: true,
|
|
509
|
+
seekBasketRemovalPermissions: true,
|
|
510
|
+
seekBasketListingPermissions: true,
|
|
511
|
+
seekPermissionWhenApplyingActionLabels: true,
|
|
512
|
+
seekPermissionWhenListingActionsByLabel: true,
|
|
513
|
+
seekCertificateDisclosurePermissions: true,
|
|
514
|
+
seekCertificateAcquisitionPermissions: true,
|
|
515
|
+
seekCertificateRelinquishmentPermissions: true,
|
|
516
|
+
seekCertificateListingPermissions: true,
|
|
517
|
+
encryptWalletMetadata: true,
|
|
518
|
+
seekSpendingPermissions: true,
|
|
519
|
+
seekGroupedPermission: true,
|
|
520
|
+
differentiatePrivilegedOperations: true,
|
|
521
|
+
...config // override with user-specified config
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* ---------------------------------------------------------------------
|
|
526
|
+
* HELPER METHODS FOR P-MODULE DELEGATION
|
|
527
|
+
* --------------------------------------------------------------------- */
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Delegates a wallet method call to a P-module if the basket or protocol name uses a P-scheme.
|
|
531
|
+
* Handles the full request/response transformation flow.
|
|
532
|
+
*
|
|
533
|
+
* @param basketOrProtocolName - The basket or protocol name to check for p-module delegation
|
|
534
|
+
* @param method - The wallet method name being called
|
|
535
|
+
* @param args - The original args passed to the method
|
|
536
|
+
* @param originator - The originator of the request
|
|
537
|
+
* @param underlyingCall - Callback that executes the underlying wallet method with transformed args
|
|
538
|
+
* @returns The transformed response, or null if not a P-basket/protocol (caller should continue normal flow)
|
|
539
|
+
*/
|
|
540
|
+
private async delegateToPModuleIfNeeded<T>(
|
|
541
|
+
basketOrProtocolName: string,
|
|
542
|
+
method: string,
|
|
543
|
+
args: object,
|
|
544
|
+
originator: string,
|
|
545
|
+
underlyingCall: (transformedArgs: object, originator: string) => Promise<T>
|
|
546
|
+
): Promise<T | null> {
|
|
547
|
+
// Check if this is a P-protocol/basket
|
|
548
|
+
if (!basketOrProtocolName.startsWith('p ')) {
|
|
549
|
+
return null // If not, caller should continue normal flow
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const schemeID = basketOrProtocolName.split(' ')[1]
|
|
553
|
+
const module = this.config.permissionModules?.[schemeID]
|
|
554
|
+
|
|
555
|
+
if (!module) {
|
|
556
|
+
throw new Error(`Unsupported P-module scheme: p ${schemeID}`)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Transform request with module
|
|
560
|
+
const transformedReq = await module.onRequest({
|
|
561
|
+
method,
|
|
562
|
+
args,
|
|
563
|
+
originator
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
// Call underlying method with transformed request
|
|
567
|
+
const results = await underlyingCall(transformedReq.args, originator)
|
|
568
|
+
|
|
569
|
+
// Transform response with module
|
|
570
|
+
return await module.onResponse(results, {
|
|
571
|
+
method,
|
|
572
|
+
originator
|
|
573
|
+
})
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Decrypts custom instructions in listOutputs results if encryption is configured.
|
|
578
|
+
*/
|
|
579
|
+
private async decryptListOutputsMetadata(results: ListOutputsResult): Promise<ListOutputsResult> {
|
|
580
|
+
if (results.outputs) {
|
|
581
|
+
for (let i = 0; i < results.outputs.length; i++) {
|
|
582
|
+
if (results.outputs[i].customInstructions) {
|
|
583
|
+
results.outputs[i].customInstructions = await this.maybeDecryptMetadata(
|
|
584
|
+
results.outputs[i].customInstructions!
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return results
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/* ---------------------------------------------------------------------
|
|
593
|
+
* 1) PUBLIC API FOR REGISTERING CALLBACKS (UI PROMPTS, LOGGING, ETC.)
|
|
594
|
+
* --------------------------------------------------------------------- */
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Binds a callback function to a named event, such as `onProtocolPermissionRequested`.
|
|
598
|
+
*
|
|
599
|
+
* @param eventName The name of the event to listen to
|
|
600
|
+
* @param handler A function that handles the event
|
|
601
|
+
* @returns A numeric ID you can use to unbind later
|
|
602
|
+
*/
|
|
603
|
+
public bindCallback(
|
|
604
|
+
eventName: keyof WalletPermissionsManagerCallbacks,
|
|
605
|
+
handler: PermissionEventHandler | GroupedPermissionEventHandler
|
|
606
|
+
): number {
|
|
607
|
+
const arr = this.callbacks[eventName]! as any[]
|
|
608
|
+
arr.push(handler)
|
|
609
|
+
return arr.length - 1
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Unbinds a previously registered callback by either its numeric ID (returned by `bindCallback`)
|
|
614
|
+
* or by exact function reference.
|
|
615
|
+
*
|
|
616
|
+
* @param eventName The event name, e.g. "onProtocolPermissionRequested"
|
|
617
|
+
* @param reference Either the numeric ID or the function reference
|
|
618
|
+
* @returns True if successfully unbound, false otherwise
|
|
619
|
+
*/
|
|
620
|
+
public unbindCallback(eventName: keyof WalletPermissionsManagerCallbacks, reference: number | Function): boolean {
|
|
621
|
+
if (!this.callbacks[eventName]) return false
|
|
622
|
+
const arr = this.callbacks[eventName] as any[]
|
|
623
|
+
if (typeof reference === 'number') {
|
|
624
|
+
if (arr[reference]) {
|
|
625
|
+
arr[reference] = null
|
|
626
|
+
return true
|
|
627
|
+
}
|
|
628
|
+
return false
|
|
629
|
+
} else {
|
|
630
|
+
const index = arr.indexOf(reference)
|
|
631
|
+
if (index !== -1) {
|
|
632
|
+
arr[index] = null
|
|
633
|
+
return true
|
|
634
|
+
}
|
|
635
|
+
return false
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Internally triggers a named event, calling all subscribed listeners.
|
|
641
|
+
* Each callback is awaited in turn (though errors are swallowed so that
|
|
642
|
+
* one failing callback doesn't prevent the others).
|
|
643
|
+
*
|
|
644
|
+
* @param eventName The event name
|
|
645
|
+
* @param param The parameter object passed to all listeners
|
|
646
|
+
*/
|
|
647
|
+
private async callEvent(eventName: keyof WalletPermissionsManagerCallbacks, param: any): Promise<void> {
|
|
648
|
+
const arr = this.callbacks[eventName] || []
|
|
649
|
+
for (const cb of arr) {
|
|
650
|
+
if (typeof cb === 'function') {
|
|
651
|
+
try {
|
|
652
|
+
await cb(param)
|
|
653
|
+
} catch (e) {
|
|
654
|
+
// Intentionally swallow errors from user-provided callbacks
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/* ---------------------------------------------------------------------
|
|
661
|
+
* 2) PERMISSION (GRANT / DENY) METHODS
|
|
662
|
+
* --------------------------------------------------------------------- */
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Grants a previously requested permission.
|
|
666
|
+
* This method:
|
|
667
|
+
* 1) Resolves all pending promise calls waiting on this request
|
|
668
|
+
* 2) Optionally creates or renews an on-chain PushDrop token (unless `ephemeral===true`)
|
|
669
|
+
*
|
|
670
|
+
* @param params requestID to identify which request is granted, plus optional expiry
|
|
671
|
+
* or `ephemeral` usage, etc.
|
|
672
|
+
*/
|
|
673
|
+
public async grantPermission(params: {
|
|
674
|
+
requestID: string
|
|
675
|
+
expiry?: number
|
|
676
|
+
ephemeral?: boolean
|
|
677
|
+
amount?: number
|
|
678
|
+
}): Promise<void> {
|
|
679
|
+
// 1) Identify the matching queued requests in `activeRequests`
|
|
680
|
+
const matching = this.activeRequests.get(params.requestID)
|
|
681
|
+
if (!matching) {
|
|
682
|
+
throw new Error('Request ID not found.')
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// 2) Mark all matching requests as resolved, deleting the entry
|
|
686
|
+
for (const x of matching.pending) {
|
|
687
|
+
x.resolve(true)
|
|
688
|
+
}
|
|
689
|
+
this.activeRequests.delete(params.requestID)
|
|
690
|
+
|
|
691
|
+
// 3) If `ephemeral !== true`, we create or renew an on-chain token
|
|
692
|
+
if (!params.ephemeral) {
|
|
693
|
+
const request = matching.request as PermissionRequest
|
|
694
|
+
if (!request.renewal) {
|
|
695
|
+
// brand-new permission token
|
|
696
|
+
await this.createPermissionOnChain(
|
|
697
|
+
request,
|
|
698
|
+
params.expiry || 0, // default: never expires
|
|
699
|
+
params.amount
|
|
700
|
+
)
|
|
701
|
+
} else {
|
|
702
|
+
// renewal => spend the old token, produce a new one
|
|
703
|
+
await this.renewPermissionOnChain(
|
|
704
|
+
request.previousToken!,
|
|
705
|
+
request,
|
|
706
|
+
params.expiry || 0, // default: never expires
|
|
707
|
+
params.amount
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Only cache non-ephemeral permissions
|
|
713
|
+
// Ephemeral permissions should not be cached as they are one-time authorizations
|
|
714
|
+
if (!params.ephemeral) {
|
|
715
|
+
const expiry = params.expiry || 0 // default: never expires
|
|
716
|
+
const key = this.buildRequestKey(matching.request as PermissionRequest)
|
|
717
|
+
this.cachePermission(key, expiry)
|
|
718
|
+
this.markRecentGrant(matching.request as PermissionRequest)
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Denies a previously requested permission.
|
|
724
|
+
* This method rejects all pending promise calls waiting on that request
|
|
725
|
+
*
|
|
726
|
+
* @param requestID requestID identifying which request to deny
|
|
727
|
+
*/
|
|
728
|
+
public async denyPermission(requestID: string): Promise<void> {
|
|
729
|
+
// 1) Identify the matching requests
|
|
730
|
+
const matching = this.activeRequests.get(requestID)
|
|
731
|
+
if (!matching) {
|
|
732
|
+
throw new Error('Request ID not found.')
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// 2) Reject all matching requests, deleting the entry
|
|
736
|
+
for (const x of matching.pending) {
|
|
737
|
+
x.reject(new Error('Permission denied.'))
|
|
738
|
+
}
|
|
739
|
+
this.activeRequests.delete(requestID)
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Grants a previously requested grouped permission.
|
|
744
|
+
* @param params.requestID The ID of the request being granted.
|
|
745
|
+
* @param params.granted A subset of the originally requested permissions that the user has granted.
|
|
746
|
+
* @param params.expiry An optional expiry time (in seconds) for the new permission tokens.
|
|
747
|
+
*/
|
|
748
|
+
public async grantGroupedPermission(params: {
|
|
749
|
+
requestID: string
|
|
750
|
+
granted: Partial<GroupedPermissions>
|
|
751
|
+
expiry?: number
|
|
752
|
+
}): Promise<void> {
|
|
753
|
+
const matching = this.activeRequests.get(params.requestID)
|
|
754
|
+
if (!matching) {
|
|
755
|
+
throw new Error('Request ID not found.')
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const originalRequest = matching.request as {
|
|
759
|
+
originator: string
|
|
760
|
+
permissions: GroupedPermissions
|
|
761
|
+
displayOriginator?: string
|
|
762
|
+
}
|
|
763
|
+
const { originator, permissions: requestedPermissions, displayOriginator } = originalRequest
|
|
764
|
+
const originLookupValues = this.buildOriginatorLookupValues(displayOriginator, originator)
|
|
765
|
+
|
|
766
|
+
// --- Validation: Ensure granted permissions are a subset of what was requested ---
|
|
767
|
+
if (
|
|
768
|
+
params.granted.spendingAuthorization &&
|
|
769
|
+
!deepEqual(params.granted.spendingAuthorization, requestedPermissions.spendingAuthorization)
|
|
770
|
+
) {
|
|
771
|
+
throw new Error('Granted spending authorization does not match the original request.')
|
|
772
|
+
}
|
|
773
|
+
if (
|
|
774
|
+
params.granted.protocolPermissions?.some(
|
|
775
|
+
g => !requestedPermissions.protocolPermissions?.find(r => deepEqual(r, g))
|
|
776
|
+
)
|
|
777
|
+
) {
|
|
778
|
+
throw new Error('Granted protocol permissions are not a subset of the original request.')
|
|
779
|
+
}
|
|
780
|
+
if (params.granted.basketAccess?.some(g => !requestedPermissions.basketAccess?.find(r => deepEqual(r, g)))) {
|
|
781
|
+
throw new Error('Granted basket access permissions are not a subset of the original request.')
|
|
782
|
+
}
|
|
783
|
+
if (
|
|
784
|
+
params.granted.certificateAccess?.some(g => !requestedPermissions.certificateAccess?.find(r => deepEqual(r, g)))
|
|
785
|
+
) {
|
|
786
|
+
throw new Error('Granted certificate access permissions are not a subset of the original request.')
|
|
787
|
+
}
|
|
788
|
+
// --- End Validation ---
|
|
789
|
+
|
|
790
|
+
const expiry = params.expiry || 0 // default: never expires
|
|
791
|
+
|
|
792
|
+
if (params.granted.spendingAuthorization) {
|
|
793
|
+
await this.createPermissionOnChain(
|
|
794
|
+
{
|
|
795
|
+
type: 'spending',
|
|
796
|
+
originator,
|
|
797
|
+
spending: { satoshis: params.granted.spendingAuthorization.amount },
|
|
798
|
+
reason: params.granted.spendingAuthorization.description
|
|
799
|
+
},
|
|
800
|
+
0, // No expiry for spending tokens
|
|
801
|
+
params.granted.spendingAuthorization.amount
|
|
802
|
+
)
|
|
803
|
+
}
|
|
804
|
+
for (const p of params.granted.protocolPermissions || []) {
|
|
805
|
+
const token = await this.findProtocolToken(
|
|
806
|
+
originator,
|
|
807
|
+
false, // No privileged protocols allowed in groups for added security.
|
|
808
|
+
p.protocolID,
|
|
809
|
+
p.counterparty || 'self',
|
|
810
|
+
true,
|
|
811
|
+
originLookupValues
|
|
812
|
+
)
|
|
813
|
+
if (token) {
|
|
814
|
+
const request: PermissionRequest = {
|
|
815
|
+
type: 'protocol',
|
|
816
|
+
originator,
|
|
817
|
+
privileged: false, // No privileged protocols allowed in groups for added security.
|
|
818
|
+
protocolID: p.protocolID,
|
|
819
|
+
counterparty: p.counterparty || 'self',
|
|
820
|
+
reason: p.description
|
|
821
|
+
}
|
|
822
|
+
await this.renewPermissionOnChain(token, request, expiry)
|
|
823
|
+
this.markRecentGrant(request)
|
|
824
|
+
} else {
|
|
825
|
+
const request: PermissionRequest = {
|
|
826
|
+
type: 'protocol',
|
|
827
|
+
originator,
|
|
828
|
+
privileged: false, // No privileged protocols allowed in groups for added security.
|
|
829
|
+
protocolID: p.protocolID,
|
|
830
|
+
counterparty: p.counterparty || 'self',
|
|
831
|
+
reason: p.description
|
|
832
|
+
}
|
|
833
|
+
await this.createPermissionOnChain(request, expiry)
|
|
834
|
+
this.markRecentGrant(request)
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
for (const b of params.granted.basketAccess || []) {
|
|
838
|
+
const request: PermissionRequest = { type: 'basket', originator, basket: b.basket, reason: b.description }
|
|
839
|
+
await this.createPermissionOnChain(request, expiry)
|
|
840
|
+
this.markRecentGrant(request)
|
|
841
|
+
}
|
|
842
|
+
for (const c of params.granted.certificateAccess || []) {
|
|
843
|
+
const request: PermissionRequest = {
|
|
844
|
+
type: 'certificate',
|
|
845
|
+
originator,
|
|
846
|
+
privileged: false, // No certificates on the privileged identity are allowed as part of groups.
|
|
847
|
+
certificate: {
|
|
848
|
+
verifier: c.verifierPublicKey,
|
|
849
|
+
certType: c.type,
|
|
850
|
+
fields: c.fields
|
|
851
|
+
},
|
|
852
|
+
reason: c.description
|
|
853
|
+
}
|
|
854
|
+
await this.createPermissionOnChain(request, expiry)
|
|
855
|
+
this.markRecentGrant(request)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Resolve all pending promises for this request
|
|
859
|
+
for (const p of matching.pending) {
|
|
860
|
+
p.resolve(true)
|
|
861
|
+
}
|
|
862
|
+
this.activeRequests.delete(params.requestID)
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Denies a previously requested grouped permission.
|
|
867
|
+
* @param requestID The ID of the request being denied.
|
|
868
|
+
*/
|
|
869
|
+
public async denyGroupedPermission(requestID: string): Promise<void> {
|
|
870
|
+
const matching = this.activeRequests.get(requestID)
|
|
871
|
+
if (!matching) {
|
|
872
|
+
throw new Error('Request ID not found.')
|
|
873
|
+
}
|
|
874
|
+
const err = new Error('The user has denied the request for permission.')
|
|
875
|
+
;(err as any).code = 'ERR_PERMISSION_DENIED'
|
|
876
|
+
for (const p of matching.pending) {
|
|
877
|
+
p.reject(err)
|
|
878
|
+
}
|
|
879
|
+
this.activeRequests.delete(requestID)
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/* ---------------------------------------------------------------------
|
|
883
|
+
* 3) THE "ENSURE" METHODS: CHECK IF PERMISSION EXISTS, OTHERWISE PROMPT
|
|
884
|
+
* --------------------------------------------------------------------- */
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Ensures the originator has protocol usage permission.
|
|
888
|
+
* If no valid (unexpired) permission token is found, triggers a permission request flow.
|
|
889
|
+
*/
|
|
890
|
+
public async ensureProtocolPermission({
|
|
891
|
+
originator,
|
|
892
|
+
privileged,
|
|
893
|
+
protocolID,
|
|
894
|
+
counterparty,
|
|
895
|
+
reason,
|
|
896
|
+
seekPermission = true,
|
|
897
|
+
usageType
|
|
898
|
+
}: {
|
|
899
|
+
originator: string
|
|
900
|
+
privileged: boolean
|
|
901
|
+
protocolID: WalletProtocol
|
|
902
|
+
counterparty: string
|
|
903
|
+
reason?: string
|
|
904
|
+
seekPermission?: boolean
|
|
905
|
+
usageType: 'signing' | 'encrypting' | 'hmac' | 'publicKey' | 'identityKey' | 'linkageRevelation' | 'generic'
|
|
906
|
+
}): Promise<boolean> {
|
|
907
|
+
const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator)
|
|
908
|
+
originator = normalizedOriginator
|
|
909
|
+
// 1) adminOriginator can do anything
|
|
910
|
+
if (this.isAdminOriginator(originator)) return true
|
|
911
|
+
|
|
912
|
+
// 2) If security level=0, we consider it "open" usage
|
|
913
|
+
const [level, protoName] = protocolID
|
|
914
|
+
if (level === 0) return true
|
|
915
|
+
|
|
916
|
+
// 3) If protocol is admin-reserved, block
|
|
917
|
+
if (this.isAdminProtocol(protocolID)) {
|
|
918
|
+
throw new Error(`Protocol “${protoName}” is admin-only.`)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Allow the configured exceptions.
|
|
922
|
+
if (usageType === 'signing' && !this.config.seekProtocolPermissionsForSigning) {
|
|
923
|
+
return true
|
|
924
|
+
}
|
|
925
|
+
if (usageType === 'encrypting' && !this.config.seekProtocolPermissionsForEncrypting) {
|
|
926
|
+
return true
|
|
927
|
+
}
|
|
928
|
+
if (usageType === 'hmac' && !this.config.seekProtocolPermissionsForHMAC) {
|
|
929
|
+
return true
|
|
930
|
+
}
|
|
931
|
+
if (usageType === 'publicKey' && !this.config.seekPermissionsForPublicKeyRevelation) {
|
|
932
|
+
return true
|
|
933
|
+
}
|
|
934
|
+
if (usageType === 'identityKey' && !this.config.seekPermissionsForIdentityKeyRevelation) {
|
|
935
|
+
return true
|
|
936
|
+
}
|
|
937
|
+
if (usageType === 'linkageRevelation' && !this.config.seekPermissionsForKeyLinkageRevelation) {
|
|
938
|
+
return true
|
|
939
|
+
}
|
|
940
|
+
if (!this.config.differentiatePrivilegedOperations) {
|
|
941
|
+
privileged = false
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const cacheKey = this.buildRequestKey({
|
|
945
|
+
type: 'protocol',
|
|
946
|
+
originator,
|
|
947
|
+
privileged,
|
|
948
|
+
protocolID,
|
|
949
|
+
counterparty
|
|
950
|
+
})
|
|
951
|
+
if (this.isPermissionCached(cacheKey)) {
|
|
952
|
+
return true
|
|
953
|
+
}
|
|
954
|
+
if (this.isRecentlyGranted(cacheKey)) {
|
|
955
|
+
return true
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// 4) Attempt to find a valid token in the internal basket
|
|
959
|
+
const token = await this.findProtocolToken(
|
|
960
|
+
originator,
|
|
961
|
+
privileged,
|
|
962
|
+
protocolID,
|
|
963
|
+
counterparty,
|
|
964
|
+
/*includeExpired=*/ true,
|
|
965
|
+
lookupValues
|
|
966
|
+
)
|
|
967
|
+
if (token) {
|
|
968
|
+
if (!this.isTokenExpired(token.expiry)) {
|
|
969
|
+
// valid and unexpired
|
|
970
|
+
this.cachePermission(cacheKey, token.expiry)
|
|
971
|
+
return true
|
|
972
|
+
} else {
|
|
973
|
+
// has a token but expired => request renewal if allowed
|
|
974
|
+
if (!seekPermission) {
|
|
975
|
+
throw new Error(`Protocol permission expired and no further user consent allowed (seekPermission=false).`)
|
|
976
|
+
}
|
|
977
|
+
return await this.requestPermissionFlow({
|
|
978
|
+
type: 'protocol',
|
|
979
|
+
originator,
|
|
980
|
+
privileged,
|
|
981
|
+
protocolID,
|
|
982
|
+
counterparty,
|
|
983
|
+
reason,
|
|
984
|
+
renewal: true,
|
|
985
|
+
previousToken: token
|
|
986
|
+
})
|
|
987
|
+
}
|
|
988
|
+
} else {
|
|
989
|
+
// No token found => request a new one if allowed
|
|
990
|
+
if (!seekPermission) {
|
|
991
|
+
throw new Error(`No protocol permission token found (seekPermission=false).`)
|
|
992
|
+
}
|
|
993
|
+
const granted = await this.requestPermissionFlow({
|
|
994
|
+
type: 'protocol',
|
|
995
|
+
originator,
|
|
996
|
+
privileged,
|
|
997
|
+
protocolID,
|
|
998
|
+
counterparty,
|
|
999
|
+
reason,
|
|
1000
|
+
renewal: false
|
|
1001
|
+
})
|
|
1002
|
+
return granted
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Ensures the originator has basket usage permission for the specified basket.
|
|
1008
|
+
* If not, triggers a permission request flow.
|
|
1009
|
+
*/
|
|
1010
|
+
public async ensureBasketAccess({
|
|
1011
|
+
originator,
|
|
1012
|
+
basket,
|
|
1013
|
+
reason,
|
|
1014
|
+
seekPermission = true,
|
|
1015
|
+
usageType
|
|
1016
|
+
}: {
|
|
1017
|
+
originator: string
|
|
1018
|
+
basket: string
|
|
1019
|
+
reason?: string
|
|
1020
|
+
seekPermission?: boolean
|
|
1021
|
+
usageType: 'insertion' | 'removal' | 'listing'
|
|
1022
|
+
}): Promise<boolean> {
|
|
1023
|
+
const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator)
|
|
1024
|
+
originator = normalizedOriginator
|
|
1025
|
+
if (this.isAdminOriginator(originator)) return true
|
|
1026
|
+
if (this.isAdminBasket(basket)) {
|
|
1027
|
+
throw new Error(`Basket “${basket}” is admin-only.`)
|
|
1028
|
+
}
|
|
1029
|
+
if (usageType === 'insertion' && !this.config.seekBasketInsertionPermissions) return true
|
|
1030
|
+
if (usageType === 'removal' && !this.config.seekBasketRemovalPermissions) return true
|
|
1031
|
+
if (usageType === 'listing' && !this.config.seekBasketListingPermissions) return true
|
|
1032
|
+
const cacheKey = this.buildRequestKey({ type: 'basket', originator, basket })
|
|
1033
|
+
if (this.isPermissionCached(cacheKey)) {
|
|
1034
|
+
return true
|
|
1035
|
+
}
|
|
1036
|
+
if (this.isRecentlyGranted(cacheKey)) {
|
|
1037
|
+
return true
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const token = await this.findBasketToken(originator, basket, true, lookupValues)
|
|
1041
|
+
if (token) {
|
|
1042
|
+
if (!this.isTokenExpired(token.expiry)) {
|
|
1043
|
+
this.cachePermission(cacheKey, token.expiry)
|
|
1044
|
+
return true
|
|
1045
|
+
} else {
|
|
1046
|
+
if (!seekPermission) {
|
|
1047
|
+
throw new Error(`Basket permission expired (seekPermission=false).`)
|
|
1048
|
+
}
|
|
1049
|
+
return await this.requestPermissionFlow({
|
|
1050
|
+
type: 'basket',
|
|
1051
|
+
originator,
|
|
1052
|
+
basket,
|
|
1053
|
+
reason,
|
|
1054
|
+
renewal: true,
|
|
1055
|
+
previousToken: token
|
|
1056
|
+
})
|
|
1057
|
+
}
|
|
1058
|
+
} else {
|
|
1059
|
+
// none
|
|
1060
|
+
if (!seekPermission) {
|
|
1061
|
+
throw new Error(`No basket permission found, and no user consent allowed (seekPermission=false).`)
|
|
1062
|
+
}
|
|
1063
|
+
const granted = await this.requestPermissionFlow({
|
|
1064
|
+
type: 'basket',
|
|
1065
|
+
originator,
|
|
1066
|
+
basket,
|
|
1067
|
+
reason,
|
|
1068
|
+
renewal: false
|
|
1069
|
+
})
|
|
1070
|
+
return granted
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Ensures the originator has a valid certificate permission.
|
|
1076
|
+
* This is relevant when revealing certificate fields in DCAP contexts.
|
|
1077
|
+
*/
|
|
1078
|
+
public async ensureCertificateAccess({
|
|
1079
|
+
originator,
|
|
1080
|
+
privileged,
|
|
1081
|
+
verifier,
|
|
1082
|
+
certType,
|
|
1083
|
+
fields,
|
|
1084
|
+
reason,
|
|
1085
|
+
seekPermission = true,
|
|
1086
|
+
usageType
|
|
1087
|
+
}: {
|
|
1088
|
+
originator: string
|
|
1089
|
+
privileged: boolean
|
|
1090
|
+
verifier: string
|
|
1091
|
+
certType: string
|
|
1092
|
+
fields: string[]
|
|
1093
|
+
reason?: string
|
|
1094
|
+
seekPermission?: boolean
|
|
1095
|
+
usageType: 'disclosure'
|
|
1096
|
+
}): Promise<boolean> {
|
|
1097
|
+
const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator)
|
|
1098
|
+
originator = normalizedOriginator
|
|
1099
|
+
if (this.isAdminOriginator(originator)) return true
|
|
1100
|
+
if (usageType === 'disclosure' && !this.config.seekCertificateDisclosurePermissions) {
|
|
1101
|
+
return true
|
|
1102
|
+
}
|
|
1103
|
+
if (!this.config.differentiatePrivilegedOperations) {
|
|
1104
|
+
privileged = false
|
|
1105
|
+
}
|
|
1106
|
+
const cacheKey = this.buildRequestKey({
|
|
1107
|
+
type: 'certificate',
|
|
1108
|
+
originator,
|
|
1109
|
+
privileged,
|
|
1110
|
+
certificate: { verifier, certType, fields }
|
|
1111
|
+
})
|
|
1112
|
+
if (this.isPermissionCached(cacheKey)) {
|
|
1113
|
+
return true
|
|
1114
|
+
}
|
|
1115
|
+
if (this.isRecentlyGranted(cacheKey)) {
|
|
1116
|
+
return true
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const token = await this.findCertificateToken(
|
|
1120
|
+
originator,
|
|
1121
|
+
privileged,
|
|
1122
|
+
verifier,
|
|
1123
|
+
certType,
|
|
1124
|
+
fields,
|
|
1125
|
+
/*includeExpired=*/ true,
|
|
1126
|
+
lookupValues
|
|
1127
|
+
)
|
|
1128
|
+
if (token) {
|
|
1129
|
+
if (!this.isTokenExpired(token.expiry)) {
|
|
1130
|
+
this.cachePermission(cacheKey, token.expiry)
|
|
1131
|
+
return true
|
|
1132
|
+
} else {
|
|
1133
|
+
if (!seekPermission) {
|
|
1134
|
+
throw new Error(`Certificate permission expired (seekPermission=false).`)
|
|
1135
|
+
}
|
|
1136
|
+
return await this.requestPermissionFlow({
|
|
1137
|
+
type: 'certificate',
|
|
1138
|
+
originator,
|
|
1139
|
+
privileged,
|
|
1140
|
+
certificate: { verifier, certType, fields },
|
|
1141
|
+
reason,
|
|
1142
|
+
renewal: true,
|
|
1143
|
+
previousToken: token
|
|
1144
|
+
})
|
|
1145
|
+
}
|
|
1146
|
+
} else {
|
|
1147
|
+
if (!seekPermission) {
|
|
1148
|
+
throw new Error(`No certificate permission found (seekPermission=false).`)
|
|
1149
|
+
}
|
|
1150
|
+
const granted = await this.requestPermissionFlow({
|
|
1151
|
+
type: 'certificate',
|
|
1152
|
+
originator,
|
|
1153
|
+
privileged,
|
|
1154
|
+
certificate: { verifier, certType, fields },
|
|
1155
|
+
reason,
|
|
1156
|
+
renewal: false
|
|
1157
|
+
})
|
|
1158
|
+
return granted
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Ensures the originator has spending authorization (DSAP) for a certain satoshi amount.
|
|
1164
|
+
* If the existing token limit is insufficient, attempts to renew. If no token, attempts to create one.
|
|
1165
|
+
*/
|
|
1166
|
+
public async ensureSpendingAuthorization({
|
|
1167
|
+
originator,
|
|
1168
|
+
satoshis,
|
|
1169
|
+
lineItems,
|
|
1170
|
+
reason,
|
|
1171
|
+
seekPermission = true
|
|
1172
|
+
}: {
|
|
1173
|
+
originator: string
|
|
1174
|
+
satoshis: number
|
|
1175
|
+
lineItems?: Array<{
|
|
1176
|
+
type: 'input' | 'output' | 'fee'
|
|
1177
|
+
description: string
|
|
1178
|
+
satoshis: number
|
|
1179
|
+
}>
|
|
1180
|
+
reason?: string
|
|
1181
|
+
seekPermission?: boolean
|
|
1182
|
+
}): Promise<boolean> {
|
|
1183
|
+
const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator)
|
|
1184
|
+
originator = normalizedOriginator
|
|
1185
|
+
if (this.isAdminOriginator(originator)) return true
|
|
1186
|
+
if (!this.config.seekSpendingPermissions) {
|
|
1187
|
+
// We skip spending permission entirely
|
|
1188
|
+
return true
|
|
1189
|
+
}
|
|
1190
|
+
const cacheKey = this.buildRequestKey({ type: 'spending', originator, spending: { satoshis } })
|
|
1191
|
+
if (this.isPermissionCached(cacheKey)) {
|
|
1192
|
+
return true
|
|
1193
|
+
}
|
|
1194
|
+
const token = await this.findSpendingToken(originator, lookupValues)
|
|
1195
|
+
if (token?.authorizedAmount) {
|
|
1196
|
+
// Check how much has been spent so far
|
|
1197
|
+
const spentSoFar = await this.querySpentSince(token)
|
|
1198
|
+
if (spentSoFar + satoshis <= token.authorizedAmount) {
|
|
1199
|
+
this.cachePermission(cacheKey, token.expiry)
|
|
1200
|
+
return true
|
|
1201
|
+
} else {
|
|
1202
|
+
// Renew if possible
|
|
1203
|
+
if (!seekPermission) {
|
|
1204
|
+
throw new Error(
|
|
1205
|
+
`Spending authorization insufficient for ${satoshis}, no user consent (seekPermission=false).`
|
|
1206
|
+
)
|
|
1207
|
+
}
|
|
1208
|
+
return await this.requestPermissionFlow({
|
|
1209
|
+
type: 'spending',
|
|
1210
|
+
originator,
|
|
1211
|
+
spending: { satoshis, lineItems },
|
|
1212
|
+
reason,
|
|
1213
|
+
renewal: true,
|
|
1214
|
+
previousToken: token
|
|
1215
|
+
})
|
|
1216
|
+
}
|
|
1217
|
+
} else {
|
|
1218
|
+
// no token
|
|
1219
|
+
if (!seekPermission) {
|
|
1220
|
+
throw new Error(`No spending authorization found, (seekPermission=false).`)
|
|
1221
|
+
}
|
|
1222
|
+
return await this.requestPermissionFlow({
|
|
1223
|
+
type: 'spending',
|
|
1224
|
+
originator,
|
|
1225
|
+
spending: { satoshis, lineItems },
|
|
1226
|
+
reason,
|
|
1227
|
+
renewal: false
|
|
1228
|
+
})
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Ensures the originator has label usage permission.
|
|
1234
|
+
* If no valid (unexpired) permission token is found, triggers a permission request flow.
|
|
1235
|
+
*/
|
|
1236
|
+
public async ensureLabelAccess({
|
|
1237
|
+
originator,
|
|
1238
|
+
label,
|
|
1239
|
+
reason,
|
|
1240
|
+
seekPermission = true,
|
|
1241
|
+
usageType
|
|
1242
|
+
}: {
|
|
1243
|
+
originator: string
|
|
1244
|
+
label: string
|
|
1245
|
+
reason?: string
|
|
1246
|
+
seekPermission?: boolean
|
|
1247
|
+
usageType: 'apply' | 'list'
|
|
1248
|
+
}): Promise<boolean> {
|
|
1249
|
+
const { normalized: normalizedOriginator } = this.prepareOriginator(originator)
|
|
1250
|
+
originator = normalizedOriginator
|
|
1251
|
+
// 1) adminOriginator can do anything
|
|
1252
|
+
if (this.isAdminOriginator(originator)) return true
|
|
1253
|
+
|
|
1254
|
+
// 2) If label is admin-reserved, block
|
|
1255
|
+
if (this.isAdminLabel(label)) {
|
|
1256
|
+
throw new Error(`Label “${label}” is admin-only.`)
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (usageType === 'apply' && !this.config.seekPermissionWhenApplyingActionLabels) {
|
|
1260
|
+
return true
|
|
1261
|
+
}
|
|
1262
|
+
if (usageType === 'list' && !this.config.seekPermissionWhenListingActionsByLabel) {
|
|
1263
|
+
return true
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const cacheKey = this.buildRequestKey({
|
|
1267
|
+
type: 'protocol',
|
|
1268
|
+
originator,
|
|
1269
|
+
privileged: false,
|
|
1270
|
+
protocolID: [1, `action label ${label}`],
|
|
1271
|
+
counterparty: 'self'
|
|
1272
|
+
})
|
|
1273
|
+
if (this.isPermissionCached(cacheKey)) {
|
|
1274
|
+
return true
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// 3) Let ensureProtocolPermission handle the rest.
|
|
1278
|
+
return await this.ensureProtocolPermission({
|
|
1279
|
+
originator,
|
|
1280
|
+
privileged: false,
|
|
1281
|
+
protocolID: [1, `action label ${label}`],
|
|
1282
|
+
counterparty: 'self',
|
|
1283
|
+
reason,
|
|
1284
|
+
seekPermission,
|
|
1285
|
+
usageType: 'generic'
|
|
1286
|
+
})
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* A central method that triggers the permission request flow.
|
|
1291
|
+
* - It checks if there's already an active request for the same key
|
|
1292
|
+
* - If so, we wait on that existing request rather than creating a duplicative one
|
|
1293
|
+
* - Otherwise we create a new request queue, call the relevant "onXXXRequested" event,
|
|
1294
|
+
* and return a promise that resolves once permission is granted or rejects if denied.
|
|
1295
|
+
*/
|
|
1296
|
+
private async requestPermissionFlow(r: PermissionRequest): Promise<boolean> {
|
|
1297
|
+
const normalizedOriginator = this.normalizeOriginator(r.originator) || r.originator
|
|
1298
|
+
const preparedRequest: PermissionRequest = {
|
|
1299
|
+
...r,
|
|
1300
|
+
originator: normalizedOriginator,
|
|
1301
|
+
displayOriginator: r.displayOriginator ?? r.originator
|
|
1302
|
+
}
|
|
1303
|
+
const key = this.buildRequestKey(preparedRequest)
|
|
1304
|
+
|
|
1305
|
+
// If there's already a queue for the same resource, we piggyback on it
|
|
1306
|
+
const existingQueue = this.activeRequests.get(key)
|
|
1307
|
+
if (existingQueue && existingQueue.pending.length > 0) {
|
|
1308
|
+
return new Promise<boolean>((resolve, reject) => {
|
|
1309
|
+
existingQueue.pending.push({ resolve, reject })
|
|
1310
|
+
})
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Otherwise, create a new queue with a single entry
|
|
1314
|
+
// Return a promise that resolves or rejects once the user grants/denies
|
|
1315
|
+
return new Promise<boolean>(async (resolve, reject) => {
|
|
1316
|
+
this.activeRequests.set(key, {
|
|
1317
|
+
request: preparedRequest,
|
|
1318
|
+
pending: [{ resolve, reject }]
|
|
1319
|
+
})
|
|
1320
|
+
|
|
1321
|
+
// Fire the relevant onXXXRequested event (which one depends on r.type)
|
|
1322
|
+
switch (preparedRequest.type) {
|
|
1323
|
+
case 'protocol':
|
|
1324
|
+
await this.callEvent('onProtocolPermissionRequested', {
|
|
1325
|
+
...preparedRequest,
|
|
1326
|
+
requestID: key
|
|
1327
|
+
})
|
|
1328
|
+
break
|
|
1329
|
+
case 'basket':
|
|
1330
|
+
await this.callEvent('onBasketAccessRequested', {
|
|
1331
|
+
...preparedRequest,
|
|
1332
|
+
requestID: key
|
|
1333
|
+
})
|
|
1334
|
+
break
|
|
1335
|
+
case 'certificate':
|
|
1336
|
+
await this.callEvent('onCertificateAccessRequested', {
|
|
1337
|
+
...preparedRequest,
|
|
1338
|
+
requestID: key
|
|
1339
|
+
})
|
|
1340
|
+
break
|
|
1341
|
+
case 'spending':
|
|
1342
|
+
await this.callEvent('onSpendingAuthorizationRequested', {
|
|
1343
|
+
...preparedRequest,
|
|
1344
|
+
requestID: key
|
|
1345
|
+
})
|
|
1346
|
+
break
|
|
1347
|
+
}
|
|
1348
|
+
})
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/* ---------------------------------------------------------------------
|
|
1352
|
+
* 4) SEARCH / DECODE / DECRYPT ON-CHAIN TOKENS (PushDrop Scripts)
|
|
1353
|
+
* --------------------------------------------------------------------- */
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* We will use a administrative "permission token encryption" protocol to store fields
|
|
1357
|
+
* in each permission's PushDrop script. This ensures that only the user's wallet
|
|
1358
|
+
* can decrypt them. In practice, this data is not super sensitive, but we still
|
|
1359
|
+
* follow the principle of least exposure.
|
|
1360
|
+
*/
|
|
1361
|
+
private static readonly PERM_TOKEN_ENCRYPTION_PROTOCOL: [2, 'admin permission token encryption'] = [
|
|
1362
|
+
2,
|
|
1363
|
+
'admin permission token encryption'
|
|
1364
|
+
]
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Similarly, we will use a "metadata encryption" protocol to preserve the confidentiality
|
|
1368
|
+
* of transaction descriptions and input/output descriptions from lower storage layers.
|
|
1369
|
+
*/
|
|
1370
|
+
private static readonly METADATA_ENCRYPTION_PROTOCOL: [2, 'admin metadata encryption'] = [
|
|
1371
|
+
2,
|
|
1372
|
+
'admin metadata encryption'
|
|
1373
|
+
]
|
|
1374
|
+
|
|
1375
|
+
/** We always use `keyID="1"` and `counterparty="self"` for these encryption ops. */
|
|
1376
|
+
private async encryptPermissionTokenField(plaintext: string | number[]): Promise<number[]> {
|
|
1377
|
+
const data = typeof plaintext === 'string' ? Utils.toArray(plaintext, 'utf8') : plaintext
|
|
1378
|
+
const { ciphertext } = await this.underlying.encrypt(
|
|
1379
|
+
{
|
|
1380
|
+
plaintext: data,
|
|
1381
|
+
protocolID: WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
|
|
1382
|
+
keyID: '1'
|
|
1383
|
+
},
|
|
1384
|
+
this.adminOriginator
|
|
1385
|
+
)
|
|
1386
|
+
return ciphertext
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
private async decryptPermissionTokenField(ciphertext: number[]): Promise<number[]> {
|
|
1390
|
+
try {
|
|
1391
|
+
const { plaintext } = await this.underlying.decrypt(
|
|
1392
|
+
{
|
|
1393
|
+
ciphertext,
|
|
1394
|
+
protocolID: WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
|
|
1395
|
+
keyID: '1'
|
|
1396
|
+
},
|
|
1397
|
+
this.adminOriginator
|
|
1398
|
+
)
|
|
1399
|
+
return plaintext
|
|
1400
|
+
} catch (e) {
|
|
1401
|
+
return ciphertext
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* Encrypts wallet metadata if configured to do so, otherwise returns the original plaintext for storage.
|
|
1407
|
+
* @param plaintext The metadata to encrypt if configured to do so
|
|
1408
|
+
* @returns The encrypted metadata, or the original value if encryption was disabled.
|
|
1409
|
+
*/
|
|
1410
|
+
private async maybeEncryptMetadata(plaintext: string): Promise<Base64String> {
|
|
1411
|
+
if (!this.config.encryptWalletMetadata) {
|
|
1412
|
+
return plaintext
|
|
1413
|
+
}
|
|
1414
|
+
const { ciphertext } = await this.underlying.encrypt(
|
|
1415
|
+
{
|
|
1416
|
+
plaintext: Utils.toArray(plaintext, 'utf8'),
|
|
1417
|
+
protocolID: WalletPermissionsManager.METADATA_ENCRYPTION_PROTOCOL,
|
|
1418
|
+
keyID: '1'
|
|
1419
|
+
},
|
|
1420
|
+
this.adminOriginator
|
|
1421
|
+
)
|
|
1422
|
+
return Utils.toBase64(ciphertext)
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Attempts to decrypt metadata. if decryption fails, assumes the value is already plaintext and returns it.
|
|
1427
|
+
* @param ciphertext The metadata to attempt decryption for.
|
|
1428
|
+
* @returns The decrypted metadata. If decryption fails, returns the original value instead.
|
|
1429
|
+
*/
|
|
1430
|
+
private async maybeDecryptMetadata(ciphertext: Base64String): Promise<string> {
|
|
1431
|
+
try {
|
|
1432
|
+
const { plaintext } = await this.underlying.decrypt(
|
|
1433
|
+
{
|
|
1434
|
+
ciphertext: Utils.toArray(ciphertext, 'base64'),
|
|
1435
|
+
protocolID: WalletPermissionsManager.METADATA_ENCRYPTION_PROTOCOL,
|
|
1436
|
+
keyID: '1'
|
|
1437
|
+
},
|
|
1438
|
+
this.adminOriginator
|
|
1439
|
+
)
|
|
1440
|
+
return Utils.toUTF8(plaintext)
|
|
1441
|
+
} catch (e) {
|
|
1442
|
+
return ciphertext
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/** Helper to see if a token's expiry is in the past. */
|
|
1447
|
+
private isTokenExpired(expiry: number): boolean {
|
|
1448
|
+
const now = Math.floor(Date.now() / 1000)
|
|
1449
|
+
return expiry > 0 && expiry < now
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/** Looks for a DPACP permission token matching origin/domain, privileged, protocol, cpty. */
|
|
1453
|
+
private async findProtocolToken(
|
|
1454
|
+
originator: string,
|
|
1455
|
+
privileged: boolean,
|
|
1456
|
+
protocolID: WalletProtocol,
|
|
1457
|
+
counterparty: string,
|
|
1458
|
+
includeExpired: boolean,
|
|
1459
|
+
originatorLookupValues?: string[]
|
|
1460
|
+
): Promise<PermissionToken | undefined> {
|
|
1461
|
+
const [secLevel, protoName] = protocolID
|
|
1462
|
+
const originsToTry = originatorLookupValues?.length ? originatorLookupValues : [originator]
|
|
1463
|
+
|
|
1464
|
+
for (const originTag of originsToTry) {
|
|
1465
|
+
const tags = [
|
|
1466
|
+
`originator ${originTag}`,
|
|
1467
|
+
`privileged ${!!privileged}`,
|
|
1468
|
+
`protocolName ${protoName}`,
|
|
1469
|
+
`protocolSecurityLevel ${secLevel}`
|
|
1470
|
+
]
|
|
1471
|
+
if (secLevel === 2) {
|
|
1472
|
+
tags.push(`counterparty ${counterparty}`)
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const result = await this.underlying.listOutputs(
|
|
1476
|
+
{
|
|
1477
|
+
basket: BASKET_MAP.protocol,
|
|
1478
|
+
tags,
|
|
1479
|
+
tagQueryMode: 'all',
|
|
1480
|
+
include: 'entire transactions'
|
|
1481
|
+
},
|
|
1482
|
+
this.adminOriginator
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
for (const out of result.outputs) {
|
|
1486
|
+
const [txid, outputIndexStr] = out.outpoint.split('.')
|
|
1487
|
+
const tx = Transaction.fromBEEF(result.BEEF!, txid)
|
|
1488
|
+
const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
|
|
1489
|
+
if (!dec || !dec.fields || dec.fields.length < 6) continue
|
|
1490
|
+
const domainRaw = dec.fields[0]
|
|
1491
|
+
const expiryRaw = dec.fields[1]
|
|
1492
|
+
const privRaw = dec.fields[2]
|
|
1493
|
+
const secLevelRaw = dec.fields[3]
|
|
1494
|
+
const protoNameRaw = dec.fields[4]
|
|
1495
|
+
const counterpartyRaw = dec.fields[5]
|
|
1496
|
+
|
|
1497
|
+
const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
|
|
1498
|
+
const normalizedDomain = this.normalizeOriginator(domainDecoded)
|
|
1499
|
+
if (normalizedDomain !== originator) {
|
|
1500
|
+
continue
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
|
|
1504
|
+
const privDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
|
|
1505
|
+
const secLevelDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(secLevelRaw)), 10) as
|
|
1506
|
+
| 0
|
|
1507
|
+
| 1
|
|
1508
|
+
| 2
|
|
1509
|
+
const protoNameDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(protoNameRaw))
|
|
1510
|
+
const cptyDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(counterpartyRaw))
|
|
1511
|
+
|
|
1512
|
+
if (
|
|
1513
|
+
privDecoded !== !!privileged ||
|
|
1514
|
+
secLevelDecoded !== secLevel ||
|
|
1515
|
+
protoNameDecoded !== protoName ||
|
|
1516
|
+
(secLevelDecoded === 2 && cptyDecoded !== counterparty)
|
|
1517
|
+
) {
|
|
1518
|
+
continue
|
|
1519
|
+
}
|
|
1520
|
+
if (!includeExpired && this.isTokenExpired(expiryDecoded)) {
|
|
1521
|
+
continue
|
|
1522
|
+
}
|
|
1523
|
+
return {
|
|
1524
|
+
tx: tx.toBEEF(),
|
|
1525
|
+
txid: out.outpoint.split('.')[0],
|
|
1526
|
+
outputIndex: parseInt(out.outpoint.split('.')[1], 10),
|
|
1527
|
+
outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
|
|
1528
|
+
satoshis: out.satoshis,
|
|
1529
|
+
originator,
|
|
1530
|
+
rawOriginator: domainDecoded,
|
|
1531
|
+
privileged,
|
|
1532
|
+
protocol: protoName,
|
|
1533
|
+
securityLevel: secLevel,
|
|
1534
|
+
expiry: expiryDecoded,
|
|
1535
|
+
counterparty: cptyDecoded
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return undefined
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/** Finds ALL DPACP permission tokens matching origin/domain, privileged, protocol, cpty. Never filters by expiry. */
|
|
1543
|
+
private async findAllProtocolTokens(
|
|
1544
|
+
originator: string,
|
|
1545
|
+
privileged: boolean,
|
|
1546
|
+
protocolID: WalletProtocol,
|
|
1547
|
+
counterparty: string,
|
|
1548
|
+
originatorLookupValues?: string[]
|
|
1549
|
+
): Promise<PermissionToken[]> {
|
|
1550
|
+
const [secLevel, protoName] = protocolID
|
|
1551
|
+
const originsToTry = originatorLookupValues?.length ? originatorLookupValues : [originator]
|
|
1552
|
+
const matches: PermissionToken[] = []
|
|
1553
|
+
const seen = new Set<string>()
|
|
1554
|
+
|
|
1555
|
+
for (const originTag of originsToTry) {
|
|
1556
|
+
const tags = [
|
|
1557
|
+
`originator ${originTag}`,
|
|
1558
|
+
`privileged ${!!privileged}`,
|
|
1559
|
+
`protocolName ${protoName}`,
|
|
1560
|
+
`protocolSecurityLevel ${secLevel}`
|
|
1561
|
+
]
|
|
1562
|
+
if (secLevel === 2) {
|
|
1563
|
+
tags.push(`counterparty ${counterparty}`)
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
const result = await this.underlying.listOutputs(
|
|
1567
|
+
{
|
|
1568
|
+
basket: BASKET_MAP.protocol,
|
|
1569
|
+
tags,
|
|
1570
|
+
tagQueryMode: 'all',
|
|
1571
|
+
include: 'entire transactions'
|
|
1572
|
+
},
|
|
1573
|
+
this.adminOriginator
|
|
1574
|
+
)
|
|
1575
|
+
|
|
1576
|
+
for (const out of result.outputs) {
|
|
1577
|
+
if (seen.has(out.outpoint)) continue
|
|
1578
|
+
const [txid, outputIndexStr] = out.outpoint.split('.')
|
|
1579
|
+
const tx = Transaction.fromBEEF(result.BEEF!, txid)
|
|
1580
|
+
const vout = Number(outputIndexStr)
|
|
1581
|
+
const dec = PushDrop.decode(tx.outputs[vout].lockingScript)
|
|
1582
|
+
if (!dec || !dec.fields || dec.fields.length < 6) continue
|
|
1583
|
+
|
|
1584
|
+
const domainRaw = dec.fields[0]
|
|
1585
|
+
const expiryRaw = dec.fields[1]
|
|
1586
|
+
const privRaw = dec.fields[2]
|
|
1587
|
+
const secLevelRaw = dec.fields[3]
|
|
1588
|
+
const protoNameRaw = dec.fields[4]
|
|
1589
|
+
const counterpartyRaw = dec.fields[5]
|
|
1590
|
+
|
|
1591
|
+
const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
|
|
1592
|
+
const normalizedDomain = this.normalizeOriginator(domainDecoded)
|
|
1593
|
+
if (normalizedDomain !== originator) {
|
|
1594
|
+
continue
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
|
|
1598
|
+
const privDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
|
|
1599
|
+
const secLevelDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(secLevelRaw)), 10) as
|
|
1600
|
+
| 0
|
|
1601
|
+
| 1
|
|
1602
|
+
| 2
|
|
1603
|
+
const protoNameDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(protoNameRaw))
|
|
1604
|
+
const cptyDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(counterpartyRaw))
|
|
1605
|
+
|
|
1606
|
+
if (
|
|
1607
|
+
privDecoded !== !!privileged ||
|
|
1608
|
+
secLevelDecoded !== secLevel ||
|
|
1609
|
+
protoNameDecoded !== protoName ||
|
|
1610
|
+
(secLevelDecoded === 2 && cptyDecoded !== counterparty)
|
|
1611
|
+
) {
|
|
1612
|
+
continue
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
seen.add(out.outpoint)
|
|
1616
|
+
matches.push({
|
|
1617
|
+
tx: tx.toBEEF(),
|
|
1618
|
+
txid,
|
|
1619
|
+
outputIndex: vout,
|
|
1620
|
+
outputScript: tx.outputs[vout].lockingScript.toHex(),
|
|
1621
|
+
satoshis: out.satoshis,
|
|
1622
|
+
originator,
|
|
1623
|
+
rawOriginator: domainDecoded,
|
|
1624
|
+
privileged,
|
|
1625
|
+
protocol: protoName,
|
|
1626
|
+
securityLevel: secLevel,
|
|
1627
|
+
expiry: expiryDecoded,
|
|
1628
|
+
counterparty: cptyDecoded
|
|
1629
|
+
})
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
return matches
|
|
1634
|
+
}
|
|
1635
|
+
/** Looks for a DBAP token matching (originator, basket). */
|
|
1636
|
+
private async findBasketToken(
|
|
1637
|
+
originator: string,
|
|
1638
|
+
basket: string,
|
|
1639
|
+
includeExpired: boolean,
|
|
1640
|
+
originatorLookupValues?: string[]
|
|
1641
|
+
): Promise<PermissionToken | undefined> {
|
|
1642
|
+
const originsToTry = originatorLookupValues?.length ? originatorLookupValues : [originator]
|
|
1643
|
+
|
|
1644
|
+
for (const originTag of originsToTry) {
|
|
1645
|
+
const result = await this.underlying.listOutputs(
|
|
1646
|
+
{
|
|
1647
|
+
basket: BASKET_MAP.basket,
|
|
1648
|
+
tags: [`originator ${originTag}`, `basket ${basket}`],
|
|
1649
|
+
tagQueryMode: 'all',
|
|
1650
|
+
include: 'entire transactions'
|
|
1651
|
+
},
|
|
1652
|
+
this.adminOriginator
|
|
1653
|
+
)
|
|
1654
|
+
|
|
1655
|
+
for (const out of result.outputs) {
|
|
1656
|
+
const [txid, outputIndexStr] = out.outpoint.split('.')
|
|
1657
|
+
const tx = Transaction.fromBEEF(result.BEEF!, txid)
|
|
1658
|
+
const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
|
|
1659
|
+
if (!dec?.fields || dec.fields.length < 3) continue
|
|
1660
|
+
const domainRaw = dec.fields[0]
|
|
1661
|
+
const expiryRaw = dec.fields[1]
|
|
1662
|
+
const basketRaw = dec.fields[2]
|
|
1663
|
+
|
|
1664
|
+
const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
|
|
1665
|
+
const normalizedDomain = this.normalizeOriginator(domainDecoded)
|
|
1666
|
+
if (normalizedDomain !== originator) {
|
|
1667
|
+
continue
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
|
|
1671
|
+
const basketDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(basketRaw))
|
|
1672
|
+
if (basketDecoded !== basket) continue
|
|
1673
|
+
if (!includeExpired && this.isTokenExpired(expiryDecoded)) continue
|
|
1674
|
+
|
|
1675
|
+
return {
|
|
1676
|
+
tx: tx.toBEEF(),
|
|
1677
|
+
txid: out.outpoint.split('.')[0],
|
|
1678
|
+
outputIndex: parseInt(out.outpoint.split('.')[1], 10),
|
|
1679
|
+
outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
|
|
1680
|
+
satoshis: out.satoshis,
|
|
1681
|
+
originator,
|
|
1682
|
+
rawOriginator: domainDecoded,
|
|
1683
|
+
basketName: basketDecoded,
|
|
1684
|
+
expiry: expiryDecoded
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
return undefined
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/** Looks for a DCAP token matching (origin, privileged, verifier, certType, fields subset). */
|
|
1692
|
+
private async findCertificateToken(
|
|
1693
|
+
originator: string,
|
|
1694
|
+
privileged: boolean,
|
|
1695
|
+
verifier: string,
|
|
1696
|
+
certType: string,
|
|
1697
|
+
fields: string[],
|
|
1698
|
+
includeExpired: boolean,
|
|
1699
|
+
originatorLookupValues?: string[]
|
|
1700
|
+
): Promise<PermissionToken | undefined> {
|
|
1701
|
+
const originsToTry = originatorLookupValues?.length ? originatorLookupValues : [originator]
|
|
1702
|
+
|
|
1703
|
+
for (const originTag of originsToTry) {
|
|
1704
|
+
const result = await this.underlying.listOutputs(
|
|
1705
|
+
{
|
|
1706
|
+
basket: BASKET_MAP.certificate,
|
|
1707
|
+
tags: [`originator ${originTag}`, `privileged ${!!privileged}`, `type ${certType}`, `verifier ${verifier}`],
|
|
1708
|
+
tagQueryMode: 'all',
|
|
1709
|
+
include: 'entire transactions'
|
|
1710
|
+
},
|
|
1711
|
+
this.adminOriginator
|
|
1712
|
+
)
|
|
1713
|
+
|
|
1714
|
+
for (const out of result.outputs) {
|
|
1715
|
+
const [txid, outputIndexStr] = out.outpoint.split('.')
|
|
1716
|
+
const tx = Transaction.fromBEEF(result.BEEF!, txid)
|
|
1717
|
+
const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
|
|
1718
|
+
if (!dec?.fields || dec.fields.length < 6) continue
|
|
1719
|
+
const [domainRaw, expiryRaw, privRaw, typeRaw, fieldsRaw, verifierRaw] = dec.fields
|
|
1720
|
+
|
|
1721
|
+
const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
|
|
1722
|
+
const normalizedDomain = this.normalizeOriginator(domainDecoded)
|
|
1723
|
+
if (normalizedDomain !== originator) {
|
|
1724
|
+
continue
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
|
|
1728
|
+
const privDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
|
|
1729
|
+
const typeDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(typeRaw))
|
|
1730
|
+
const verifierDec = Utils.toUTF8(await this.decryptPermissionTokenField(verifierRaw))
|
|
1731
|
+
|
|
1732
|
+
const fieldsJson = await this.decryptPermissionTokenField(fieldsRaw)
|
|
1733
|
+
const allFields = JSON.parse(Utils.toUTF8(fieldsJson)) as string[]
|
|
1734
|
+
|
|
1735
|
+
if (privDecoded !== !!privileged || typeDecoded !== certType || verifierDec !== verifier) {
|
|
1736
|
+
continue
|
|
1737
|
+
}
|
|
1738
|
+
// Check if 'fields' is a subset of 'allFields'
|
|
1739
|
+
const setAll = new Set(allFields)
|
|
1740
|
+
if (fields.some(f => !setAll.has(f))) {
|
|
1741
|
+
continue
|
|
1742
|
+
}
|
|
1743
|
+
if (!includeExpired && this.isTokenExpired(expiryDecoded)) {
|
|
1744
|
+
continue
|
|
1745
|
+
}
|
|
1746
|
+
return {
|
|
1747
|
+
tx: tx.toBEEF(),
|
|
1748
|
+
txid: out.outpoint.split('.')[0],
|
|
1749
|
+
outputIndex: parseInt(out.outpoint.split('.')[1], 10),
|
|
1750
|
+
outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
|
|
1751
|
+
satoshis: out.satoshis,
|
|
1752
|
+
originator,
|
|
1753
|
+
rawOriginator: domainDecoded,
|
|
1754
|
+
privileged,
|
|
1755
|
+
verifier: verifierDec,
|
|
1756
|
+
certType: typeDecoded,
|
|
1757
|
+
certFields: allFields,
|
|
1758
|
+
expiry: expiryDecoded
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return undefined
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/** Looks for a DSAP token matching origin, returning the first one found. */
|
|
1766
|
+
private async findSpendingToken(
|
|
1767
|
+
originator: string,
|
|
1768
|
+
originatorLookupValues?: string[]
|
|
1769
|
+
): Promise<PermissionToken | undefined> {
|
|
1770
|
+
const originsToTry = originatorLookupValues?.length ? originatorLookupValues : [originator]
|
|
1771
|
+
|
|
1772
|
+
for (const originTag of originsToTry) {
|
|
1773
|
+
const result = await this.underlying.listOutputs(
|
|
1774
|
+
{
|
|
1775
|
+
basket: BASKET_MAP.spending,
|
|
1776
|
+
tags: [`originator ${originTag}`],
|
|
1777
|
+
tagQueryMode: 'all',
|
|
1778
|
+
include: 'entire transactions'
|
|
1779
|
+
},
|
|
1780
|
+
this.adminOriginator
|
|
1781
|
+
)
|
|
1782
|
+
|
|
1783
|
+
for (const out of result.outputs) {
|
|
1784
|
+
const [txid, outputIndexStr] = out.outpoint.split('.')
|
|
1785
|
+
const tx = Transaction.fromBEEF(result.BEEF!, txid)
|
|
1786
|
+
const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
|
|
1787
|
+
if (!dec?.fields || dec.fields.length < 2) continue
|
|
1788
|
+
const domainRaw = dec.fields[0]
|
|
1789
|
+
const amtRaw = dec.fields[1]
|
|
1790
|
+
|
|
1791
|
+
const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
|
|
1792
|
+
const normalizedDomain = this.normalizeOriginator(domainDecoded)
|
|
1793
|
+
if (normalizedDomain !== originator) continue
|
|
1794
|
+
const amtDecodedStr = Utils.toUTF8(await this.decryptPermissionTokenField(amtRaw))
|
|
1795
|
+
const authorizedAmount = parseInt(amtDecodedStr, 10)
|
|
1796
|
+
|
|
1797
|
+
return {
|
|
1798
|
+
tx: tx.toBEEF(),
|
|
1799
|
+
txid: out.outpoint.split('.')[0],
|
|
1800
|
+
outputIndex: parseInt(out.outpoint.split('.')[1], 10),
|
|
1801
|
+
outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
|
|
1802
|
+
satoshis: out.satoshis,
|
|
1803
|
+
originator,
|
|
1804
|
+
rawOriginator: domainDecoded,
|
|
1805
|
+
authorizedAmount,
|
|
1806
|
+
expiry: 0 // Not time-limited, monthly authorization
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
return undefined
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* Returns the current month and year in UTC as a string in the format "YYYY-MM".
|
|
1815
|
+
*
|
|
1816
|
+
* @returns {string} The current month and year in UTC.
|
|
1817
|
+
*/
|
|
1818
|
+
private getCurrentMonthYearUTC(): string {
|
|
1819
|
+
const now = new Date()
|
|
1820
|
+
const year = now.getUTCFullYear()
|
|
1821
|
+
const month = (now.getUTCMonth() + 1).toString().padStart(2, '0') // Ensure 2-digit month
|
|
1822
|
+
return `${year}-${month}`
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
/**
|
|
1826
|
+
* Returns spending for an originator in the current calendar month.
|
|
1827
|
+
*/
|
|
1828
|
+
public async querySpentSince(token: PermissionToken): Promise<number> {
|
|
1829
|
+
const labelOrigins = this.buildOriginatorLookupValues(token.rawOriginator, token.originator)
|
|
1830
|
+
let total = 0
|
|
1831
|
+
|
|
1832
|
+
for (const labelOrigin of labelOrigins) {
|
|
1833
|
+
const { actions } = await this.underlying.listActions(
|
|
1834
|
+
{
|
|
1835
|
+
labels: [`admin originator ${labelOrigin}`, `admin month ${this.getCurrentMonthYearUTC()}`],
|
|
1836
|
+
labelQueryMode: 'all'
|
|
1837
|
+
},
|
|
1838
|
+
this.adminOriginator
|
|
1839
|
+
)
|
|
1840
|
+
total += actions.reduce((a, e) => a + e.satoshis, 0)
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
return total
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/* ---------------------------------------------------------------------
|
|
1847
|
+
* 5) CREATE / RENEW / REVOKE PERMISSION TOKENS ON CHAIN
|
|
1848
|
+
* --------------------------------------------------------------------- */
|
|
1849
|
+
|
|
1850
|
+
/**
|
|
1851
|
+
* Creates a brand-new permission token as a single-output PushDrop script in the relevant admin basket.
|
|
1852
|
+
*
|
|
1853
|
+
* The main difference between each type of token is in the "fields" we store in the PushDrop script.
|
|
1854
|
+
*
|
|
1855
|
+
* @param r The permission request
|
|
1856
|
+
* @param expiry The expiry epoch time
|
|
1857
|
+
* @param amount For DSAP, the authorized spending limit
|
|
1858
|
+
*/
|
|
1859
|
+
private async createPermissionOnChain(r: PermissionRequest, expiry: number, amount?: number): Promise<void> {
|
|
1860
|
+
const normalizedOriginator = this.normalizeOriginator(r.originator) || r.originator
|
|
1861
|
+
r.originator = normalizedOriginator
|
|
1862
|
+
const basketName = BASKET_MAP[r.type]
|
|
1863
|
+
if (!basketName) return
|
|
1864
|
+
|
|
1865
|
+
// Build the array of encrypted fields for the PushDrop script
|
|
1866
|
+
const fields: number[][] = await this.buildPushdropFields(r, expiry, amount)
|
|
1867
|
+
|
|
1868
|
+
// Construct the script. We do a simple P2PK check. We ask `PushDrop.lock(...)`
|
|
1869
|
+
// to create a script with a single OP_CHECKSIG verifying ownership to redeem.
|
|
1870
|
+
const script = await new PushDrop(this.underlying).lock(
|
|
1871
|
+
fields,
|
|
1872
|
+
WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
|
|
1873
|
+
'1',
|
|
1874
|
+
'self',
|
|
1875
|
+
true,
|
|
1876
|
+
true
|
|
1877
|
+
)
|
|
1878
|
+
|
|
1879
|
+
// Create tags
|
|
1880
|
+
const tags = this.buildTagsForRequest(r)
|
|
1881
|
+
|
|
1882
|
+
// Build a transaction with exactly one output, no explicit inputs since the wallet
|
|
1883
|
+
// can internally fund it from its balance.
|
|
1884
|
+
await this.createAction(
|
|
1885
|
+
{
|
|
1886
|
+
description: `Grant ${r.type} permission`,
|
|
1887
|
+
outputs: [
|
|
1888
|
+
{
|
|
1889
|
+
lockingScript: script.toHex(),
|
|
1890
|
+
satoshis: 1,
|
|
1891
|
+
outputDescription: `${r.type} permission token`,
|
|
1892
|
+
basket: basketName,
|
|
1893
|
+
tags
|
|
1894
|
+
}
|
|
1895
|
+
],
|
|
1896
|
+
options: {
|
|
1897
|
+
acceptDelayedBroadcast: false
|
|
1898
|
+
}
|
|
1899
|
+
},
|
|
1900
|
+
this.adminOriginator
|
|
1901
|
+
)
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
private async coalescePermissionTokens(
|
|
1905
|
+
oldTokens: PermissionToken[],
|
|
1906
|
+
newScript: LockingScript,
|
|
1907
|
+
opts?: {
|
|
1908
|
+
tags?: string[]
|
|
1909
|
+
basket?: string
|
|
1910
|
+
description?: string
|
|
1911
|
+
}
|
|
1912
|
+
): Promise<string> {
|
|
1913
|
+
if (!oldTokens?.length) throw new Error('No permission tokens to coalesce')
|
|
1914
|
+
if (oldTokens.length < 2) throw new Error('Need at least 2 tokens to coalesce')
|
|
1915
|
+
// 1) Create a signable action with N inputs and a single renewed output
|
|
1916
|
+
// Merge all input token BEEFs into a single BEEF structure
|
|
1917
|
+
const inputBeef = new Beef()
|
|
1918
|
+
for (const token of oldTokens) {
|
|
1919
|
+
inputBeef.mergeBeef(Beef.fromBinary(token.tx))
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
const { signableTransaction } = await this.createAction(
|
|
1923
|
+
{
|
|
1924
|
+
description: opts?.description ?? `Coalesce ${oldTokens.length} permission tokens`,
|
|
1925
|
+
inputBEEF: inputBeef.toBinary(),
|
|
1926
|
+
inputs: oldTokens.map((t, i) => ({
|
|
1927
|
+
outpoint: `${t.txid}.${t.outputIndex}`,
|
|
1928
|
+
unlockingScriptLength: 74,
|
|
1929
|
+
inputDescription: `Consume permission token #${i + 1}`
|
|
1930
|
+
})),
|
|
1931
|
+
outputs: [
|
|
1932
|
+
{
|
|
1933
|
+
lockingScript: newScript.toHex(),
|
|
1934
|
+
satoshis: 1,
|
|
1935
|
+
outputDescription: 'Renewed permission token',
|
|
1936
|
+
...(opts?.basket ? { basket: opts.basket } : {}),
|
|
1937
|
+
...(opts?.tags ? { tags: opts.tags } : {})
|
|
1938
|
+
}
|
|
1939
|
+
],
|
|
1940
|
+
options: {
|
|
1941
|
+
acceptDelayedBroadcast: false,
|
|
1942
|
+
randomizeOutputs: false,
|
|
1943
|
+
signAndProcess: false
|
|
1944
|
+
}
|
|
1945
|
+
},
|
|
1946
|
+
this.adminOriginator
|
|
1947
|
+
)
|
|
1948
|
+
|
|
1949
|
+
if (!signableTransaction?.reference || !signableTransaction.tx) {
|
|
1950
|
+
throw new Error('Failed to create signable transaction')
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// 2) Sign each input - each token needs its own unlocker with the correct locking script
|
|
1954
|
+
const partialTx = Transaction.fromAtomicBEEF(signableTransaction.tx)
|
|
1955
|
+
const pushdrop = new PushDrop(this.underlying)
|
|
1956
|
+
|
|
1957
|
+
const spends: Record<number, { unlockingScript: string }> = {}
|
|
1958
|
+
for (let i = 0; i < oldTokens.length; i++) {
|
|
1959
|
+
const token = oldTokens[i]
|
|
1960
|
+
// Each token requires its own unlocker with the specific locking script
|
|
1961
|
+
const unlocker = pushdrop.unlock(
|
|
1962
|
+
WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
|
|
1963
|
+
'1',
|
|
1964
|
+
'self',
|
|
1965
|
+
'all',
|
|
1966
|
+
false,
|
|
1967
|
+
1,
|
|
1968
|
+
LockingScript.fromHex(token.outputScript)
|
|
1969
|
+
)
|
|
1970
|
+
const unlockingScript = await unlocker.sign(partialTx, i)
|
|
1971
|
+
spends[i] = { unlockingScript: unlockingScript.toHex() }
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// 3) Finalize the action
|
|
1975
|
+
const { txid } = await this.underlying.signAction({
|
|
1976
|
+
reference: signableTransaction.reference,
|
|
1977
|
+
spends
|
|
1978
|
+
})
|
|
1979
|
+
if (!txid) throw new Error('Failed to finalize coalescing transaction')
|
|
1980
|
+
return txid
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Renews a permission token by spending the old token as input and creating a new token output.
|
|
1984
|
+
* This invalidates the old token and replaces it with a new one.
|
|
1985
|
+
*
|
|
1986
|
+
* @param oldToken The old token to consume
|
|
1987
|
+
* @param r The permission request being renewed
|
|
1988
|
+
* @param newExpiry The new expiry epoch time
|
|
1989
|
+
* @param newAmount For DSAP, the new authorized amount
|
|
1990
|
+
*/
|
|
1991
|
+
private async renewPermissionOnChain(
|
|
1992
|
+
oldToken: PermissionToken,
|
|
1993
|
+
r: PermissionRequest,
|
|
1994
|
+
newExpiry: number,
|
|
1995
|
+
newAmount?: number
|
|
1996
|
+
): Promise<void> {
|
|
1997
|
+
r.originator = this.normalizeOriginator(r.originator) || r.originator
|
|
1998
|
+
// 1) build new fields
|
|
1999
|
+
const newFields = await this.buildPushdropFields(r, newExpiry, newAmount)
|
|
2000
|
+
|
|
2001
|
+
// 2) new script
|
|
2002
|
+
const newScript = await new PushDrop(this.underlying).lock(
|
|
2003
|
+
newFields,
|
|
2004
|
+
WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
|
|
2005
|
+
'1',
|
|
2006
|
+
'self',
|
|
2007
|
+
true,
|
|
2008
|
+
true
|
|
2009
|
+
)
|
|
2010
|
+
const tags = this.buildTagsForRequest(r)
|
|
2011
|
+
// Check if there are multiple old tokens for the same parameters (shouldn't usually happen)
|
|
2012
|
+
const oldTokens = await this.findAllProtocolTokens(
|
|
2013
|
+
oldToken.originator,
|
|
2014
|
+
oldToken.privileged!,
|
|
2015
|
+
[oldToken.securityLevel!, oldToken.protocol!],
|
|
2016
|
+
oldToken.counterparty!,
|
|
2017
|
+
this.buildOriginatorLookupValues(oldToken.rawOriginator, oldToken.originator)
|
|
2018
|
+
)
|
|
2019
|
+
|
|
2020
|
+
// If so, coalesce them into a single token first, to avoid bloat
|
|
2021
|
+
if (oldTokens.length > 1) {
|
|
2022
|
+
await this.coalescePermissionTokens(oldTokens, newScript, {
|
|
2023
|
+
tags,
|
|
2024
|
+
basket: BASKET_MAP[r.type],
|
|
2025
|
+
description: `Coalesce ${r.type} permission tokens`
|
|
2026
|
+
})
|
|
2027
|
+
} else {
|
|
2028
|
+
// Otherwise, just proceed with the single-token renewal
|
|
2029
|
+
// 3) For BRC-100, we do a "createAction" with a partial input referencing oldToken
|
|
2030
|
+
// plus a single new output. We'll hydrate the template, then signAction for the wallet to finalize.
|
|
2031
|
+
const oldOutpoint = `${oldToken.txid}.${oldToken.outputIndex}`
|
|
2032
|
+
const { signableTransaction } = await this.createAction(
|
|
2033
|
+
{
|
|
2034
|
+
description: `Renew ${r.type} permission`,
|
|
2035
|
+
inputBEEF: oldToken.tx,
|
|
2036
|
+
inputs: [
|
|
2037
|
+
{
|
|
2038
|
+
outpoint: oldOutpoint,
|
|
2039
|
+
unlockingScriptLength: 73, // length of signature
|
|
2040
|
+
inputDescription: `Consume old ${r.type} token`
|
|
2041
|
+
}
|
|
2042
|
+
],
|
|
2043
|
+
outputs: [
|
|
2044
|
+
{
|
|
2045
|
+
lockingScript: newScript.toHex(),
|
|
2046
|
+
satoshis: 1,
|
|
2047
|
+
outputDescription: `Renewed ${r.type} permission token`,
|
|
2048
|
+
basket: BASKET_MAP[r.type],
|
|
2049
|
+
tags
|
|
2050
|
+
}
|
|
2051
|
+
],
|
|
2052
|
+
options: {
|
|
2053
|
+
acceptDelayedBroadcast: false
|
|
2054
|
+
}
|
|
2055
|
+
},
|
|
2056
|
+
this.adminOriginator
|
|
2057
|
+
)
|
|
2058
|
+
const tx = Transaction.fromBEEF(signableTransaction!.tx)
|
|
2059
|
+
const unlocker = new PushDrop(this.underlying).unlock(
|
|
2060
|
+
WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
|
|
2061
|
+
'1',
|
|
2062
|
+
'self',
|
|
2063
|
+
'all',
|
|
2064
|
+
false,
|
|
2065
|
+
1,
|
|
2066
|
+
LockingScript.fromHex(oldToken.outputScript)
|
|
2067
|
+
)
|
|
2068
|
+
const unlockingScript = await unlocker.sign(tx, 0)
|
|
2069
|
+
await this.underlying.signAction({
|
|
2070
|
+
reference: signableTransaction!.reference,
|
|
2071
|
+
spends: {
|
|
2072
|
+
0: {
|
|
2073
|
+
unlockingScript: unlockingScript.toHex()
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
})
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
/**
|
|
2081
|
+
* Builds the encrypted array of fields for a PushDrop permission token
|
|
2082
|
+
* (protocol / basket / certificate / spending).
|
|
2083
|
+
*/
|
|
2084
|
+
private async buildPushdropFields(r: PermissionRequest, expiry: number, amount?: number): Promise<number[][]> {
|
|
2085
|
+
switch (r.type) {
|
|
2086
|
+
case 'protocol': {
|
|
2087
|
+
const [secLevel, protoName] = r.protocolID!
|
|
2088
|
+
return [
|
|
2089
|
+
await this.encryptPermissionTokenField(r.originator), // domain
|
|
2090
|
+
await this.encryptPermissionTokenField(String(expiry)), // expiry
|
|
2091
|
+
await this.encryptPermissionTokenField(r.privileged === true ? 'true' : 'false'),
|
|
2092
|
+
await this.encryptPermissionTokenField(String(secLevel)),
|
|
2093
|
+
await this.encryptPermissionTokenField(protoName),
|
|
2094
|
+
await this.encryptPermissionTokenField(r.counterparty!)
|
|
2095
|
+
]
|
|
2096
|
+
}
|
|
2097
|
+
case 'basket': {
|
|
2098
|
+
return [
|
|
2099
|
+
await this.encryptPermissionTokenField(r.originator),
|
|
2100
|
+
await this.encryptPermissionTokenField(String(expiry)),
|
|
2101
|
+
await this.encryptPermissionTokenField(r.basket!)
|
|
2102
|
+
]
|
|
2103
|
+
}
|
|
2104
|
+
case 'certificate': {
|
|
2105
|
+
const { certType, fields, verifier } = r.certificate!
|
|
2106
|
+
return [
|
|
2107
|
+
await this.encryptPermissionTokenField(r.originator),
|
|
2108
|
+
await this.encryptPermissionTokenField(String(expiry)),
|
|
2109
|
+
await this.encryptPermissionTokenField(r.privileged ? 'true' : 'false'),
|
|
2110
|
+
await this.encryptPermissionTokenField(certType),
|
|
2111
|
+
await this.encryptPermissionTokenField(JSON.stringify(fields)),
|
|
2112
|
+
await this.encryptPermissionTokenField(verifier)
|
|
2113
|
+
]
|
|
2114
|
+
}
|
|
2115
|
+
case 'spending': {
|
|
2116
|
+
// DSAP
|
|
2117
|
+
const authAmt = amount ?? (r.spending?.satoshis || 0)
|
|
2118
|
+
return [
|
|
2119
|
+
await this.encryptPermissionTokenField(r.originator),
|
|
2120
|
+
await this.encryptPermissionTokenField(String(authAmt))
|
|
2121
|
+
]
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
/**
|
|
2127
|
+
* Helper to build an array of tags for the new output, matching the user request's
|
|
2128
|
+
* origin, basket, privileged, protocol name, etc.
|
|
2129
|
+
*/
|
|
2130
|
+
private buildTagsForRequest(r: PermissionRequest): string[] {
|
|
2131
|
+
const tags: string[] = [`originator ${r.originator}`]
|
|
2132
|
+
switch (r.type) {
|
|
2133
|
+
case 'protocol': {
|
|
2134
|
+
tags.push(`privileged ${!!r.privileged}`)
|
|
2135
|
+
tags.push(`protocolName ${r.protocolID![1]}`)
|
|
2136
|
+
tags.push(`protocolSecurityLevel ${r.protocolID![0]}`)
|
|
2137
|
+
if (r.protocolID![0] === 2) {
|
|
2138
|
+
tags.push(`counterparty ${r.counterparty}`)
|
|
2139
|
+
}
|
|
2140
|
+
break
|
|
2141
|
+
}
|
|
2142
|
+
case 'basket': {
|
|
2143
|
+
tags.push(`basket ${r.basket}`)
|
|
2144
|
+
break
|
|
2145
|
+
}
|
|
2146
|
+
case 'certificate': {
|
|
2147
|
+
tags.push(`privileged ${!!r.privileged}`)
|
|
2148
|
+
tags.push(`type ${r.certificate!.certType}`)
|
|
2149
|
+
tags.push(`verifier ${r.certificate!.verifier}`)
|
|
2150
|
+
break
|
|
2151
|
+
}
|
|
2152
|
+
case 'spending': {
|
|
2153
|
+
// Only 'originator' is strictly required as a tag.
|
|
2154
|
+
break
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
return tags
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
/* ---------------------------------------------------------------------
|
|
2161
|
+
* 6) PUBLIC "LIST/HAS/REVOKE" METHODS
|
|
2162
|
+
* --------------------------------------------------------------------- */
|
|
2163
|
+
|
|
2164
|
+
/**
|
|
2165
|
+
* Lists all protocol permission tokens (DPACP) with optional filters.
|
|
2166
|
+
* @param originator Optional originator domain to filter by
|
|
2167
|
+
* @param privileged Optional boolean to filter by privileged status
|
|
2168
|
+
* @param protocolName Optional protocol name to filter by
|
|
2169
|
+
* @param protocolSecurityLevel Optional protocol security level to filter by
|
|
2170
|
+
* @param counterparty Optional counterparty to filter by
|
|
2171
|
+
* @returns Array of permission tokens that match the filter criteria
|
|
2172
|
+
*/
|
|
2173
|
+
public async listProtocolPermissions({
|
|
2174
|
+
originator,
|
|
2175
|
+
privileged,
|
|
2176
|
+
protocolName,
|
|
2177
|
+
protocolSecurityLevel,
|
|
2178
|
+
counterparty
|
|
2179
|
+
}: {
|
|
2180
|
+
originator?: string
|
|
2181
|
+
privileged?: boolean
|
|
2182
|
+
protocolName?: string
|
|
2183
|
+
protocolSecurityLevel?: number
|
|
2184
|
+
counterparty?: string
|
|
2185
|
+
} = {}): Promise<PermissionToken[]> {
|
|
2186
|
+
const basketName = BASKET_MAP.protocol
|
|
2187
|
+
const baseTags: string[] = []
|
|
2188
|
+
|
|
2189
|
+
if (privileged !== undefined) {
|
|
2190
|
+
baseTags.push(`privileged ${!!privileged}`)
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
if (protocolName) {
|
|
2194
|
+
baseTags.push(`protocolName ${protocolName}`)
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
if (protocolSecurityLevel !== undefined) {
|
|
2198
|
+
baseTags.push(`protocolSecurityLevel ${protocolSecurityLevel}`)
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
if (counterparty) {
|
|
2202
|
+
baseTags.push(`counterparty ${counterparty}`)
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
const originFilter = originator ? this.prepareOriginator(originator) : undefined
|
|
2206
|
+
const originVariants = originFilter ? originFilter.lookupValues : [undefined]
|
|
2207
|
+
const seen = new Set<string>()
|
|
2208
|
+
const tokens: PermissionToken[] = []
|
|
2209
|
+
|
|
2210
|
+
for (const originTag of originVariants) {
|
|
2211
|
+
const tags = [...baseTags]
|
|
2212
|
+
if (originTag) {
|
|
2213
|
+
tags.push(`originator ${originTag}`)
|
|
2214
|
+
}
|
|
2215
|
+
const result = await this.underlying.listOutputs(
|
|
2216
|
+
{
|
|
2217
|
+
basket: basketName,
|
|
2218
|
+
tags,
|
|
2219
|
+
tagQueryMode: 'all',
|
|
2220
|
+
include: 'entire transactions',
|
|
2221
|
+
limit: 100
|
|
2222
|
+
},
|
|
2223
|
+
this.adminOriginator
|
|
2224
|
+
)
|
|
2225
|
+
|
|
2226
|
+
for (const out of result.outputs) {
|
|
2227
|
+
if (seen.has(out.outpoint)) continue
|
|
2228
|
+
const [txid, outputIndexStr] = out.outpoint.split('.')
|
|
2229
|
+
const tx = Transaction.fromBEEF(result.BEEF!, txid)
|
|
2230
|
+
const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
|
|
2231
|
+
if (!dec?.fields || dec.fields.length < 6) continue
|
|
2232
|
+
const [domainRaw, expiryRaw, privRaw, secRaw, protoRaw, cptyRaw] = dec.fields
|
|
2233
|
+
|
|
2234
|
+
const domainDec = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
|
|
2235
|
+
const normalizedDomain = this.normalizeOriginator(domainDec)
|
|
2236
|
+
if (originFilter && normalizedDomain !== originFilter.normalized) {
|
|
2237
|
+
continue
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
const expiryDec = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
|
|
2241
|
+
const privDec = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
|
|
2242
|
+
const secDec = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(secRaw)), 10) as 0 | 1 | 2
|
|
2243
|
+
const protoDec = Utils.toUTF8(await this.decryptPermissionTokenField(protoRaw))
|
|
2244
|
+
const cptyDec = Utils.toUTF8(await this.decryptPermissionTokenField(cptyRaw))
|
|
2245
|
+
|
|
2246
|
+
seen.add(out.outpoint)
|
|
2247
|
+
tokens.push({
|
|
2248
|
+
tx: tx.toBEEF(),
|
|
2249
|
+
txid: out.outpoint.split('.')[0],
|
|
2250
|
+
outputIndex: parseInt(out.outpoint.split('.')[1], 10),
|
|
2251
|
+
outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
|
|
2252
|
+
satoshis: out.satoshis,
|
|
2253
|
+
originator: normalizedDomain,
|
|
2254
|
+
rawOriginator: domainDec,
|
|
2255
|
+
expiry: expiryDec,
|
|
2256
|
+
privileged: privDec,
|
|
2257
|
+
securityLevel: secDec,
|
|
2258
|
+
protocol: protoDec,
|
|
2259
|
+
counterparty: cptyDec
|
|
2260
|
+
})
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
return tokens
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
/**
|
|
2267
|
+
* Returns true if the originator already holds a valid unexpired protocol permission.
|
|
2268
|
+
* This calls `ensureProtocolPermission` with `seekPermission=false`, so it won't prompt.
|
|
2269
|
+
*/
|
|
2270
|
+
public async hasProtocolPermission(params: {
|
|
2271
|
+
originator: string
|
|
2272
|
+
privileged: boolean
|
|
2273
|
+
protocolID: WalletProtocol
|
|
2274
|
+
counterparty: string
|
|
2275
|
+
}): Promise<boolean> {
|
|
2276
|
+
try {
|
|
2277
|
+
await this.ensureProtocolPermission({
|
|
2278
|
+
...params,
|
|
2279
|
+
reason: 'hasProtocolPermission',
|
|
2280
|
+
seekPermission: false,
|
|
2281
|
+
usageType: 'generic'
|
|
2282
|
+
})
|
|
2283
|
+
return true
|
|
2284
|
+
} catch {
|
|
2285
|
+
return false
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
/**
|
|
2290
|
+
* Lists basket permission tokens (DBAP) for a given originator or basket (or for all if not specified).
|
|
2291
|
+
* @param params.originator Optional originator to filter by
|
|
2292
|
+
* @param params.basket Optional basket name to filter by
|
|
2293
|
+
* @returns Array of permission tokens that match the filter criteria
|
|
2294
|
+
*/
|
|
2295
|
+
public async listBasketAccess(params: { originator?: string; basket?: string } = {}): Promise<PermissionToken[]> {
|
|
2296
|
+
const basketName = BASKET_MAP.basket
|
|
2297
|
+
const baseTags: string[] = []
|
|
2298
|
+
|
|
2299
|
+
if (params.basket) {
|
|
2300
|
+
baseTags.push(`basket ${params.basket}`)
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
const originFilter = params.originator ? this.prepareOriginator(params.originator) : undefined
|
|
2304
|
+
const originVariants = originFilter ? originFilter.lookupValues : [undefined]
|
|
2305
|
+
const seen = new Set<string>()
|
|
2306
|
+
const tokens: PermissionToken[] = []
|
|
2307
|
+
|
|
2308
|
+
for (const originTag of originVariants) {
|
|
2309
|
+
const tags = [...baseTags]
|
|
2310
|
+
if (originTag) {
|
|
2311
|
+
tags.push(`originator ${originTag}`)
|
|
2312
|
+
}
|
|
2313
|
+
const result = await this.underlying.listOutputs(
|
|
2314
|
+
{
|
|
2315
|
+
basket: basketName,
|
|
2316
|
+
tags,
|
|
2317
|
+
tagQueryMode: 'all',
|
|
2318
|
+
include: 'entire transactions',
|
|
2319
|
+
limit: 10000
|
|
2320
|
+
},
|
|
2321
|
+
this.adminOriginator
|
|
2322
|
+
)
|
|
2323
|
+
|
|
2324
|
+
for (const out of result.outputs) {
|
|
2325
|
+
if (seen.has(out.outpoint)) continue
|
|
2326
|
+
const [txid, outputIndexStr] = out.outpoint.split('.')
|
|
2327
|
+
const tx = Transaction.fromBEEF(result.BEEF!, txid)
|
|
2328
|
+
const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
|
|
2329
|
+
if (!dec?.fields || dec.fields.length < 3) continue
|
|
2330
|
+
const [domainRaw, expiryRaw, basketRaw] = dec.fields
|
|
2331
|
+
const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
|
|
2332
|
+
const normalizedDomain = this.normalizeOriginator(domainDecoded)
|
|
2333
|
+
if (originFilter && normalizedDomain !== originFilter.normalized) {
|
|
2334
|
+
continue
|
|
2335
|
+
}
|
|
2336
|
+
const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
|
|
2337
|
+
const basketDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(basketRaw))
|
|
2338
|
+
seen.add(out.outpoint)
|
|
2339
|
+
tokens.push({
|
|
2340
|
+
tx: tx.toBEEF(),
|
|
2341
|
+
txid: out.outpoint.split('.')[0],
|
|
2342
|
+
outputIndex: parseInt(out.outpoint.split('.')[1], 10),
|
|
2343
|
+
satoshis: out.satoshis,
|
|
2344
|
+
outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
|
|
2345
|
+
originator: normalizedDomain,
|
|
2346
|
+
rawOriginator: domainDecoded,
|
|
2347
|
+
basketName: basketDecoded,
|
|
2348
|
+
expiry: expiryDecoded
|
|
2349
|
+
})
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
return tokens
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
/**
|
|
2356
|
+
* Returns `true` if the originator already holds a valid unexpired basket permission for `basket`.
|
|
2357
|
+
*/
|
|
2358
|
+
public async hasBasketAccess(params: { originator: string; basket: string }): Promise<boolean> {
|
|
2359
|
+
try {
|
|
2360
|
+
await this.ensureBasketAccess({
|
|
2361
|
+
originator: params.originator,
|
|
2362
|
+
basket: params.basket,
|
|
2363
|
+
seekPermission: false,
|
|
2364
|
+
usageType: 'insertion' // TODO: Consider a generic case for "has"
|
|
2365
|
+
})
|
|
2366
|
+
return true
|
|
2367
|
+
} catch {
|
|
2368
|
+
return false
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
/**
|
|
2373
|
+
* Lists spending authorization tokens (DSAP) for a given originator (or all).
|
|
2374
|
+
*/
|
|
2375
|
+
public async listSpendingAuthorizations(params: { originator?: string }): Promise<PermissionToken[]> {
|
|
2376
|
+
const basketName = BASKET_MAP.spending
|
|
2377
|
+
const tags: string[] = []
|
|
2378
|
+
if (params.originator) {
|
|
2379
|
+
tags.push(`originator ${params.originator}`)
|
|
2380
|
+
}
|
|
2381
|
+
const result = await this.underlying.listOutputs(
|
|
2382
|
+
{
|
|
2383
|
+
basket: basketName,
|
|
2384
|
+
tags,
|
|
2385
|
+
tagQueryMode: 'all',
|
|
2386
|
+
include: 'entire transactions',
|
|
2387
|
+
limit: 10000
|
|
2388
|
+
},
|
|
2389
|
+
this.adminOriginator
|
|
2390
|
+
)
|
|
2391
|
+
|
|
2392
|
+
const tokens: PermissionToken[] = []
|
|
2393
|
+
for (const out of result.outputs) {
|
|
2394
|
+
const [txid, outputIndexStr] = out.outpoint.split('.')
|
|
2395
|
+
const tx = Transaction.fromBEEF(result.BEEF!, txid)
|
|
2396
|
+
const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
|
|
2397
|
+
if (!dec?.fields || dec.fields.length < 2) continue
|
|
2398
|
+
const [domainRaw, amtRaw] = dec.fields
|
|
2399
|
+
const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
|
|
2400
|
+
const amtDecodedStr = Utils.toUTF8(await this.decryptPermissionTokenField(amtRaw))
|
|
2401
|
+
const authorizedAmount = parseInt(amtDecodedStr, 10)
|
|
2402
|
+
tokens.push({
|
|
2403
|
+
tx: tx.toBEEF(),
|
|
2404
|
+
txid: out.outpoint.split('.')[0],
|
|
2405
|
+
outputIndex: parseInt(out.outpoint.split('.')[1], 10),
|
|
2406
|
+
satoshis: out.satoshis,
|
|
2407
|
+
outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
|
|
2408
|
+
originator: domainDecoded,
|
|
2409
|
+
authorizedAmount,
|
|
2410
|
+
expiry: 0
|
|
2411
|
+
})
|
|
2412
|
+
}
|
|
2413
|
+
return tokens
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
/**
|
|
2417
|
+
* Returns `true` if the originator already holds a valid spending authorization token
|
|
2418
|
+
* with enough available monthly spend. We do not prompt (seekPermission=false).
|
|
2419
|
+
*/
|
|
2420
|
+
public async hasSpendingAuthorization(params: { originator: string; satoshis: number }): Promise<boolean> {
|
|
2421
|
+
try {
|
|
2422
|
+
await this.ensureSpendingAuthorization({
|
|
2423
|
+
originator: params.originator,
|
|
2424
|
+
satoshis: params.satoshis,
|
|
2425
|
+
seekPermission: false
|
|
2426
|
+
})
|
|
2427
|
+
return true
|
|
2428
|
+
} catch {
|
|
2429
|
+
return false
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* Lists certificate permission tokens (DCAP) with optional filters.
|
|
2435
|
+
* @param originator Optional originator domain to filter by
|
|
2436
|
+
* @param privileged Optional boolean to filter by privileged status
|
|
2437
|
+
* @param certType Optional certificate type to filter by
|
|
2438
|
+
* @param verifier Optional verifier to filter by
|
|
2439
|
+
* @returns Array of permission tokens that match the filter criteria
|
|
2440
|
+
*/
|
|
2441
|
+
public async listCertificateAccess(
|
|
2442
|
+
params: {
|
|
2443
|
+
originator?: string
|
|
2444
|
+
privileged?: boolean
|
|
2445
|
+
certType?: Base64String
|
|
2446
|
+
verifier?: PubKeyHex
|
|
2447
|
+
} = {}
|
|
2448
|
+
): Promise<PermissionToken[]> {
|
|
2449
|
+
const basketName = BASKET_MAP.certificate
|
|
2450
|
+
const baseTags: string[] = []
|
|
2451
|
+
|
|
2452
|
+
if (params.privileged !== undefined) {
|
|
2453
|
+
baseTags.push(`privileged ${!!params.privileged}`)
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
if (params.certType) {
|
|
2457
|
+
baseTags.push(`type ${params.certType}`)
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
if (params.verifier) {
|
|
2461
|
+
baseTags.push(`verifier ${params.verifier}`)
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
const originFilter = params.originator ? this.prepareOriginator(params.originator) : undefined
|
|
2465
|
+
const originVariants = originFilter ? originFilter.lookupValues : [undefined]
|
|
2466
|
+
const seen = new Set<string>()
|
|
2467
|
+
const tokens: PermissionToken[] = []
|
|
2468
|
+
|
|
2469
|
+
for (const originTag of originVariants) {
|
|
2470
|
+
const tags = [...baseTags]
|
|
2471
|
+
if (originTag) {
|
|
2472
|
+
tags.push(`originator ${originTag}`)
|
|
2473
|
+
}
|
|
2474
|
+
const result = await this.underlying.listOutputs(
|
|
2475
|
+
{
|
|
2476
|
+
basket: basketName,
|
|
2477
|
+
tags,
|
|
2478
|
+
tagQueryMode: 'all',
|
|
2479
|
+
include: 'entire transactions',
|
|
2480
|
+
limit: 10000
|
|
2481
|
+
},
|
|
2482
|
+
this.adminOriginator
|
|
2483
|
+
)
|
|
2484
|
+
|
|
2485
|
+
for (const out of result.outputs) {
|
|
2486
|
+
if (seen.has(out.outpoint)) continue
|
|
2487
|
+
const [txid, outputIndexStr] = out.outpoint.split('.')
|
|
2488
|
+
const tx = Transaction.fromBEEF(result.BEEF!, txid)
|
|
2489
|
+
const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
|
|
2490
|
+
if (!dec?.fields || dec.fields.length < 6) continue
|
|
2491
|
+
const [domainRaw, expiryRaw, privRaw, typeRaw, fieldsRaw, verifierRaw] = dec.fields
|
|
2492
|
+
const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
|
|
2493
|
+
const normalizedDomain = this.normalizeOriginator(domainDecoded)
|
|
2494
|
+
if (originFilter && normalizedDomain !== originFilter.normalized) {
|
|
2495
|
+
continue
|
|
2496
|
+
}
|
|
2497
|
+
const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
|
|
2498
|
+
const privDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
|
|
2499
|
+
const typeDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(typeRaw))
|
|
2500
|
+
const verifierDec = Utils.toUTF8(await this.decryptPermissionTokenField(verifierRaw))
|
|
2501
|
+
const fieldsJson = await this.decryptPermissionTokenField(fieldsRaw)
|
|
2502
|
+
const allFields = JSON.parse(Utils.toUTF8(fieldsJson)) as string[]
|
|
2503
|
+
seen.add(out.outpoint)
|
|
2504
|
+
tokens.push({
|
|
2505
|
+
tx: tx.toBEEF(),
|
|
2506
|
+
txid: out.outpoint.split('.')[0],
|
|
2507
|
+
outputIndex: parseInt(out.outpoint.split('.')[1], 10),
|
|
2508
|
+
satoshis: out.satoshis,
|
|
2509
|
+
outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
|
|
2510
|
+
originator: normalizedDomain,
|
|
2511
|
+
rawOriginator: domainDecoded,
|
|
2512
|
+
privileged: privDecoded,
|
|
2513
|
+
certType: typeDecoded,
|
|
2514
|
+
certFields: allFields,
|
|
2515
|
+
verifier: verifierDec,
|
|
2516
|
+
expiry: expiryDecoded
|
|
2517
|
+
})
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
return tokens
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
/**
|
|
2524
|
+
* Returns `true` if the originator already holds a valid unexpired certificate access
|
|
2525
|
+
* for the given certType/fields. Does not prompt the user.
|
|
2526
|
+
*/
|
|
2527
|
+
public async hasCertificateAccess(params: {
|
|
2528
|
+
originator: string
|
|
2529
|
+
privileged: boolean
|
|
2530
|
+
verifier: string
|
|
2531
|
+
certType: string
|
|
2532
|
+
fields: string[]
|
|
2533
|
+
}): Promise<boolean> {
|
|
2534
|
+
try {
|
|
2535
|
+
await this.ensureCertificateAccess({
|
|
2536
|
+
originator: params.originator,
|
|
2537
|
+
privileged: params.privileged,
|
|
2538
|
+
verifier: params.verifier,
|
|
2539
|
+
certType: params.certType,
|
|
2540
|
+
fields: params.fields,
|
|
2541
|
+
seekPermission: false,
|
|
2542
|
+
usageType: 'disclosure'
|
|
2543
|
+
})
|
|
2544
|
+
return true
|
|
2545
|
+
} catch {
|
|
2546
|
+
return false
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
/**
|
|
2551
|
+
* Revokes a permission token by spending it with no replacement output.
|
|
2552
|
+
* The manager builds a BRC-100 transaction that consumes the token, effectively invalidating it.
|
|
2553
|
+
*/
|
|
2554
|
+
public async revokePermission(oldToken: PermissionToken): Promise<void> {
|
|
2555
|
+
const oldOutpoint = `${oldToken.txid}.${oldToken.outputIndex}`
|
|
2556
|
+
const { signableTransaction } = await this.createAction(
|
|
2557
|
+
{
|
|
2558
|
+
description: `Revoke permission`,
|
|
2559
|
+
inputBEEF: oldToken.tx,
|
|
2560
|
+
inputs: [
|
|
2561
|
+
{
|
|
2562
|
+
outpoint: oldOutpoint,
|
|
2563
|
+
unlockingScriptLength: 73, // length of signature
|
|
2564
|
+
inputDescription: `Consume old permission token`
|
|
2565
|
+
}
|
|
2566
|
+
],
|
|
2567
|
+
options: {
|
|
2568
|
+
acceptDelayedBroadcast: false
|
|
2569
|
+
}
|
|
2570
|
+
},
|
|
2571
|
+
this.adminOriginator
|
|
2572
|
+
)
|
|
2573
|
+
const tx = Transaction.fromBEEF(signableTransaction!.tx)
|
|
2574
|
+
const unlocker = new PushDrop(this.underlying).unlock(
|
|
2575
|
+
WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
|
|
2576
|
+
'1',
|
|
2577
|
+
'self',
|
|
2578
|
+
'all',
|
|
2579
|
+
false,
|
|
2580
|
+
1,
|
|
2581
|
+
LockingScript.fromHex(oldToken.outputScript)
|
|
2582
|
+
)
|
|
2583
|
+
const unlockingScript = await unlocker.sign(tx, 0)
|
|
2584
|
+
await this.underlying.signAction({
|
|
2585
|
+
reference: signableTransaction!.reference,
|
|
2586
|
+
spends: {
|
|
2587
|
+
0: {
|
|
2588
|
+
unlockingScript: unlockingScript.toHex()
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
})
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
/* ---------------------------------------------------------------------
|
|
2595
|
+
* 7) BRC-100 WALLET INTERFACE FORWARDING WITH PERMISSION CHECKS
|
|
2596
|
+
* --------------------------------------------------------------------- */
|
|
2597
|
+
|
|
2598
|
+
public async createAction(
|
|
2599
|
+
args: Parameters<WalletInterface['createAction']>[0],
|
|
2600
|
+
originator?: string
|
|
2601
|
+
): ReturnType<WalletInterface['createAction']> {
|
|
2602
|
+
// 1) Identify unique P-modules involved (one per schemeID)
|
|
2603
|
+
const pModulesByScheme = new Map<string, PermissionsModule>()
|
|
2604
|
+
const nonPBaskets: string[] = []
|
|
2605
|
+
|
|
2606
|
+
if (args.outputs) {
|
|
2607
|
+
for (const out of args.outputs) {
|
|
2608
|
+
if (out.basket) {
|
|
2609
|
+
if (out.basket.startsWith('p ')) {
|
|
2610
|
+
const schemeID = out.basket.split(' ')[1]
|
|
2611
|
+
if (!pModulesByScheme.has(schemeID)) {
|
|
2612
|
+
const module = this.config.permissionModules?.[schemeID]
|
|
2613
|
+
if (!module) {
|
|
2614
|
+
throw new Error(`Unsupported P-basket scheme: p ${schemeID}`)
|
|
2615
|
+
}
|
|
2616
|
+
pModulesByScheme.set(schemeID, module)
|
|
2617
|
+
}
|
|
2618
|
+
} else {
|
|
2619
|
+
// Track non-P baskets for normal permission checks
|
|
2620
|
+
nonPBaskets.push(out.basket)
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
// 2) Check permissions for non-P baskets
|
|
2627
|
+
for (const basket of nonPBaskets) {
|
|
2628
|
+
await this.ensureBasketAccess({
|
|
2629
|
+
originator: originator!,
|
|
2630
|
+
basket,
|
|
2631
|
+
reason: args.description,
|
|
2632
|
+
usageType: 'insertion'
|
|
2633
|
+
})
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
if (args.labels) {
|
|
2637
|
+
for (const lbl of args.labels) {
|
|
2638
|
+
await this.ensureLabelAccess({
|
|
2639
|
+
originator: originator!,
|
|
2640
|
+
label: lbl,
|
|
2641
|
+
reason: args.description,
|
|
2642
|
+
usageType: 'apply'
|
|
2643
|
+
})
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
/**
|
|
2648
|
+
* 4) Force signAndProcess=false unless the originator is admin and explicitly sets it to true.
|
|
2649
|
+
* This ensures the underlying wallet returns a signableTransaction, letting us parse the transaction
|
|
2650
|
+
* to determine net spending and request authorization if needed.
|
|
2651
|
+
*/
|
|
2652
|
+
const modifiedOptions = { ...(args.options || {}) }
|
|
2653
|
+
if (modifiedOptions.signAndProcess !== true) {
|
|
2654
|
+
modifiedOptions.signAndProcess = false
|
|
2655
|
+
} else if (!this.isAdminOriginator(originator!)) {
|
|
2656
|
+
throw new Error('Only the admin originator can set signAndProcess=true explicitly.')
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
// 5) Encrypt transaction metadata, saving originals for use in permissions and line items.
|
|
2660
|
+
const originalDescription = args.description
|
|
2661
|
+
const originalInputDescriptions = {}
|
|
2662
|
+
const originalOutputDescriptions = {}
|
|
2663
|
+
args.description = await this.maybeEncryptMetadata(args.description)
|
|
2664
|
+
for (let i = 0; i < (args.inputs || []).length; i++) {
|
|
2665
|
+
if (args.inputs![i].inputDescription) {
|
|
2666
|
+
originalInputDescriptions[i] = args.inputs![i].inputDescription
|
|
2667
|
+
args.inputs![i].inputDescription = await this.maybeEncryptMetadata(args.inputs![i].inputDescription)
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
for (let i = 0; i < (args.outputs || []).length; i++) {
|
|
2671
|
+
if (args.outputs![i].outputDescription) {
|
|
2672
|
+
originalOutputDescriptions[i] = args.outputs![i].outputDescription
|
|
2673
|
+
args.outputs![i].outputDescription = await this.maybeEncryptMetadata(args.outputs![i].outputDescription)
|
|
2674
|
+
}
|
|
2675
|
+
if (args.outputs![i].customInstructions) {
|
|
2676
|
+
args.outputs![i].customInstructions = await this.maybeEncryptMetadata(args.outputs![i].customInstructions!)
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
/**
|
|
2681
|
+
* 6) Call the underlying wallet's createAction.
|
|
2682
|
+
* - If P-modules are involved, chain request transformations through them first
|
|
2683
|
+
* - Add two "admin" labels for tracking: "admin originator <domain>" and "admin month YYYY-MM"
|
|
2684
|
+
* - If P-modules are involved, chain response transformations back through them
|
|
2685
|
+
*/
|
|
2686
|
+
const finalArgs = {
|
|
2687
|
+
...args,
|
|
2688
|
+
options: modifiedOptions,
|
|
2689
|
+
labels: [...(args.labels || []), `admin originator ${originator}`, `admin month ${this.getCurrentMonthYearUTC()}`]
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
let createResult: Awaited<ReturnType<WalletInterface['createAction']>>
|
|
2693
|
+
|
|
2694
|
+
if (pModulesByScheme.size > 0) {
|
|
2695
|
+
// P-modules are involved - chain transformations
|
|
2696
|
+
const pModules = Array.from(pModulesByScheme.values())
|
|
2697
|
+
|
|
2698
|
+
// Chain onRequest calls through all modules
|
|
2699
|
+
let transformedArgs: object = finalArgs
|
|
2700
|
+
for (const module of pModules) {
|
|
2701
|
+
const transformed = await module.onRequest({
|
|
2702
|
+
method: 'createAction',
|
|
2703
|
+
args: transformedArgs,
|
|
2704
|
+
originator: originator!
|
|
2705
|
+
})
|
|
2706
|
+
transformedArgs = transformed.args
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
// Call underlying wallet with transformed args
|
|
2710
|
+
createResult = await this.underlying.createAction(transformedArgs as CreateActionArgs, originator!)
|
|
2711
|
+
|
|
2712
|
+
// Chain onResponse calls in reverse order
|
|
2713
|
+
for (let i = pModules.length - 1; i >= 0; i--) {
|
|
2714
|
+
createResult = await pModules[i].onResponse(createResult, {
|
|
2715
|
+
method: 'createAction',
|
|
2716
|
+
originator: originator!
|
|
2717
|
+
})
|
|
2718
|
+
}
|
|
2719
|
+
} else {
|
|
2720
|
+
// No P-modules - call underlying wallet directly
|
|
2721
|
+
createResult = await this.underlying.createAction(finalArgs, originator!)
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
// If there's no signableTransaction, the underlying wallet must have fully finalized it. Return as is.
|
|
2725
|
+
if (!createResult.signableTransaction) {
|
|
2726
|
+
return createResult
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
/**
|
|
2730
|
+
* 7) We have a signable transaction. Parse it to determine how much the originator is actually spending.
|
|
2731
|
+
* We only consider inputs the originator explicitly listed in args.inputs.
|
|
2732
|
+
* netSpent = (sum of originator-requested outputs) - (sum of matching originator inputs).
|
|
2733
|
+
* If netSpent > 0, we need spending authorization.
|
|
2734
|
+
*/
|
|
2735
|
+
const tx = Transaction.fromAtomicBEEF(createResult.signableTransaction.tx)
|
|
2736
|
+
const reference = createResult.signableTransaction.reference
|
|
2737
|
+
|
|
2738
|
+
let netSpent = 0
|
|
2739
|
+
const lineItems: Array<{
|
|
2740
|
+
type: 'input' | 'output' | 'fee'
|
|
2741
|
+
description: string
|
|
2742
|
+
satoshis: number
|
|
2743
|
+
}> = []
|
|
2744
|
+
|
|
2745
|
+
// Sum originator-provided inputs:
|
|
2746
|
+
let totalInputSatoshis = 0
|
|
2747
|
+
for (const input of tx.inputs) {
|
|
2748
|
+
const outpoint = `${input.sourceTXID}.${input.sourceOutputIndex}`
|
|
2749
|
+
const matchingIndex = (args.inputs || []).findIndex(i => i.outpoint === outpoint)
|
|
2750
|
+
if (matchingIndex !== -1) {
|
|
2751
|
+
const satoshis = input.sourceTransaction!.outputs[input.sourceOutputIndex].satoshis
|
|
2752
|
+
totalInputSatoshis += satoshis!
|
|
2753
|
+
lineItems.push({
|
|
2754
|
+
type: 'input',
|
|
2755
|
+
description: originalInputDescriptions[matchingIndex] || 'No input description provided',
|
|
2756
|
+
satoshis: satoshis!
|
|
2757
|
+
})
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
// Sum originator-requested outputs:
|
|
2762
|
+
const totalOutputSatoshis = (args.outputs || []).reduce((acc, out) => acc + out.satoshis, 0)
|
|
2763
|
+
for (const outIndex in args.outputs || []) {
|
|
2764
|
+
const out = args.outputs![outIndex]
|
|
2765
|
+
lineItems.push({
|
|
2766
|
+
type: 'output',
|
|
2767
|
+
satoshis: out.satoshis,
|
|
2768
|
+
description: originalOutputDescriptions[outIndex] || 'No output description provided'
|
|
2769
|
+
})
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// Add an entry for the transaction fee:
|
|
2773
|
+
lineItems.push({
|
|
2774
|
+
type: 'fee',
|
|
2775
|
+
satoshis: tx.getFee(),
|
|
2776
|
+
description: 'Network fee'
|
|
2777
|
+
})
|
|
2778
|
+
|
|
2779
|
+
/**
|
|
2780
|
+
* When it comes to spending authorizations, and the computation of net spend, there are
|
|
2781
|
+
* two types of inputs and two types of outputs:
|
|
2782
|
+
*
|
|
2783
|
+
* There are foreign (originator-requested) ones, and domestic (internally-provided) ones.
|
|
2784
|
+
* The net spend is always calculated from the domestic, internal perspective. Therefore, the
|
|
2785
|
+
* cost of funding the foreign outputs is the base cost to the domestic user, unless this is
|
|
2786
|
+
* somehow offset.
|
|
2787
|
+
*
|
|
2788
|
+
* The only way to offset this cost is when the foreign inputs help carry some of the burden.
|
|
2789
|
+
* This is why we can subtract the sum of the foreign inputs from the sum of foreign outputs,
|
|
2790
|
+
* to gague how much of that cost needs to be born domestically by the user.
|
|
2791
|
+
*
|
|
2792
|
+
* The logic does not need to account for whatever domestic inputs are provided, or whatever
|
|
2793
|
+
* domestic outputs are re-captured by the wallet back as change. The wallet could conceivably
|
|
2794
|
+
* provide 21e8 satoshis as input and re-capture the same amount as change, but the net effect
|
|
2795
|
+
* on actual spending would be zero. Therefore, we base net spend on total foreign outflows
|
|
2796
|
+
* minus total foreign inflows. Fees are also considered.
|
|
2797
|
+
*/
|
|
2798
|
+
netSpent = totalOutputSatoshis + tx.getFee() - totalInputSatoshis
|
|
2799
|
+
|
|
2800
|
+
// 8) If netSpent > 0, require spending authorization. Abort if denied.
|
|
2801
|
+
if (netSpent > 0) {
|
|
2802
|
+
try {
|
|
2803
|
+
await this.ensureSpendingAuthorization({
|
|
2804
|
+
originator: originator!,
|
|
2805
|
+
satoshis: netSpent,
|
|
2806
|
+
lineItems,
|
|
2807
|
+
reason: originalDescription
|
|
2808
|
+
})
|
|
2809
|
+
} catch (err) {
|
|
2810
|
+
await this.underlying.abortAction({ reference })
|
|
2811
|
+
throw err
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
/**
|
|
2816
|
+
* 9) Decide whether to finalize the transaction automatically or return the signableTransaction:
|
|
2817
|
+
* - If the user originally wanted signAndProcess (the default when undefined), we forcibly set it to false earlier, so check if we should now finalize it.
|
|
2818
|
+
* - If the transaction still needs more signatures, we must return the signableTransaction.
|
|
2819
|
+
*/
|
|
2820
|
+
const vargs = Validation.validateCreateActionArgs(args)
|
|
2821
|
+
if (vargs.isSignAction) {
|
|
2822
|
+
return createResult
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
const signResult = await this.underlying.signAction({ reference, spends: {}, options: args.options }, originator)
|
|
2826
|
+
// Merge signResult into createResult and remove signableTransaction:
|
|
2827
|
+
return {
|
|
2828
|
+
...createResult,
|
|
2829
|
+
...signResult,
|
|
2830
|
+
signableTransaction: undefined
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
public async signAction(
|
|
2835
|
+
...args: Parameters<WalletInterface['signAction']>
|
|
2836
|
+
): ReturnType<WalletInterface['signAction']> {
|
|
2837
|
+
return this.underlying.signAction(...args)
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
public async abortAction(
|
|
2841
|
+
...args: Parameters<WalletInterface['abortAction']>
|
|
2842
|
+
): ReturnType<WalletInterface['abortAction']> {
|
|
2843
|
+
return this.underlying.abortAction(...args)
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
public async listActions(
|
|
2847
|
+
...args: Parameters<WalletInterface['listActions']>
|
|
2848
|
+
): ReturnType<WalletInterface['listActions']> {
|
|
2849
|
+
const [requestArgs, originator] = args
|
|
2850
|
+
// for each label, ensure label access
|
|
2851
|
+
if (requestArgs.labels) {
|
|
2852
|
+
for (const lbl of requestArgs.labels) {
|
|
2853
|
+
await this.ensureLabelAccess({
|
|
2854
|
+
originator: originator!,
|
|
2855
|
+
label: lbl,
|
|
2856
|
+
reason: 'listActions',
|
|
2857
|
+
usageType: 'list'
|
|
2858
|
+
})
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
const results = await this.underlying.listActions(...args)
|
|
2862
|
+
// Transparently decrypt transaction metadata, if configured to do so.
|
|
2863
|
+
if (results.actions) {
|
|
2864
|
+
for (let i = 0; i < results.actions.length; i++) {
|
|
2865
|
+
if (results.actions[i].description) {
|
|
2866
|
+
results.actions[i].description = await this.maybeDecryptMetadata(results.actions[i].description)
|
|
2867
|
+
}
|
|
2868
|
+
if (results.actions[i].inputs) {
|
|
2869
|
+
for (let j = 0; j < results.actions[i].inputs!.length; j++) {
|
|
2870
|
+
if (results.actions[i].inputs![j].inputDescription) {
|
|
2871
|
+
results.actions[i].inputs![j].inputDescription = await this.maybeDecryptMetadata(
|
|
2872
|
+
results.actions[i].inputs![j].inputDescription
|
|
2873
|
+
)
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
if (results.actions[i].outputs) {
|
|
2878
|
+
for (let j = 0; j < results.actions[i].outputs!.length; j++) {
|
|
2879
|
+
if (results.actions[i].outputs![j].outputDescription) {
|
|
2880
|
+
results.actions[i].outputs![j].outputDescription = await this.maybeDecryptMetadata(
|
|
2881
|
+
results.actions[i].outputs![j].outputDescription
|
|
2882
|
+
)
|
|
2883
|
+
}
|
|
2884
|
+
if (results.actions[i].outputs![j].customInstructions) {
|
|
2885
|
+
results.actions[i].outputs![j].customInstructions = await this.maybeDecryptMetadata(
|
|
2886
|
+
results.actions[i].outputs![j].customInstructions!
|
|
2887
|
+
)
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
return results
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
public async internalizeAction(
|
|
2897
|
+
...args: Parameters<WalletInterface['internalizeAction']>
|
|
2898
|
+
): ReturnType<WalletInterface['internalizeAction']> {
|
|
2899
|
+
const [requestArgs, originator] = args
|
|
2900
|
+
// If the transaction is inserting outputs into baskets, we also ensure basket permission
|
|
2901
|
+
for (const outIndex in requestArgs.outputs) {
|
|
2902
|
+
const out = requestArgs.outputs[outIndex]
|
|
2903
|
+
if (out.protocol === 'basket insertion') {
|
|
2904
|
+
// Delegate to permission module if needed
|
|
2905
|
+
const pModuleResult = await this.delegateToPModuleIfNeeded(
|
|
2906
|
+
out.insertionRemittance!.basket,
|
|
2907
|
+
'internalizeAction',
|
|
2908
|
+
requestArgs,
|
|
2909
|
+
originator!,
|
|
2910
|
+
async transformedArgs => {
|
|
2911
|
+
if (out.insertionRemittance!.customInstructions) {
|
|
2912
|
+
;(transformedArgs as InternalizeActionArgs).outputs[outIndex].insertionRemittance!.customInstructions =
|
|
2913
|
+
await this.maybeEncryptMetadata(out.insertionRemittance!.customInstructions)
|
|
2914
|
+
}
|
|
2915
|
+
return await this.underlying.internalizeAction(transformedArgs as InternalizeActionArgs, originator!)
|
|
2916
|
+
}
|
|
2917
|
+
)
|
|
2918
|
+
if (pModuleResult !== null) {
|
|
2919
|
+
return pModuleResult
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
await this.ensureBasketAccess({
|
|
2923
|
+
originator: originator!,
|
|
2924
|
+
basket: out.insertionRemittance!.basket,
|
|
2925
|
+
reason: requestArgs.description,
|
|
2926
|
+
usageType: 'insertion'
|
|
2927
|
+
})
|
|
2928
|
+
if (out.insertionRemittance!.customInstructions) {
|
|
2929
|
+
requestArgs.outputs[outIndex].insertionRemittance!.customInstructions = await this.maybeEncryptMetadata(
|
|
2930
|
+
out.insertionRemittance!.customInstructions
|
|
2931
|
+
)
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
return this.underlying.internalizeAction(...args)
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
public async listOutputs(
|
|
2939
|
+
...args: Parameters<WalletInterface['listOutputs']>
|
|
2940
|
+
): ReturnType<WalletInterface['listOutputs']> {
|
|
2941
|
+
const [requestArgs, originator] = args
|
|
2942
|
+
|
|
2943
|
+
// Delegate to permission module if needed
|
|
2944
|
+
const pModuleResult = await this.delegateToPModuleIfNeeded(
|
|
2945
|
+
requestArgs.basket,
|
|
2946
|
+
'listOutputs',
|
|
2947
|
+
requestArgs,
|
|
2948
|
+
originator!,
|
|
2949
|
+
async transformedArgs => {
|
|
2950
|
+
const result = await this.underlying.listOutputs(transformedArgs as ListOutputsArgs, originator!)
|
|
2951
|
+
// Apply metadata decryption to permission module response
|
|
2952
|
+
return await this.decryptListOutputsMetadata(result)
|
|
2953
|
+
}
|
|
2954
|
+
)
|
|
2955
|
+
if (pModuleResult !== null) {
|
|
2956
|
+
return pModuleResult
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
// Ensure the originator has permission for the basket.
|
|
2960
|
+
await this.ensureBasketAccess({
|
|
2961
|
+
originator: originator!,
|
|
2962
|
+
basket: requestArgs.basket,
|
|
2963
|
+
reason: 'listOutputs',
|
|
2964
|
+
usageType: 'listing'
|
|
2965
|
+
})
|
|
2966
|
+
const results = await this.underlying.listOutputs(...args)
|
|
2967
|
+
|
|
2968
|
+
// Apply metadata decryption to regular response
|
|
2969
|
+
return await this.decryptListOutputsMetadata(results)
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
public async relinquishOutput(
|
|
2973
|
+
...args: Parameters<WalletInterface['relinquishOutput']>
|
|
2974
|
+
): ReturnType<WalletInterface['relinquishOutput']> {
|
|
2975
|
+
const [requestArgs, originator] = args
|
|
2976
|
+
|
|
2977
|
+
// Delegate to permission module if needed
|
|
2978
|
+
const pModuleResult = await this.delegateToPModuleIfNeeded(
|
|
2979
|
+
requestArgs.basket,
|
|
2980
|
+
'relinquishOutput',
|
|
2981
|
+
requestArgs,
|
|
2982
|
+
originator!,
|
|
2983
|
+
async transformedArgs => {
|
|
2984
|
+
return await this.underlying.relinquishOutput(transformedArgs as RelinquishOutputArgs, originator!)
|
|
2985
|
+
}
|
|
2986
|
+
)
|
|
2987
|
+
if (pModuleResult !== null) {
|
|
2988
|
+
return pModuleResult
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
await this.ensureBasketAccess({
|
|
2992
|
+
originator: originator!,
|
|
2993
|
+
basket: requestArgs.basket,
|
|
2994
|
+
reason: 'relinquishOutput',
|
|
2995
|
+
usageType: 'removal'
|
|
2996
|
+
})
|
|
2997
|
+
return this.underlying.relinquishOutput(...args)
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
public async getPublicKey(
|
|
3001
|
+
...args: Parameters<WalletInterface['getPublicKey']>
|
|
3002
|
+
): ReturnType<WalletInterface['getPublicKey']> {
|
|
3003
|
+
const [requestArgs, originator] = args
|
|
3004
|
+
|
|
3005
|
+
if (requestArgs.protocolID) {
|
|
3006
|
+
// Delegate to permission module if needed
|
|
3007
|
+
const pModuleResult = await this.delegateToPModuleIfNeeded(
|
|
3008
|
+
requestArgs.protocolID[1],
|
|
3009
|
+
'getPublicKey',
|
|
3010
|
+
requestArgs,
|
|
3011
|
+
originator!,
|
|
3012
|
+
async transformedArgs => {
|
|
3013
|
+
return await this.underlying.getPublicKey(transformedArgs as GetPublicKeyArgs, originator!)
|
|
3014
|
+
}
|
|
3015
|
+
)
|
|
3016
|
+
if (pModuleResult !== null) {
|
|
3017
|
+
return pModuleResult
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
// Not a P-protocol, continue with normal permission flow
|
|
3021
|
+
await this.ensureProtocolPermission({
|
|
3022
|
+
originator: originator!,
|
|
3023
|
+
privileged: requestArgs.privileged!,
|
|
3024
|
+
protocolID: requestArgs.protocolID,
|
|
3025
|
+
counterparty: requestArgs.counterparty || 'self',
|
|
3026
|
+
reason: requestArgs.privilegedReason,
|
|
3027
|
+
usageType: 'publicKey'
|
|
3028
|
+
})
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
if (requestArgs.identityKey) {
|
|
3032
|
+
// We also require a minimal protocol permission to retrieve the user's identity key
|
|
3033
|
+
await this.ensureProtocolPermission({
|
|
3034
|
+
originator: originator!,
|
|
3035
|
+
privileged: requestArgs.privileged!,
|
|
3036
|
+
protocolID: [1, 'identity key retrieval'],
|
|
3037
|
+
counterparty: 'self',
|
|
3038
|
+
reason: requestArgs.privilegedReason,
|
|
3039
|
+
usageType: 'identityKey'
|
|
3040
|
+
})
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
return this.underlying.getPublicKey(...args)
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
public async revealCounterpartyKeyLinkage(
|
|
3047
|
+
...args: Parameters<WalletInterface['revealCounterpartyKeyLinkage']>
|
|
3048
|
+
): ReturnType<WalletInterface['revealCounterpartyKeyLinkage']> {
|
|
3049
|
+
const [requestArgs, originator] = args
|
|
3050
|
+
await this.ensureProtocolPermission({
|
|
3051
|
+
originator: originator!,
|
|
3052
|
+
privileged: requestArgs.privileged!,
|
|
3053
|
+
protocolID: [2, `counterparty key linkage revelation ${requestArgs.counterparty}`],
|
|
3054
|
+
counterparty: requestArgs.verifier,
|
|
3055
|
+
reason: requestArgs.privilegedReason,
|
|
3056
|
+
usageType: 'linkageRevelation'
|
|
3057
|
+
})
|
|
3058
|
+
return this.underlying.revealCounterpartyKeyLinkage(...args)
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
public async revealSpecificKeyLinkage(
|
|
3062
|
+
...args: Parameters<WalletInterface['revealSpecificKeyLinkage']>
|
|
3063
|
+
): ReturnType<WalletInterface['revealSpecificKeyLinkage']> {
|
|
3064
|
+
const [requestArgs, originator] = args
|
|
3065
|
+
await this.ensureProtocolPermission({
|
|
3066
|
+
originator: originator!,
|
|
3067
|
+
privileged: requestArgs.privileged!,
|
|
3068
|
+
protocolID: [
|
|
3069
|
+
2,
|
|
3070
|
+
`specific key linkage revelation ${requestArgs.protocolID[1]} ${requestArgs.protocolID[0] === 2 ? requestArgs.keyID : 'all'}`
|
|
3071
|
+
],
|
|
3072
|
+
counterparty: requestArgs.verifier,
|
|
3073
|
+
reason: requestArgs.privilegedReason,
|
|
3074
|
+
usageType: 'linkageRevelation'
|
|
3075
|
+
})
|
|
3076
|
+
return this.underlying.revealSpecificKeyLinkage(...args)
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
public async encrypt(...args: Parameters<WalletInterface['encrypt']>): ReturnType<WalletInterface['encrypt']> {
|
|
3080
|
+
const [requestArgs, originator] = args
|
|
3081
|
+
// Delegate to permission module if needed
|
|
3082
|
+
const pModuleResult = await this.delegateToPModuleIfNeeded(
|
|
3083
|
+
requestArgs.protocolID[1],
|
|
3084
|
+
'encrypt',
|
|
3085
|
+
requestArgs,
|
|
3086
|
+
originator!,
|
|
3087
|
+
async transformedArgs => {
|
|
3088
|
+
return await this.underlying.encrypt(transformedArgs as WalletEncryptArgs, originator!)
|
|
3089
|
+
}
|
|
3090
|
+
)
|
|
3091
|
+
if (pModuleResult !== null) {
|
|
3092
|
+
return pModuleResult
|
|
3093
|
+
}
|
|
3094
|
+
await this.ensureProtocolPermission({
|
|
3095
|
+
originator: originator!,
|
|
3096
|
+
protocolID: requestArgs.protocolID,
|
|
3097
|
+
privileged: requestArgs.privileged!,
|
|
3098
|
+
counterparty: requestArgs.counterparty || 'self',
|
|
3099
|
+
reason: requestArgs.privilegedReason,
|
|
3100
|
+
usageType: 'encrypting'
|
|
3101
|
+
})
|
|
3102
|
+
return this.underlying.encrypt(...args)
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
public async decrypt(...args: Parameters<WalletInterface['decrypt']>): ReturnType<WalletInterface['decrypt']> {
|
|
3106
|
+
const [requestArgs, originator] = args
|
|
3107
|
+
// Delegate to permission module if needed
|
|
3108
|
+
const pModuleResult = await this.delegateToPModuleIfNeeded(
|
|
3109
|
+
requestArgs.protocolID[1],
|
|
3110
|
+
'decrypt',
|
|
3111
|
+
requestArgs,
|
|
3112
|
+
originator!,
|
|
3113
|
+
async transformedArgs => {
|
|
3114
|
+
return await this.underlying.decrypt(transformedArgs as WalletDecryptArgs, originator!)
|
|
3115
|
+
}
|
|
3116
|
+
)
|
|
3117
|
+
if (pModuleResult !== null) {
|
|
3118
|
+
return pModuleResult
|
|
3119
|
+
}
|
|
3120
|
+
await this.ensureProtocolPermission({
|
|
3121
|
+
originator: originator!,
|
|
3122
|
+
privileged: requestArgs.privileged!,
|
|
3123
|
+
protocolID: requestArgs.protocolID,
|
|
3124
|
+
counterparty: requestArgs.counterparty || 'self',
|
|
3125
|
+
reason: requestArgs.privilegedReason,
|
|
3126
|
+
usageType: 'encrypting'
|
|
3127
|
+
})
|
|
3128
|
+
return this.underlying.decrypt(...args)
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
public async createHmac(
|
|
3132
|
+
...args: Parameters<WalletInterface['createHmac']>
|
|
3133
|
+
): ReturnType<WalletInterface['createHmac']> {
|
|
3134
|
+
const [requestArgs, originator] = args
|
|
3135
|
+
// Delegate to permission module if needed
|
|
3136
|
+
const pModuleResult = await this.delegateToPModuleIfNeeded(
|
|
3137
|
+
requestArgs.protocolID[1],
|
|
3138
|
+
'createHmac',
|
|
3139
|
+
requestArgs,
|
|
3140
|
+
originator!,
|
|
3141
|
+
async transformedArgs => {
|
|
3142
|
+
return await this.underlying.createHmac(transformedArgs as CreateHmacArgs, originator!)
|
|
3143
|
+
}
|
|
3144
|
+
)
|
|
3145
|
+
if (pModuleResult !== null) {
|
|
3146
|
+
return pModuleResult
|
|
3147
|
+
}
|
|
3148
|
+
await this.ensureProtocolPermission({
|
|
3149
|
+
originator: originator!,
|
|
3150
|
+
privileged: requestArgs.privileged!,
|
|
3151
|
+
protocolID: requestArgs.protocolID,
|
|
3152
|
+
counterparty: requestArgs.counterparty || 'self',
|
|
3153
|
+
reason: requestArgs.privilegedReason,
|
|
3154
|
+
usageType: 'hmac'
|
|
3155
|
+
})
|
|
3156
|
+
return this.underlying.createHmac(...args)
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
public async verifyHmac(
|
|
3160
|
+
...args: Parameters<WalletInterface['verifyHmac']>
|
|
3161
|
+
): ReturnType<WalletInterface['verifyHmac']> {
|
|
3162
|
+
const [requestArgs, originator] = args
|
|
3163
|
+
// Delegate to permission module if needed
|
|
3164
|
+
const pModuleResult = await this.delegateToPModuleIfNeeded(
|
|
3165
|
+
requestArgs.protocolID[1],
|
|
3166
|
+
'verifyHmac',
|
|
3167
|
+
requestArgs,
|
|
3168
|
+
originator!,
|
|
3169
|
+
async transformedArgs => {
|
|
3170
|
+
return await this.underlying.verifyHmac(transformedArgs as VerifyHmacArgs, originator!)
|
|
3171
|
+
}
|
|
3172
|
+
)
|
|
3173
|
+
if (pModuleResult !== null) {
|
|
3174
|
+
return pModuleResult
|
|
3175
|
+
}
|
|
3176
|
+
await this.ensureProtocolPermission({
|
|
3177
|
+
originator: originator!,
|
|
3178
|
+
privileged: requestArgs.privileged!,
|
|
3179
|
+
protocolID: requestArgs.protocolID,
|
|
3180
|
+
counterparty: requestArgs.counterparty || 'self',
|
|
3181
|
+
reason: requestArgs.privilegedReason,
|
|
3182
|
+
usageType: 'hmac'
|
|
3183
|
+
})
|
|
3184
|
+
return this.underlying.verifyHmac(...args)
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
public async createSignature(
|
|
3188
|
+
...args: Parameters<WalletInterface['createSignature']>
|
|
3189
|
+
): ReturnType<WalletInterface['createSignature']> {
|
|
3190
|
+
const [requestArgs, originator] = args
|
|
3191
|
+
// Delegate to permission module if needed
|
|
3192
|
+
const pModuleResult = await this.delegateToPModuleIfNeeded(
|
|
3193
|
+
requestArgs.protocolID[1],
|
|
3194
|
+
'createSignature',
|
|
3195
|
+
requestArgs,
|
|
3196
|
+
originator!,
|
|
3197
|
+
async transformedArgs => {
|
|
3198
|
+
return await this.underlying.createSignature(transformedArgs as CreateSignatureArgs, originator!)
|
|
3199
|
+
}
|
|
3200
|
+
)
|
|
3201
|
+
if (pModuleResult !== null) {
|
|
3202
|
+
return pModuleResult
|
|
3203
|
+
}
|
|
3204
|
+
await this.ensureProtocolPermission({
|
|
3205
|
+
originator: originator!,
|
|
3206
|
+
privileged: requestArgs.privileged!,
|
|
3207
|
+
protocolID: requestArgs.protocolID,
|
|
3208
|
+
counterparty: requestArgs.counterparty || 'self',
|
|
3209
|
+
reason: requestArgs.privilegedReason,
|
|
3210
|
+
usageType: 'signing'
|
|
3211
|
+
})
|
|
3212
|
+
return this.underlying.createSignature(...args)
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
public async verifySignature(
|
|
3216
|
+
...args: Parameters<WalletInterface['verifySignature']>
|
|
3217
|
+
): ReturnType<WalletInterface['verifySignature']> {
|
|
3218
|
+
const [requestArgs, originator] = args
|
|
3219
|
+
// Delegate to permission module if needed
|
|
3220
|
+
const pModuleResult = await this.delegateToPModuleIfNeeded(
|
|
3221
|
+
requestArgs.protocolID[1],
|
|
3222
|
+
'verifySignature',
|
|
3223
|
+
requestArgs,
|
|
3224
|
+
originator!,
|
|
3225
|
+
async transformedArgs => {
|
|
3226
|
+
return await this.underlying.verifySignature(transformedArgs as VerifySignatureArgs, originator!)
|
|
3227
|
+
}
|
|
3228
|
+
)
|
|
3229
|
+
if (pModuleResult !== null) {
|
|
3230
|
+
return pModuleResult
|
|
3231
|
+
}
|
|
3232
|
+
await this.ensureProtocolPermission({
|
|
3233
|
+
originator: originator!,
|
|
3234
|
+
privileged: requestArgs.privileged!,
|
|
3235
|
+
protocolID: requestArgs.protocolID,
|
|
3236
|
+
counterparty: requestArgs.counterparty || 'self',
|
|
3237
|
+
reason: requestArgs.privilegedReason,
|
|
3238
|
+
usageType: 'signing'
|
|
3239
|
+
})
|
|
3240
|
+
return this.underlying.verifySignature(...args)
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
public async acquireCertificate(
|
|
3244
|
+
...args: Parameters<WalletInterface['acquireCertificate']>
|
|
3245
|
+
): ReturnType<WalletInterface['acquireCertificate']> {
|
|
3246
|
+
const [requestArgs, originator] = args
|
|
3247
|
+
if (this.config.seekCertificateAcquisitionPermissions) {
|
|
3248
|
+
await this.ensureProtocolPermission({
|
|
3249
|
+
originator: originator!,
|
|
3250
|
+
privileged: requestArgs.privileged!,
|
|
3251
|
+
protocolID: [1, `certificate acquisition ${requestArgs.type}`],
|
|
3252
|
+
counterparty: 'self',
|
|
3253
|
+
reason: requestArgs.privilegedReason,
|
|
3254
|
+
usageType: 'generic'
|
|
3255
|
+
})
|
|
3256
|
+
}
|
|
3257
|
+
return this.underlying.acquireCertificate(...args)
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
public async listCertificates(
|
|
3261
|
+
...args: Parameters<WalletInterface['listCertificates']>
|
|
3262
|
+
): ReturnType<WalletInterface['listCertificates']> {
|
|
3263
|
+
const [requestArgs, originator] = args
|
|
3264
|
+
if (this.config.seekCertificateListingPermissions) {
|
|
3265
|
+
await this.ensureProtocolPermission({
|
|
3266
|
+
originator: originator!,
|
|
3267
|
+
privileged: requestArgs.privileged!,
|
|
3268
|
+
protocolID: [1, `certificate list`],
|
|
3269
|
+
counterparty: 'self',
|
|
3270
|
+
reason: requestArgs.privilegedReason,
|
|
3271
|
+
usageType: 'generic'
|
|
3272
|
+
})
|
|
3273
|
+
}
|
|
3274
|
+
return this.underlying.listCertificates(...args)
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
public async proveCertificate(
|
|
3278
|
+
...args: Parameters<WalletInterface['proveCertificate']>
|
|
3279
|
+
): ReturnType<WalletInterface['proveCertificate']> {
|
|
3280
|
+
const [requestArgs, originator] = args
|
|
3281
|
+
await this.ensureCertificateAccess({
|
|
3282
|
+
originator: originator!,
|
|
3283
|
+
privileged: requestArgs.privileged!,
|
|
3284
|
+
verifier: requestArgs.verifier,
|
|
3285
|
+
certType: requestArgs.certificate.type!,
|
|
3286
|
+
fields: requestArgs.fieldsToReveal,
|
|
3287
|
+
reason: 'proveCertificate',
|
|
3288
|
+
usageType: 'disclosure'
|
|
3289
|
+
})
|
|
3290
|
+
return this.underlying.proveCertificate(...args)
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
public async relinquishCertificate(
|
|
3294
|
+
...args: Parameters<WalletInterface['relinquishCertificate']>
|
|
3295
|
+
): ReturnType<WalletInterface['relinquishCertificate']> {
|
|
3296
|
+
const [requestArgs, originator] = args
|
|
3297
|
+
if (this.config.seekCertificateRelinquishmentPermissions) {
|
|
3298
|
+
await this.ensureProtocolPermission({
|
|
3299
|
+
originator: originator!,
|
|
3300
|
+
privileged: (requestArgs as any).privileged ? true : false,
|
|
3301
|
+
protocolID: [1, `certificate relinquishment ${requestArgs.type}`],
|
|
3302
|
+
counterparty: 'self',
|
|
3303
|
+
reason: (requestArgs as any).privilegedReason || 'relinquishCertificate',
|
|
3304
|
+
usageType: 'generic'
|
|
3305
|
+
})
|
|
3306
|
+
}
|
|
3307
|
+
return this.underlying.relinquishCertificate(...args)
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
public async discoverByIdentityKey(
|
|
3311
|
+
...args: Parameters<WalletInterface['discoverByIdentityKey']>
|
|
3312
|
+
): ReturnType<WalletInterface['discoverByIdentityKey']> {
|
|
3313
|
+
const [_, originator] = args
|
|
3314
|
+
if (this.config.seekPermissionsForIdentityResolution) {
|
|
3315
|
+
await this.ensureProtocolPermission({
|
|
3316
|
+
originator: originator!,
|
|
3317
|
+
privileged: false,
|
|
3318
|
+
protocolID: [1, `identity resolution`],
|
|
3319
|
+
counterparty: 'self',
|
|
3320
|
+
reason: 'discoverByIdentityKey',
|
|
3321
|
+
usageType: 'generic'
|
|
3322
|
+
})
|
|
3323
|
+
}
|
|
3324
|
+
return this.underlying.discoverByIdentityKey(...args)
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
public async discoverByAttributes(
|
|
3328
|
+
...args: Parameters<WalletInterface['discoverByAttributes']>
|
|
3329
|
+
): ReturnType<WalletInterface['discoverByAttributes']> {
|
|
3330
|
+
const [_, originator] = args
|
|
3331
|
+
if (this.config.seekPermissionsForIdentityResolution) {
|
|
3332
|
+
await this.ensureProtocolPermission({
|
|
3333
|
+
originator: originator!,
|
|
3334
|
+
privileged: false,
|
|
3335
|
+
protocolID: [1, `identity resolution`],
|
|
3336
|
+
counterparty: 'self',
|
|
3337
|
+
reason: 'discoverByAttributes',
|
|
3338
|
+
usageType: 'generic'
|
|
3339
|
+
})
|
|
3340
|
+
}
|
|
3341
|
+
return this.underlying.discoverByAttributes(...args)
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
public async isAuthenticated(
|
|
3345
|
+
...args: Parameters<WalletInterface['isAuthenticated']>
|
|
3346
|
+
): ReturnType<WalletInterface['isAuthenticated']> {
|
|
3347
|
+
return this.underlying.isAuthenticated(...args)
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
public async waitForAuthentication(
|
|
3351
|
+
...args: Parameters<WalletInterface['waitForAuthentication']>
|
|
3352
|
+
): ReturnType<WalletInterface['waitForAuthentication']> {
|
|
3353
|
+
let [_, originator] = args
|
|
3354
|
+
if (this.config.seekGroupedPermission && originator) {
|
|
3355
|
+
const { normalized: normalizedOriginator } = this.prepareOriginator(originator)
|
|
3356
|
+
originator = normalizedOriginator
|
|
3357
|
+
// 1. Fetch manifest.json from the originator
|
|
3358
|
+
let groupPermissions: GroupedPermissions | undefined
|
|
3359
|
+
try {
|
|
3360
|
+
const proto = originator.startsWith('localhost:') ? 'http' : 'https'
|
|
3361
|
+
const response = await fetch(`${proto}://${originator}/manifest.json`)
|
|
3362
|
+
if (response.ok) {
|
|
3363
|
+
const manifest = await response.json()
|
|
3364
|
+
if (manifest?.babbage?.groupPermissions) {
|
|
3365
|
+
groupPermissions = manifest.babbage.groupPermissions
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
} catch (e) {
|
|
3369
|
+
// Ignore fetch/parse errors, just proceed without group permissions.
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
if (groupPermissions) {
|
|
3373
|
+
// 2. Filter out already-granted permissions
|
|
3374
|
+
const permissionsToRequest: GroupedPermissions = {
|
|
3375
|
+
protocolPermissions: [],
|
|
3376
|
+
basketAccess: [],
|
|
3377
|
+
certificateAccess: []
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
if (groupPermissions.spendingAuthorization) {
|
|
3381
|
+
const hasAuth = await this.hasSpendingAuthorization({
|
|
3382
|
+
originator,
|
|
3383
|
+
satoshis: groupPermissions.spendingAuthorization.amount
|
|
3384
|
+
})
|
|
3385
|
+
if (!hasAuth) {
|
|
3386
|
+
permissionsToRequest.spendingAuthorization = groupPermissions.spendingAuthorization
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
for (const p of groupPermissions.protocolPermissions || []) {
|
|
3391
|
+
const hasPerm = await this.hasProtocolPermission({
|
|
3392
|
+
originator,
|
|
3393
|
+
privileged: false, // Privilege is never allowed here
|
|
3394
|
+
protocolID: p.protocolID,
|
|
3395
|
+
counterparty: p.counterparty || 'self'
|
|
3396
|
+
})
|
|
3397
|
+
if (!hasPerm) {
|
|
3398
|
+
permissionsToRequest.protocolPermissions!.push(p)
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
for (const b of groupPermissions.basketAccess || []) {
|
|
3403
|
+
const hasAccess = await this.hasBasketAccess({
|
|
3404
|
+
originator,
|
|
3405
|
+
basket: b.basket
|
|
3406
|
+
})
|
|
3407
|
+
if (!hasAccess) {
|
|
3408
|
+
permissionsToRequest.basketAccess!.push(b)
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
for (const c of groupPermissions.certificateAccess || []) {
|
|
3413
|
+
const hasAccess = await this.hasCertificateAccess({
|
|
3414
|
+
originator,
|
|
3415
|
+
privileged: false, // Privilege is never allowed here for security
|
|
3416
|
+
verifier: c.verifierPublicKey,
|
|
3417
|
+
certType: c.type,
|
|
3418
|
+
fields: c.fields
|
|
3419
|
+
})
|
|
3420
|
+
if (!hasAccess) {
|
|
3421
|
+
permissionsToRequest.certificateAccess!.push(c)
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
// 3. If any permissions are left to request, start the flow
|
|
3426
|
+
const hasRequests =
|
|
3427
|
+
permissionsToRequest.spendingAuthorization ||
|
|
3428
|
+
(permissionsToRequest.protocolPermissions?.length ?? 0) > 0 ||
|
|
3429
|
+
(permissionsToRequest.basketAccess?.length ?? 0) > 0 ||
|
|
3430
|
+
(permissionsToRequest.certificateAccess?.length ?? 0) > 0
|
|
3431
|
+
|
|
3432
|
+
if (hasRequests) {
|
|
3433
|
+
const key = `group:${originator}`
|
|
3434
|
+
if (this.activeRequests.has(key)) {
|
|
3435
|
+
// Another call is already waiting, piggyback on it
|
|
3436
|
+
await new Promise<boolean>((resolve, reject) => {
|
|
3437
|
+
this.activeRequests.get(key)!.pending.push({ resolve, reject })
|
|
3438
|
+
})
|
|
3439
|
+
} else {
|
|
3440
|
+
// This is the first call, create a new request
|
|
3441
|
+
try {
|
|
3442
|
+
await new Promise<boolean>(async (resolve, reject) => {
|
|
3443
|
+
this.activeRequests.set(key, {
|
|
3444
|
+
request: { originator: originator as string, permissions: permissionsToRequest },
|
|
3445
|
+
pending: [{ resolve, reject }]
|
|
3446
|
+
})
|
|
3447
|
+
|
|
3448
|
+
await this.callEvent('onGroupedPermissionRequested', {
|
|
3449
|
+
requestID: key,
|
|
3450
|
+
originator,
|
|
3451
|
+
permissions: permissionsToRequest
|
|
3452
|
+
})
|
|
3453
|
+
})
|
|
3454
|
+
} catch (e) {
|
|
3455
|
+
// Permission was denied, re-throw to stop execution
|
|
3456
|
+
throw e
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
// Finally, after handling grouped permissions, call the underlying method.
|
|
3464
|
+
return this.underlying.waitForAuthentication(...args)
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
public async getHeight(...args: Parameters<WalletInterface['getHeight']>): ReturnType<WalletInterface['getHeight']> {
|
|
3468
|
+
return this.underlying.getHeight(...args)
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
public async getHeaderForHeight(
|
|
3472
|
+
...args: Parameters<WalletInterface['getHeaderForHeight']>
|
|
3473
|
+
): ReturnType<WalletInterface['getHeaderForHeight']> {
|
|
3474
|
+
return this.underlying.getHeaderForHeight(...args)
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
public async getNetwork(
|
|
3478
|
+
...args: Parameters<WalletInterface['getNetwork']>
|
|
3479
|
+
): ReturnType<WalletInterface['getNetwork']> {
|
|
3480
|
+
return this.underlying.getNetwork(...args)
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
public async getVersion(
|
|
3484
|
+
...args: Parameters<WalletInterface['getVersion']>
|
|
3485
|
+
): ReturnType<WalletInterface['getVersion']> {
|
|
3486
|
+
return this.underlying.getVersion(...args)
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
/* ---------------------------------------------------------------------
|
|
3490
|
+
* 8) INTERNAL HELPER UTILITIES
|
|
3491
|
+
* --------------------------------------------------------------------- */
|
|
3492
|
+
|
|
3493
|
+
/** Returns true if the specified origin is the admin originator. */
|
|
3494
|
+
private isAdminOriginator(originator: string): boolean {
|
|
3495
|
+
return this.normalizeOriginator(originator) === this.adminOriginator
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
/**
|
|
3499
|
+
* Checks if the given protocol is admin-reserved per BRC-100 rules:
|
|
3500
|
+
*
|
|
3501
|
+
* - Must not start with `admin` (admin-reserved)
|
|
3502
|
+
* - Must not start with `p ` (allows for future specially permissioned protocols)
|
|
3503
|
+
*
|
|
3504
|
+
* If it violates these rules and the caller is not admin, we consider it "admin-only."
|
|
3505
|
+
*/
|
|
3506
|
+
private isAdminProtocol(proto: WalletProtocol): boolean {
|
|
3507
|
+
const protocolName = proto[1]
|
|
3508
|
+
if (protocolName.startsWith('admin')) {
|
|
3509
|
+
return true
|
|
3510
|
+
}
|
|
3511
|
+
return false
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
/**
|
|
3515
|
+
* Checks if the given label is admin-reserved per BRC-100 rules:
|
|
3516
|
+
*
|
|
3517
|
+
* - Must not start with `admin` (admin-reserved)
|
|
3518
|
+
*
|
|
3519
|
+
* If it violates these rules and the caller is not admin, we consider it "admin-only."
|
|
3520
|
+
*/
|
|
3521
|
+
private isAdminLabel(label: string): boolean {
|
|
3522
|
+
if (label.startsWith('admin')) {
|
|
3523
|
+
return true
|
|
3524
|
+
}
|
|
3525
|
+
return false
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
/**
|
|
3529
|
+
* Checks if the given basket is admin-reserved per BRC-100 rules:
|
|
3530
|
+
*
|
|
3531
|
+
* - Must not start with `admin`
|
|
3532
|
+
* - Must not be `default` (some wallets use this for internal operations)
|
|
3533
|
+
* - Must not start with `p ` (future specially permissioned baskets)
|
|
3534
|
+
*/
|
|
3535
|
+
private isAdminBasket(basket: string): boolean {
|
|
3536
|
+
if (basket === 'default') return true
|
|
3537
|
+
if (basket.startsWith('admin')) return true
|
|
3538
|
+
return false
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
/**
|
|
3542
|
+
* Returns true if we have a cached record that the permission identified by
|
|
3543
|
+
* `key` is valid and unexpired.
|
|
3544
|
+
*/
|
|
3545
|
+
private isPermissionCached(key: string): boolean {
|
|
3546
|
+
const entry = this.permissionCache.get(key)
|
|
3547
|
+
if (!entry) return false
|
|
3548
|
+
if (Date.now() - entry.cachedAt > WalletPermissionsManager.CACHE_TTL_MS) {
|
|
3549
|
+
this.permissionCache.delete(key)
|
|
3550
|
+
return false
|
|
3551
|
+
}
|
|
3552
|
+
if (this.isTokenExpired(entry.expiry)) {
|
|
3553
|
+
this.permissionCache.delete(key)
|
|
3554
|
+
return false
|
|
3555
|
+
}
|
|
3556
|
+
return true
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
/** Caches the fact that the permission for `key` is valid until `expiry`. */
|
|
3560
|
+
private cachePermission(key: string, expiry: number): void {
|
|
3561
|
+
this.permissionCache.set(key, { expiry, cachedAt: Date.now() })
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
/** Records that a non-spending permission was just granted so we can skip re-prompting briefly. */
|
|
3565
|
+
private markRecentGrant(request: PermissionRequest): void {
|
|
3566
|
+
if (request.type === 'spending') return
|
|
3567
|
+
const key = this.buildRequestKey(request)
|
|
3568
|
+
if (!key) return
|
|
3569
|
+
this.recentGrants.set(key, Date.now() + WalletPermissionsManager.RECENT_GRANT_COVER_MS)
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
/** Returns true if we are inside the short "cover window" immediately after granting permission. */
|
|
3573
|
+
private isRecentlyGranted(key: string): boolean {
|
|
3574
|
+
const expiry = this.recentGrants.get(key)
|
|
3575
|
+
if (!expiry) return false
|
|
3576
|
+
if (Date.now() > expiry) {
|
|
3577
|
+
this.recentGrants.delete(key)
|
|
3578
|
+
return false
|
|
3579
|
+
}
|
|
3580
|
+
return true
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
/** Normalizes and canonicalizes originator domains (e.g., lowercase + drop default ports). */
|
|
3584
|
+
private normalizeOriginator(originator?: string): string {
|
|
3585
|
+
if (!originator) return ''
|
|
3586
|
+
const trimmed = originator.trim()
|
|
3587
|
+
if (!trimmed) {
|
|
3588
|
+
return ''
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
try {
|
|
3592
|
+
const hasScheme = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(trimmed)
|
|
3593
|
+
const candidate = hasScheme ? trimmed : `https://${trimmed}`
|
|
3594
|
+
const url = new URL(candidate)
|
|
3595
|
+
if (!url.hostname) {
|
|
3596
|
+
return trimmed.toLowerCase()
|
|
3597
|
+
}
|
|
3598
|
+
const hostname = url.hostname.toLowerCase()
|
|
3599
|
+
const needsBrackets = hostname.includes(':')
|
|
3600
|
+
const baseHost = needsBrackets ? `[${hostname}]` : hostname
|
|
3601
|
+
const port = url.port
|
|
3602
|
+
const defaultPort = WalletPermissionsManager.DEFAULT_PORTS[url.protocol]
|
|
3603
|
+
if (port && defaultPort && port === defaultPort) {
|
|
3604
|
+
return baseHost
|
|
3605
|
+
}
|
|
3606
|
+
return port ? `${baseHost}:${port}` : baseHost
|
|
3607
|
+
} catch {
|
|
3608
|
+
// Fall back to a conservative lowercase trim if URL parsing fails.
|
|
3609
|
+
return trimmed.toLowerCase()
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
/**
|
|
3614
|
+
* Produces a normalized originator value along with the set of legacy
|
|
3615
|
+
* representations that should be considered when searching for existing
|
|
3616
|
+
* permission tokens (for backwards compatibility).
|
|
3617
|
+
*/
|
|
3618
|
+
private prepareOriginator(originator?: string): { normalized: string; lookupValues: string[] } {
|
|
3619
|
+
const trimmed = originator?.trim()
|
|
3620
|
+
if (!trimmed) {
|
|
3621
|
+
throw new Error('Originator is required for permission checks.')
|
|
3622
|
+
}
|
|
3623
|
+
const normalized = this.normalizeOriginator(trimmed) || trimmed.toLowerCase()
|
|
3624
|
+
const lookupValues = Array.from(new Set([trimmed, normalized])).filter(Boolean)
|
|
3625
|
+
return { normalized, lookupValues }
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
/**
|
|
3629
|
+
* Builds a unique list of originator variants that should be searched when
|
|
3630
|
+
* looking up on-chain tokens (e.g., legacy raw + normalized forms).
|
|
3631
|
+
*/
|
|
3632
|
+
private buildOriginatorLookupValues(...origins: Array<string | undefined>): string[] {
|
|
3633
|
+
const variants = new Set<string>()
|
|
3634
|
+
for (const origin of origins) {
|
|
3635
|
+
const trimmed = origin?.trim()
|
|
3636
|
+
if (trimmed) {
|
|
3637
|
+
variants.add(trimmed)
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
return Array.from(variants)
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
/**
|
|
3644
|
+
* Builds a "map key" string so that identical requests (e.g. "protocol:domain:true:protoName:counterparty")
|
|
3645
|
+
* do not produce multiple user prompts.
|
|
3646
|
+
*/
|
|
3647
|
+
private buildRequestKey(r: PermissionRequest): string {
|
|
3648
|
+
const normalizedOriginator = this.normalizeOriginator(r.originator)
|
|
3649
|
+
switch (r.type) {
|
|
3650
|
+
case 'protocol':
|
|
3651
|
+
return `proto:${normalizedOriginator}:${!!r.privileged}:${r.protocolID?.join(',')}:${r.counterparty}`
|
|
3652
|
+
case 'basket':
|
|
3653
|
+
return `basket:${normalizedOriginator}:${r.basket}`
|
|
3654
|
+
case 'certificate':
|
|
3655
|
+
return `cert:${normalizedOriginator}:${!!r.privileged}:${r.certificate?.verifier}:${r.certificate?.certType}:${r.certificate?.fields.join('|')}`
|
|
3656
|
+
case 'spending':
|
|
3657
|
+
return `spend:${normalizedOriginator}:${r.spending?.satoshis}`
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
}
|