@bsv/wallet-toolbox 1.1.25 → 1.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/docs/client.md +78 -2313
  2. package/docs/setup.md +15 -19
  3. package/docs/wallet.md +78 -2313
  4. package/out/src/Setup.d.ts +4 -0
  5. package/out/src/Setup.d.ts.map +1 -1
  6. package/out/src/Setup.js +19 -9
  7. package/out/src/Setup.js.map +1 -1
  8. package/out/src/Wallet.d.ts +1 -6
  9. package/out/src/Wallet.d.ts.map +1 -1
  10. package/out/src/Wallet.js +2 -29
  11. package/out/src/Wallet.js.map +1 -1
  12. package/out/src/index.all.d.ts +0 -9
  13. package/out/src/index.all.d.ts.map +1 -1
  14. package/out/src/index.all.js +0 -9
  15. package/out/src/index.all.js.map +1 -1
  16. package/out/src/index.client.d.ts +0 -9
  17. package/out/src/index.client.d.ts.map +1 -1
  18. package/out/src/index.client.js +0 -9
  19. package/out/src/index.client.js.map +1 -1
  20. package/out/src/storage/WalletStorageManager.d.ts.map +1 -1
  21. package/out/src/storage/WalletStorageManager.js +2 -0
  22. package/out/src/storage/WalletStorageManager.js.map +1 -1
  23. package/out/test/examples/backup.test.d.ts +14 -0
  24. package/out/test/examples/backup.test.d.ts.map +1 -0
  25. package/out/test/examples/backup.test.js +59 -0
  26. package/out/test/examples/backup.test.js.map +1 -0
  27. package/out/test/wallet/action/abortAction.test.d.ts.map +1 -0
  28. package/out/test/{Wallet → wallet}/action/abortAction.test.js.map +1 -1
  29. package/out/test/wallet/action/createAction.test.d.ts.map +1 -0
  30. package/out/test/{Wallet → wallet}/action/createAction.test.js.map +1 -1
  31. package/out/test/{Wallet → wallet}/action/createAction2.test.d.ts.map +1 -1
  32. package/out/test/{Wallet → wallet}/action/createAction2.test.js.map +1 -1
  33. package/out/test/wallet/action/createActionToGenerateBeefs.man.test.d.ts.map +1 -0
  34. package/out/test/{Wallet → wallet}/action/createActionToGenerateBeefs.man.test.js.map +1 -1
  35. package/out/test/wallet/action/internalizeAction.test.d.ts.map +1 -0
  36. package/out/test/{Wallet → wallet}/action/internalizeAction.test.js.map +1 -1
  37. package/out/test/wallet/action/relinquishOutput.test.d.ts.map +1 -0
  38. package/out/test/{Wallet → wallet}/action/relinquishOutput.test.js.map +1 -1
  39. package/out/test/wallet/construct/Wallet.constructor.test.d.ts.map +1 -0
  40. package/out/test/{Wallet → wallet}/construct/Wallet.constructor.test.js.map +1 -1
  41. package/out/test/wallet/list/listActions.test.d.ts.map +1 -0
  42. package/out/test/{Wallet → wallet}/list/listActions.test.js.map +1 -1
  43. package/out/test/wallet/list/listActions2.test.d.ts.map +1 -0
  44. package/out/test/{Wallet → wallet}/list/listActions2.test.js.map +1 -1
  45. package/out/test/wallet/list/listCertificates.test.d.ts.map +1 -0
  46. package/out/test/{Wallet → wallet}/list/listCertificates.test.js.map +1 -1
  47. package/out/test/wallet/list/listOutputs.test.d.ts.map +1 -0
  48. package/out/test/{Wallet → wallet}/list/listOutputs.test.js.map +1 -1
  49. package/out/test/wallet/sync/Wallet.sync.test.d.ts.map +1 -0
  50. package/out/test/{Wallet → wallet}/sync/Wallet.sync.test.js.map +1 -1
  51. package/out/tsconfig.all.tsbuildinfo +1 -1
  52. package/package.json +3 -3
  53. package/src/Setup.ts +22 -9
  54. package/src/Wallet.ts +3 -47
  55. package/src/index.all.ts +0 -9
  56. package/src/index.client.ts +0 -9
  57. package/src/storage/WalletStorageManager.ts +1 -0
  58. package/test/examples/backup.test.ts +66 -0
  59. package/out/src/CWIStyleWalletManager.d.ts +0 -411
  60. package/out/src/CWIStyleWalletManager.d.ts.map +0 -1
  61. package/out/src/CWIStyleWalletManager.js +0 -1131
  62. package/out/src/CWIStyleWalletManager.js.map +0 -1
  63. package/out/src/SetupClient.d.ts +0 -249
  64. package/out/src/SetupClient.d.ts.map +0 -1
  65. package/out/src/SetupClient.js +0 -252
  66. package/out/src/SetupClient.js.map +0 -1
  67. package/out/src/SimpleWalletManager.d.ts +0 -169
  68. package/out/src/SimpleWalletManager.d.ts.map +0 -1
  69. package/out/src/SimpleWalletManager.js +0 -315
  70. package/out/src/SimpleWalletManager.js.map +0 -1
  71. package/out/src/WalletAuthenticationManager.d.ts +0 -33
  72. package/out/src/WalletAuthenticationManager.d.ts.map +0 -1
  73. package/out/src/WalletAuthenticationManager.js +0 -107
  74. package/out/src/WalletAuthenticationManager.js.map +0 -1
  75. package/out/src/WalletPermissionsManager.d.ts +0 -575
  76. package/out/src/WalletPermissionsManager.d.ts.map +0 -1
  77. package/out/src/WalletPermissionsManager.js +0 -1807
  78. package/out/src/WalletPermissionsManager.js.map +0 -1
  79. package/out/src/WalletSettingsManager.d.ts +0 -59
  80. package/out/src/WalletSettingsManager.d.ts.map +0 -1
  81. package/out/src/WalletSettingsManager.js +0 -168
  82. package/out/src/WalletSettingsManager.js.map +0 -1
  83. package/out/src/__tests/CWIStyleWalletManager.test.d.ts +0 -2
  84. package/out/src/__tests/CWIStyleWalletManager.test.d.ts.map +0 -1
  85. package/out/src/__tests/CWIStyleWalletManager.test.js +0 -472
  86. package/out/src/__tests/CWIStyleWalletManager.test.js.map +0 -1
  87. package/out/src/__tests/WalletPermissionsManager.callbacks.test.d.ts +0 -2
  88. package/out/src/__tests/WalletPermissionsManager.callbacks.test.d.ts.map +0 -1
  89. package/out/src/__tests/WalletPermissionsManager.callbacks.test.js +0 -239
  90. package/out/src/__tests/WalletPermissionsManager.callbacks.test.js.map +0 -1
  91. package/out/src/__tests/WalletPermissionsManager.checks.test.d.ts +0 -2
  92. package/out/src/__tests/WalletPermissionsManager.checks.test.d.ts.map +0 -1
  93. package/out/src/__tests/WalletPermissionsManager.checks.test.js +0 -644
  94. package/out/src/__tests/WalletPermissionsManager.checks.test.js.map +0 -1
  95. package/out/src/__tests/WalletPermissionsManager.encryption.test.d.ts +0 -2
  96. package/out/src/__tests/WalletPermissionsManager.encryption.test.d.ts.map +0 -1
  97. package/out/src/__tests/WalletPermissionsManager.encryption.test.js +0 -295
  98. package/out/src/__tests/WalletPermissionsManager.encryption.test.js.map +0 -1
  99. package/out/src/__tests/WalletPermissionsManager.fixtures.d.ts +0 -82
  100. package/out/src/__tests/WalletPermissionsManager.fixtures.d.ts.map +0 -1
  101. package/out/src/__tests/WalletPermissionsManager.fixtures.js +0 -260
  102. package/out/src/__tests/WalletPermissionsManager.fixtures.js.map +0 -1
  103. package/out/src/__tests/WalletPermissionsManager.flows.test.d.ts +0 -2
  104. package/out/src/__tests/WalletPermissionsManager.flows.test.d.ts.map +0 -1
  105. package/out/src/__tests/WalletPermissionsManager.flows.test.js +0 -389
  106. package/out/src/__tests/WalletPermissionsManager.flows.test.js.map +0 -1
  107. package/out/src/__tests/WalletPermissionsManager.initialization.test.d.ts +0 -2
  108. package/out/src/__tests/WalletPermissionsManager.initialization.test.d.ts.map +0 -1
  109. package/out/src/__tests/WalletPermissionsManager.initialization.test.js +0 -227
  110. package/out/src/__tests/WalletPermissionsManager.initialization.test.js.map +0 -1
  111. package/out/src/__tests/WalletPermissionsManager.proxying.test.d.ts +0 -2
  112. package/out/src/__tests/WalletPermissionsManager.proxying.test.d.ts.map +0 -1
  113. package/out/src/__tests/WalletPermissionsManager.proxying.test.js +0 -566
  114. package/out/src/__tests/WalletPermissionsManager.proxying.test.js.map +0 -1
  115. package/out/src/__tests/WalletPermissionsManager.tokens.test.d.ts +0 -2
  116. package/out/src/__tests/WalletPermissionsManager.tokens.test.d.ts.map +0 -1
  117. package/out/src/__tests/WalletPermissionsManager.tokens.test.js +0 -460
  118. package/out/src/__tests/WalletPermissionsManager.tokens.test.js.map +0 -1
  119. package/out/src/utility/identityUtils.d.ts +0 -31
  120. package/out/src/utility/identityUtils.d.ts.map +0 -1
  121. package/out/src/utility/identityUtils.js +0 -114
  122. package/out/src/utility/identityUtils.js.map +0 -1
  123. package/out/src/wab-client/WABClient.d.ts +0 -38
  124. package/out/src/wab-client/WABClient.d.ts.map +0 -1
  125. package/out/src/wab-client/WABClient.js +0 -95
  126. package/out/src/wab-client/WABClient.js.map +0 -1
  127. package/out/src/wab-client/__tests/WABClient.test.d.ts +0 -2
  128. package/out/src/wab-client/__tests/WABClient.test.d.ts.map +0 -1
  129. package/out/src/wab-client/__tests/WABClient.test.js +0 -47
  130. package/out/src/wab-client/__tests/WABClient.test.js.map +0 -1
  131. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts +0 -34
  132. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts.map +0 -1
  133. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js +0 -16
  134. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js.map +0 -1
  135. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.d.ts +0 -7
  136. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.d.ts.map +0 -1
  137. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.js +0 -40
  138. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.js.map +0 -1
  139. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts +0 -28
  140. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts.map +0 -1
  141. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js +0 -73
  142. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js.map +0 -1
  143. package/out/test/Wallet/action/abortAction.test.d.ts.map +0 -1
  144. package/out/test/Wallet/action/createAction.test.d.ts.map +0 -1
  145. package/out/test/Wallet/action/createActionToGenerateBeefs.man.test.d.ts.map +0 -1
  146. package/out/test/Wallet/action/internalizeAction.test.d.ts.map +0 -1
  147. package/out/test/Wallet/action/relinquishOutput.test.d.ts.map +0 -1
  148. package/out/test/Wallet/construct/Wallet.constructor.test.d.ts.map +0 -1
  149. package/out/test/Wallet/list/listActions.test.d.ts.map +0 -1
  150. package/out/test/Wallet/list/listActions2.test.d.ts.map +0 -1
  151. package/out/test/Wallet/list/listCertificates.test.d.ts.map +0 -1
  152. package/out/test/Wallet/list/listOutputs.test.d.ts.map +0 -1
  153. package/out/test/Wallet/sync/Wallet.sync.test.d.ts.map +0 -1
  154. package/src/CWIStyleWalletManager.ts +0 -1891
  155. package/src/SimpleWalletManager.ts +0 -553
  156. package/src/WalletAuthenticationManager.ts +0 -183
  157. package/src/WalletPermissionsManager.ts +0 -2639
  158. package/src/WalletSettingsManager.ts +0 -241
  159. package/src/__tests/CWIStyleWalletManager.test.ts +0 -709
  160. package/src/__tests/WalletPermissionsManager.callbacks.test.ts +0 -328
  161. package/src/__tests/WalletPermissionsManager.checks.test.ts +0 -857
  162. package/src/__tests/WalletPermissionsManager.encryption.test.ts +0 -407
  163. package/src/__tests/WalletPermissionsManager.fixtures.ts +0 -283
  164. package/src/__tests/WalletPermissionsManager.flows.test.ts +0 -490
  165. package/src/__tests/WalletPermissionsManager.initialization.test.ts +0 -333
  166. package/src/__tests/WalletPermissionsManager.proxying.test.ts +0 -753
  167. package/src/__tests/WalletPermissionsManager.tokens.test.ts +0 -584
  168. package/src/utility/identityUtils.ts +0 -170
  169. package/src/wab-client/WABClient.ts +0 -103
  170. package/src/wab-client/__tests/WABClient.test.ts +0 -58
  171. package/src/wab-client/auth-method-interactors/AuthMethodInteractor.ts +0 -47
  172. package/src/wab-client/auth-method-interactors/PersonaIDInteractor.ts +0 -45
  173. package/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.ts +0 -82
  174. /package/out/test/{Wallet → wallet}/action/abortAction.test.d.ts +0 -0
  175. /package/out/test/{Wallet → wallet}/action/abortAction.test.js +0 -0
  176. /package/out/test/{Wallet → wallet}/action/createAction.test.d.ts +0 -0
  177. /package/out/test/{Wallet → wallet}/action/createAction.test.js +0 -0
  178. /package/out/test/{Wallet → wallet}/action/createAction2.test.d.ts +0 -0
  179. /package/out/test/{Wallet → wallet}/action/createAction2.test.js +0 -0
  180. /package/out/test/{Wallet → wallet}/action/createActionToGenerateBeefs.man.test.d.ts +0 -0
  181. /package/out/test/{Wallet → wallet}/action/createActionToGenerateBeefs.man.test.js +0 -0
  182. /package/out/test/{Wallet → wallet}/action/internalizeAction.test.d.ts +0 -0
  183. /package/out/test/{Wallet → wallet}/action/internalizeAction.test.js +0 -0
  184. /package/out/test/{Wallet → wallet}/action/relinquishOutput.test.d.ts +0 -0
  185. /package/out/test/{Wallet → wallet}/action/relinquishOutput.test.js +0 -0
  186. /package/out/test/{Wallet → wallet}/construct/Wallet.constructor.test.d.ts +0 -0
  187. /package/out/test/{Wallet → wallet}/construct/Wallet.constructor.test.js +0 -0
  188. /package/out/test/{Wallet → wallet}/list/listActions.test.d.ts +0 -0
  189. /package/out/test/{Wallet → wallet}/list/listActions.test.js +0 -0
  190. /package/out/test/{Wallet → wallet}/list/listActions2.test.d.ts +0 -0
  191. /package/out/test/{Wallet → wallet}/list/listActions2.test.js +0 -0
  192. /package/out/test/{Wallet → wallet}/list/listCertificates.test.d.ts +0 -0
  193. /package/out/test/{Wallet → wallet}/list/listCertificates.test.js +0 -0
  194. /package/out/test/{Wallet → wallet}/list/listOutputs.test.d.ts +0 -0
  195. /package/out/test/{Wallet → wallet}/list/listOutputs.test.js +0 -0
  196. /package/out/test/{Wallet → wallet}/sync/Wallet.sync.test.d.ts +0 -0
  197. /package/out/test/{Wallet → wallet}/sync/Wallet.sync.test.js +0 -0
  198. /package/test/{Wallet → wallet}/action/abortAction.test.ts +0 -0
  199. /package/test/{Wallet → wallet}/action/createAction.test.ts +0 -0
  200. /package/test/{Wallet → wallet}/action/createAction2.test.ts +0 -0
  201. /package/test/{Wallet → wallet}/action/createActionToGenerateBeefs.man.test.ts +0 -0
  202. /package/test/{Wallet → wallet}/action/internalizeAction.test.ts +0 -0
  203. /package/test/{Wallet → wallet}/action/relinquishOutput.test.ts +0 -0
  204. /package/test/{Wallet → wallet}/construct/Wallet.constructor.test.ts +0 -0
  205. /package/test/{Wallet → wallet}/list/listActions.test.ts +0 -0
  206. /package/test/{Wallet → wallet}/list/listActions2.test.ts +0 -0
  207. /package/test/{Wallet → wallet}/list/listCertificates.test.ts +0 -0
  208. /package/test/{Wallet → wallet}/list/listOutputs.test.ts +0 -0
  209. /package/test/{Wallet → wallet}/sync/Wallet.sync.test.ts +0 -0
@@ -1,2639 +0,0 @@
1
- import {
2
- WalletInterface,
3
- Utils,
4
- PushDrop,
5
- LockingScript,
6
- Transaction
7
- } from '@bsv/sdk'
8
-
9
- ////// TODO: ADD SUPPORT FOR ADMIN COUNTERPARTIES BASED ON WALLET STORAGE
10
- ////// !!!!!!!! SECURITY-CRITICAL ADDITION — DO NOT USE UNTIL IMPLEMENTED.
11
-
12
- /**
13
- * Describes a single requested permission that the user must either grant or deny.
14
- *
15
- * Four categories of permission are supported, each with a unique protocol:
16
- * 1) protocol - "DPACP" (Domain Protocol Access Control Protocol)
17
- * 2) basket - "DBAP" (Domain Basket Access Protocol)
18
- * 3) certificate - "DCAP" (Domain Certificate Access Protocol)
19
- * 4) spending - "DSAP" (Domain Spending Authorization Protocol)
20
- *
21
- * This model underpins "requests" made to the user for permission, which the user can
22
- * either grant or deny. The manager can then create on-chain tokens (PushDrop outputs)
23
- * if permission is granted. Denying requests cause the underlying operation to throw,
24
- * and no token is created. An "ephemeral" grant is also possible, denoting a one-time
25
- * authorization without an associated persistent on-chain token.
26
- */
27
- export interface PermissionRequest {
28
- type: 'protocol' | 'basket' | 'certificate' | 'spending'
29
- originator: string // The domain or FQDN of the requesting application
30
- privileged?: boolean // For "protocol" or "certificate" usage, indicating privileged key usage
31
- protocolID?: [0 | 1 | 2, string] // For type='protocol': BRC-43 style (securityLevel, protocolName)
32
- counterparty?: string // For type='protocol': e.g. target public key or "self"/"anyone"
33
-
34
- basket?: string // For type='basket': the basket name being requested
35
-
36
- certificate?: {
37
- // For type='certificate': details about the cert usage
38
- verifier: string
39
- certType: string
40
- fields: string[]
41
- }
42
-
43
- spending?: {
44
- // For type='spending': details about the requested spend
45
- satoshis: number
46
- lineItems?: Array<{
47
- type: 'input' | 'output' | 'fee'
48
- description: string
49
- satoshis: number
50
- }>
51
- }
52
-
53
- reason?: string // Human-readable explanation for requesting permission
54
- renewal?: boolean // Whether this request is for renewing an expired token
55
- previousToken?: PermissionToken // If renewing an expired permission, reference to the old token
56
- }
57
-
58
- /**
59
- * Signature for functions that handle a permission request event, e.g. "Please ask the user to allow basket X".
60
- */
61
- export type PermissionEventHandler = (
62
- request: PermissionRequest & { requestID: string }
63
- ) => void | Promise<void>
64
-
65
- /**
66
- * Data structure representing an on-chain permission token.
67
- * It is typically stored as a single unspent PushDrop output in a special "internal" admin basket belonging to
68
- * the user, held in their underlying wallet.
69
- *
70
- * It can represent any of the four permission categories by having the relevant fields:
71
- * - DPACP: originator, privileged, protocol, securityLevel, counterparty
72
- * - DBAP: originator, basketName
73
- * - DCAP: originator, privileged, verifier, certType, certFields
74
- * - DSAP: originator, authorizedAmount
75
- */
76
- export interface PermissionToken {
77
- /** The transaction ID where this token resides. */
78
- txid: string
79
-
80
- /** The output index within that transaction. */
81
- outputIndex: number
82
-
83
- /** The exact script hex for the locking script. */
84
- outputScript: string
85
-
86
- /** The amount of satoshis assigned to the permission output (often 1). */
87
- satoshis: number
88
-
89
- /** The originator domain or FQDN that is allowed to use this permission. */
90
- originator: string
91
-
92
- /** The expiration time for this token in UNIX epoch seconds. (0 or omitted for spending authorizations, which are indefinite) */
93
- expiry: number
94
-
95
- /** Whether this token grants privileged usage (for protocol or certificate). */
96
- privileged?: boolean
97
-
98
- /** The protocol name, if this is a DPACP token. */
99
- protocol?: string
100
-
101
- /** The security level (0,1,2) for DPACP. */
102
- securityLevel?: 0 | 1 | 2
103
-
104
- /** The counterparty, for DPACP. */
105
- counterparty?: string
106
-
107
- /** The name of a basket, if this is a DBAP token. */
108
- basketName?: string
109
-
110
- /** The certificate type, if this is a DCAP token. */
111
- certType?: string
112
-
113
- /** The certificate fields that this token covers, if DCAP token. */
114
- certFields?: string[]
115
-
116
- /** The "verifier" public key string, if DCAP. */
117
- verifier?: string
118
-
119
- /** For DSAP, the maximum authorized spending for the month. */
120
- authorizedAmount?: number
121
- }
122
-
123
- /**
124
- * A map from each permission type to a special "admin basket" name used for storing
125
- * the tokens. The tokens themselves are unspent transaction outputs (UTXOs) with a
126
- * specialized PushDrop script that references the originator, expiry, etc.
127
- */
128
- const BASKET_MAP = {
129
- protocol: 'admin protocol-permission',
130
- basket: 'admin basket-access',
131
- certificate: 'admin certificate-access',
132
- spending: 'admin spending-authorization'
133
- }
134
-
135
- /**
136
- * The set of callbacks that external code can bind to, e.g. to display UI prompts or logs
137
- * when a permission is requested.
138
- */
139
- export interface WalletPermissionsManagerCallbacks {
140
- onProtocolPermissionRequested?: PermissionEventHandler[]
141
- onBasketAccessRequested?: PermissionEventHandler[]
142
- onCertificateAccessRequested?: PermissionEventHandler[]
143
- onSpendingAuthorizationRequested?: PermissionEventHandler[]
144
- }
145
-
146
- /**
147
- * Configuration object for the WalletPermissionsManager. If a given option is `false`,
148
- * the manager will skip or alter certain permission checks or behaviors.
149
- *
150
- * By default, all of these are `true` unless specified otherwise. This is the most secure configuration.
151
- */
152
- export interface PermissionsManagerConfig {
153
- /**
154
- * For `createSignature` and `verifySignature`,
155
- * require a "protocol usage" permission check?
156
- */
157
- seekProtocolPermissionsForSigning?: boolean
158
-
159
- /**
160
- * For methods that perform encryption (encrypt/decrypt), require
161
- * a "protocol usage" permission check?
162
- */
163
- seekProtocolPermissionsForEncrypting?: boolean
164
-
165
- /**
166
- * For methods that perform HMAC creation or verification (createHmac, verifyHmac),
167
- * require a "protocol usage" permission check?
168
- */
169
- seekProtocolPermissionsForHMAC?: boolean
170
-
171
- /**
172
- * For revealing counterparty-level or specific key linkage revelation information,
173
- * should we require permission?
174
- */
175
- seekPermissionsForKeyLinkageRevelation?: boolean
176
-
177
- /**
178
- * For revealing any user public key (getPublicKey) **other** than the identity key,
179
- * should we require permission?
180
- */
181
- seekPermissionsForPublicKeyRevelation?: boolean
182
-
183
- /**
184
- * If getPublicKey is requested with `identityKey=true`, do we require permission?
185
- */
186
- seekPermissionsForIdentityKeyRevelation?: boolean
187
-
188
- /**
189
- * If discoverByIdentityKey / discoverByAttributes are called, do we require permission
190
- * for "identity resolution" usage?
191
- */
192
- seekPermissionsForIdentityResolution?: boolean
193
-
194
- /**
195
- * When we do internalizeAction with `basket insertion`, or include outputs in baskets
196
- * with `createAction, do we ask for basket permission?
197
- */
198
- seekBasketInsertionPermissions?: boolean
199
-
200
- /**
201
- * When relinquishOutput is called, do we ask for basket permission?
202
- */
203
- seekBasketRemovalPermissions?: boolean
204
-
205
- /**
206
- * When listOutputs is called, do we ask for basket permission?
207
- */
208
- seekBasketListingPermissions?: boolean
209
-
210
- /**
211
- * When createAction is called with labels, do we ask for "label usage" permission?
212
- */
213
- seekPermissionWhenApplyingActionLabels?: boolean
214
-
215
- /**
216
- * When listActions is called with labels, do we ask for "label usage" permission?
217
- */
218
- seekPermissionWhenListingActionsByLabel?: boolean
219
-
220
- /**
221
- * If proving a certificate (proveCertificate) or revealing certificate fields,
222
- * do we require a "certificate access" permission?
223
- */
224
- seekCertificateDisclosurePermissions?: boolean
225
-
226
- /**
227
- * If acquiring a certificate (acquireCertificate), do we require a permission check?
228
- */
229
- seekCertificateAcquisitionPermissions?: boolean
230
-
231
- /**
232
- * If relinquishing a certificate (relinquishCertificate), do we require a permission check?
233
- */
234
- seekCertificateRelinquishmentPermissions?: boolean
235
-
236
- /**
237
- * If listing a user's certificates (listCertificates), do we require a permission check?
238
- */
239
- seekCertificateListingPermissions?: boolean
240
-
241
- /**
242
- * Should transaction descriptions, input descriptions, and output descriptions be encrypted
243
- * when before they are passed to the underlying wallet, and transparently decrypted when retrieved?
244
- */
245
- encryptWalletMetadata?: boolean
246
-
247
- /**
248
- * If the originator tries to spend wallet funds (netSpent > 0 in createAction),
249
- * do we seek spending authorization?
250
- */
251
- seekSpendingPermissions?: boolean
252
-
253
- /**
254
- * If false, permissions are checked without regard for whether we are in
255
- * privileged mode. Privileged status is ignored with respect to whether
256
- * permissions are granted. Internally, they are always sought and checked
257
- * with privileged=false, regardless of the actual value.
258
- */
259
- differentiatePrivilegedOperations?: boolean
260
- }
261
-
262
- /**
263
- * @class WalletPermissionsManager
264
- *
265
- * Wraps an underlying BRC-100 `Wallet` implementation with permissions management capabilities.
266
- * The manager intercepts calls from external applications (identified by originators), checks if the request is allowed,
267
- * and if not, orchestrates user permission flows. It creates or renews on-chain tokens in special
268
- * admin baskets to track these authorizations. Finally, it proxies the actual call to the underlying wallet.
269
- *
270
- * ### Key Responsibilities:
271
- * - **Permission Checking**: Before standard wallet operations (e.g. `encrypt`),
272
- * the manager checks if a valid permission token exists. If not, it attempts to request permission from the user.
273
- * - **On-Chain Tokens**: When permission is granted, the manager stores it as an unspent "PushDrop" output.
274
- * This can be spent later to revoke or renew the permission.
275
- * - **Callbacks**: The manager triggers user-defined callbacks on permission requests (to show a UI prompt),
276
- * on grants/denials, and on internal processes.
277
- *
278
- * ### Implementation Notes:
279
- * - The manager follows the BRC-100 `createAction` + `signAction` pattern for building or spending these tokens.
280
- * - Token revocation or renewal uses standard BRC-100 flows: we build a transaction that consumes
281
- * the old token UTXO and outputs a new one (or none, if fully revoked).
282
- */
283
- export class WalletPermissionsManager implements WalletInterface {
284
- /** A reference to the BRC-100 wallet instance. */
285
- private underlying: WalletInterface
286
-
287
- /** The "admin" domain or FQDN that is implicitly allowed to do everything. */
288
- private adminOriginator: string
289
-
290
- /**
291
- * Event callbacks that external code can subscribe to, e.g. to show a UI prompt
292
- * or log events. Each event can have multiple handlers.
293
- */
294
- private callbacks: WalletPermissionsManagerCallbacks = {
295
- onProtocolPermissionRequested: [],
296
- onBasketAccessRequested: [],
297
- onCertificateAccessRequested: [],
298
- onSpendingAuthorizationRequested: []
299
- }
300
-
301
- /**
302
- * We queue parallel requests for the same resource so that only one
303
- * user prompt is created for a single resource. If multiple calls come
304
- * in at once for the same "protocol:domain:privileged:counterparty" etc.,
305
- * they get merged.
306
- *
307
- * The key is a string derived from the operation; the value is an object with a reference to the
308
- * associated request and an array of pending promise resolve/reject pairs, one for each active
309
- * operation that's waiting on the particular resource described by the key.
310
- */
311
- private activeRequests: Map<
312
- string,
313
- {
314
- request: PermissionRequest
315
- pending: Array<{
316
- resolve: (val: boolean) => void
317
- reject: (err: any) => void
318
- }>
319
- }
320
- > = new Map()
321
-
322
- /**
323
- * Configuration that determines whether to skip or apply various checks and encryption.
324
- */
325
- private config: PermissionsManagerConfig
326
-
327
- /**
328
- * Constructs a new Permissions Manager instance.
329
- *
330
- * @param underlyingWallet The underlying BRC-100 wallet, where requests are forwarded after permission is granted
331
- * @param adminOriginator The domain or FQDN that is automatically allowed everything
332
- * @param config A set of boolean flags controlling how strictly permissions are enforced
333
- */
334
- constructor(
335
- underlyingWallet: WalletInterface,
336
- adminOriginator: string,
337
- config: PermissionsManagerConfig = {}
338
- ) {
339
- this.underlying = underlyingWallet
340
- this.adminOriginator = adminOriginator
341
-
342
- // Default all config options to true unless specified
343
- this.config = {
344
- seekProtocolPermissionsForSigning: true,
345
- seekProtocolPermissionsForEncrypting: true,
346
- seekProtocolPermissionsForHMAC: true,
347
- seekPermissionsForKeyLinkageRevelation: true,
348
- seekPermissionsForPublicKeyRevelation: true,
349
- seekPermissionsForIdentityKeyRevelation: true,
350
- seekPermissionsForIdentityResolution: true,
351
- seekBasketInsertionPermissions: true,
352
- seekBasketRemovalPermissions: true,
353
- seekBasketListingPermissions: true,
354
- seekPermissionWhenApplyingActionLabels: true,
355
- seekPermissionWhenListingActionsByLabel: true,
356
- seekCertificateDisclosurePermissions: true,
357
- seekCertificateAcquisitionPermissions: true,
358
- seekCertificateRelinquishmentPermissions: true,
359
- seekCertificateListingPermissions: true,
360
- encryptWalletMetadata: true,
361
- seekSpendingPermissions: true,
362
- differentiatePrivilegedOperations: true,
363
- ...config // override with user-specified config
364
- }
365
- }
366
-
367
- /* ---------------------------------------------------------------------
368
- * 1) PUBLIC API FOR REGISTERING CALLBACKS (UI PROMPTS, LOGGING, ETC.)
369
- * --------------------------------------------------------------------- */
370
-
371
- /**
372
- * Binds a callback function to a named event, such as `onProtocolPermissionRequested`.
373
- *
374
- * @param eventName The name of the event to listen to
375
- * @param handler A function that handles the event
376
- * @returns A numeric ID you can use to unbind later
377
- */
378
- public bindCallback(
379
- eventName: keyof WalletPermissionsManagerCallbacks,
380
- handler: PermissionEventHandler
381
- ): number {
382
- const arr = this.callbacks[eventName]!
383
- arr.push(handler)
384
- return arr.length - 1
385
- }
386
-
387
- /**
388
- * Unbinds a previously registered callback by either its numeric ID (returned by `bindCallback`)
389
- * or by exact function reference.
390
- *
391
- * @param eventName The event name, e.g. "onProtocolPermissionRequested"
392
- * @param reference Either the numeric ID or the function reference
393
- * @returns True if successfully unbound, false otherwise
394
- */
395
- public unbindCallback(
396
- eventName: keyof WalletPermissionsManagerCallbacks,
397
- reference: number | Function
398
- ): boolean {
399
- if (!this.callbacks[eventName]) return false
400
- const arr = this.callbacks[eventName] as any[]
401
- if (typeof reference === 'number') {
402
- if (arr[reference]) {
403
- arr[reference] = null
404
- return true
405
- }
406
- return false
407
- } else {
408
- const index = arr.indexOf(reference)
409
- if (index !== -1) {
410
- arr[index] = null
411
- return true
412
- }
413
- return false
414
- }
415
- }
416
-
417
- /**
418
- * Internally triggers a named event, calling all subscribed listeners.
419
- * Each callback is awaited in turn (though errors are swallowed so that
420
- * one failing callback doesn't prevent the others).
421
- *
422
- * @param eventName The event name
423
- * @param param The parameter object passed to all listeners
424
- */
425
- private async callEvent(
426
- eventName: keyof WalletPermissionsManagerCallbacks,
427
- param: any
428
- ): Promise<void> {
429
- const arr = this.callbacks[eventName] || []
430
- for (const cb of arr) {
431
- if (typeof cb === 'function') {
432
- try {
433
- await cb(param)
434
- } catch (e) {
435
- // Intentionally swallow errors from user-provided callbacks
436
- }
437
- }
438
- }
439
- }
440
-
441
- /* ---------------------------------------------------------------------
442
- * 2) PERMISSION (GRANT / DENY) METHODS
443
- * --------------------------------------------------------------------- */
444
-
445
- /**
446
- * Grants a previously requested permission.
447
- * This method:
448
- * 1) Resolves all pending promise calls waiting on this request
449
- * 2) Optionally creates or renews an on-chain PushDrop token (unless `ephemeral===true`)
450
- *
451
- * @param params requestID to identify which request is granted, plus optional expiry
452
- * or `ephemeral` usage, etc.
453
- */
454
- public async grantPermission(params: {
455
- requestID: string
456
- expiry?: number
457
- ephemeral?: boolean
458
- amount?: number
459
- }): Promise<void> {
460
- // 1) Identify the matching queued requests in `activeRequests`
461
- const matching = this.activeRequests.get(params.requestID)
462
- if (!matching) {
463
- throw new Error('Request ID not found.')
464
- }
465
-
466
- // 2) Mark all matching requests as resolved, deleting the entry
467
- for (const x of matching.pending) {
468
- x.resolve(true)
469
- }
470
- this.activeRequests.delete(params.requestID)
471
-
472
- // 3) If `ephemeral !== true`, we create or renew an on-chain token
473
- if (!params.ephemeral) {
474
- if (!matching.request.renewal) {
475
- // brand-new permission token
476
- await this.createPermissionOnChain(
477
- matching.request,
478
- params.expiry || Math.floor(Date.now() / 1000) + 3600 * 24 * 30, // default 30-day expiry
479
- params.amount
480
- )
481
- } else {
482
- // renewal => spend the old token, produce a new one
483
- await this.renewPermissionOnChain(
484
- matching.request.previousToken!,
485
- matching.request,
486
- params.expiry || Math.floor(Date.now() / 1000) + 3600 * 24 * 30,
487
- params.amount
488
- )
489
- }
490
- }
491
- }
492
-
493
- /**
494
- * Denies a previously requested permission.
495
- * This method rejects all pending promise calls waiting on that request
496
- *
497
- * @param requestID requestID identifying which request to deny
498
- */
499
- public async denyPermission(requestID: string): Promise<void> {
500
- // 1) Identify the matching requests
501
- const matching = this.activeRequests.get(requestID)
502
- if (!matching) {
503
- throw new Error('Request ID not found.')
504
- }
505
-
506
- // 2) Reject all matching requests, deleting the entry
507
- for (const x of matching.pending) {
508
- x.reject(new Error('Permission denied.'))
509
- }
510
- this.activeRequests.delete(requestID)
511
- }
512
-
513
- /* ---------------------------------------------------------------------
514
- * 3) THE "ENSURE" METHODS: CHECK IF PERMISSION EXISTS, OTHERWISE PROMPT
515
- * --------------------------------------------------------------------- */
516
-
517
- /**
518
- * Ensures the originator has protocol usage permission.
519
- * If no valid (unexpired) permission token is found, triggers a permission request flow.
520
- */
521
- public async ensureProtocolPermission({
522
- originator,
523
- privileged,
524
- protocolID,
525
- counterparty,
526
- reason,
527
- seekPermission = true,
528
- usageType
529
- }: {
530
- originator: string
531
- privileged: boolean
532
- protocolID: [0 | 1 | 2, string]
533
- counterparty: string
534
- reason?: string
535
- seekPermission?: boolean
536
- usageType:
537
- | 'signing'
538
- | 'encrypting'
539
- | 'hmac'
540
- | 'publicKey'
541
- | 'identityKey'
542
- | 'linkageRevelation'
543
- | 'generic'
544
- }): Promise<boolean> {
545
- // 1) adminOriginator can do anything
546
- if (this.isAdminOriginator(originator)) return true
547
-
548
- // 2) If security level=0, we consider it "open" usage
549
- const [level, protoName] = protocolID
550
- if (level === 0) return true
551
-
552
- // 3) If protocol is admin-reserved, block
553
- if (this.isAdminProtocol(protocolID)) {
554
- throw new Error(`Protocol “${protoName}” is admin-only.`)
555
- }
556
-
557
- // Allow the configured exceptions.
558
- if (
559
- usageType === 'signing' &&
560
- !this.config.seekProtocolPermissionsForSigning
561
- ) {
562
- return true
563
- }
564
- if (
565
- usageType === 'encrypting' &&
566
- !this.config.seekProtocolPermissionsForEncrypting
567
- ) {
568
- return true
569
- }
570
- if (usageType === 'hmac' && !this.config.seekProtocolPermissionsForHMAC) {
571
- return true
572
- }
573
- if (
574
- usageType === 'publicKey' &&
575
- !this.config.seekPermissionsForPublicKeyRevelation
576
- ) {
577
- return true
578
- }
579
- if (
580
- usageType === 'identityKey' &&
581
- !this.config.seekPermissionsForIdentityKeyRevelation
582
- ) {
583
- return true
584
- }
585
- if (
586
- usageType === 'linkageRevelation' &&
587
- !this.config.seekPermissionsForKeyLinkageRevelation
588
- ) {
589
- return true
590
- }
591
- if (!this.config.differentiatePrivilegedOperations) {
592
- privileged = false
593
- }
594
-
595
- // 4) Attempt to find a valid token in the internal basket
596
- const token = await this.findProtocolToken(
597
- originator,
598
- privileged,
599
- protocolID,
600
- counterparty,
601
- /*includeExpired=*/ true
602
- )
603
- if (token) {
604
- if (!this.isTokenExpired(token.expiry)) {
605
- // valid and unexpired
606
- return true
607
- } else {
608
- // has a token but expired => request renewal if allowed
609
- if (!seekPermission) {
610
- throw new Error(
611
- `Protocol permission expired and no further user consent allowed (seekPermission=false).`
612
- )
613
- }
614
- return await this.requestPermissionFlow({
615
- type: 'protocol',
616
- originator,
617
- privileged,
618
- protocolID,
619
- counterparty,
620
- reason,
621
- renewal: true,
622
- previousToken: token
623
- })
624
- }
625
- } else {
626
- // No token found => request a new one if allowed
627
- if (!seekPermission) {
628
- throw new Error(
629
- `No protocol permission token found (seekPermission=false).`
630
- )
631
- }
632
- return await this.requestPermissionFlow({
633
- type: 'protocol',
634
- originator,
635
- privileged,
636
- protocolID,
637
- counterparty,
638
- reason,
639
- renewal: false
640
- })
641
- }
642
- }
643
-
644
- /**
645
- * Ensures the originator has basket usage permission for the specified basket.
646
- * If not, triggers a permission request flow.
647
- */
648
- public async ensureBasketAccess({
649
- originator,
650
- basket,
651
- reason,
652
- seekPermission = true,
653
- usageType
654
- }: {
655
- originator: string
656
- basket: string
657
- reason?: string
658
- seekPermission?: boolean
659
- usageType: 'insertion' | 'removal' | 'listing'
660
- }): Promise<boolean> {
661
- if (this.isAdminOriginator(originator)) return true
662
- if (this.isAdminBasket(basket)) {
663
- throw new Error(`Basket “${basket}” is admin-only.`)
664
- }
665
- if (
666
- usageType === 'insertion' &&
667
- !this.config.seekBasketInsertionPermissions
668
- )
669
- return true
670
- if (usageType === 'removal' && !this.config.seekBasketRemovalPermissions)
671
- return true
672
- if (usageType === 'listing' && !this.config.seekBasketListingPermissions)
673
- return true
674
- const token = await this.findBasketToken(originator, basket, true)
675
- if (token) {
676
- if (!this.isTokenExpired(token.expiry)) {
677
- return true
678
- } else {
679
- if (!seekPermission) {
680
- throw new Error(`Basket permission expired (seekPermission=false).`)
681
- }
682
- return await this.requestPermissionFlow({
683
- type: 'basket',
684
- originator,
685
- basket,
686
- reason,
687
- renewal: true,
688
- previousToken: token
689
- })
690
- }
691
- } else {
692
- // none
693
- if (!seekPermission) {
694
- throw new Error(
695
- `No basket permission found, and no user consent allowed (seekPermission=false).`
696
- )
697
- }
698
- return await this.requestPermissionFlow({
699
- type: 'basket',
700
- originator,
701
- basket,
702
- reason,
703
- renewal: false
704
- })
705
- }
706
- }
707
-
708
- /**
709
- * Ensures the originator has a valid certificate permission.
710
- * This is relevant when revealing certificate fields in DCAP contexts.
711
- */
712
- public async ensureCertificateAccess({
713
- originator,
714
- privileged,
715
- verifier,
716
- certType,
717
- fields,
718
- reason,
719
- seekPermission = true,
720
- usageType
721
- }: {
722
- originator: string
723
- privileged: boolean
724
- verifier: string
725
- certType: string
726
- fields: string[]
727
- reason?: string
728
- seekPermission?: boolean
729
- usageType: 'disclosure'
730
- }): Promise<boolean> {
731
- if (this.isAdminOriginator(originator)) return true
732
- if (
733
- usageType === 'disclosure' &&
734
- !this.config.seekCertificateDisclosurePermissions
735
- ) {
736
- return true
737
- }
738
- if (!this.config.differentiatePrivilegedOperations) {
739
- privileged = false
740
- }
741
- const token = await this.findCertificateToken(
742
- originator,
743
- privileged,
744
- verifier,
745
- certType,
746
- fields,
747
- /*includeExpired=*/ true
748
- )
749
- if (token) {
750
- if (!this.isTokenExpired(token.expiry)) {
751
- return true
752
- } else {
753
- if (!seekPermission) {
754
- throw new Error(
755
- `Certificate permission expired (seekPermission=false).`
756
- )
757
- }
758
- return await this.requestPermissionFlow({
759
- type: 'certificate',
760
- originator,
761
- privileged,
762
- certificate: { verifier, certType, fields },
763
- reason,
764
- renewal: true,
765
- previousToken: token
766
- })
767
- }
768
- } else {
769
- if (!seekPermission) {
770
- throw new Error(
771
- `No certificate permission found (seekPermission=false).`
772
- )
773
- }
774
- return await this.requestPermissionFlow({
775
- type: 'certificate',
776
- originator,
777
- privileged,
778
- certificate: { verifier, certType, fields },
779
- reason,
780
- renewal: false
781
- })
782
- }
783
- }
784
-
785
- /**
786
- * Ensures the originator has spending authorization (DSAP) for a certain satoshi amount.
787
- * If the existing token limit is insufficient, attempts to renew. If no token, attempts to create one.
788
- */
789
- public async ensureSpendingAuthorization({
790
- originator,
791
- satoshis,
792
- lineItems,
793
- reason,
794
- seekPermission = true
795
- }: {
796
- originator: string
797
- satoshis: number
798
- lineItems?: Array<{
799
- type: 'input' | 'output' | 'fee'
800
- description: string
801
- satoshis: number
802
- }>
803
- reason?: string
804
- seekPermission?: boolean
805
- }): Promise<boolean> {
806
- if (this.isAdminOriginator(originator)) return true
807
- if (!this.config.seekSpendingPermissions) {
808
- // We skip spending permission entirely
809
- return true
810
- }
811
- const token = await this.findSpendingToken(originator)
812
- if (token?.authorizedAmount) {
813
- // Check how much has been spent so far
814
- const spentSoFar = await this.querySpentSince(token)
815
- if (spentSoFar + satoshis <= token.authorizedAmount) {
816
- return true
817
- } else {
818
- // Renew if possible
819
- if (!seekPermission) {
820
- throw new Error(
821
- `Spending authorization insufficient for ${satoshis}, no user consent (seekPermission=false).`
822
- )
823
- }
824
- return await this.requestPermissionFlow({
825
- type: 'spending',
826
- originator,
827
- spending: { satoshis, lineItems },
828
- reason,
829
- renewal: true,
830
- previousToken: token
831
- })
832
- }
833
- } else {
834
- // no token
835
- if (!seekPermission) {
836
- throw new Error(
837
- `No spending authorization found, (seekPermission=false).`
838
- )
839
- }
840
- return await this.requestPermissionFlow({
841
- type: 'spending',
842
- originator,
843
- spending: { satoshis, lineItems },
844
- reason,
845
- renewal: false
846
- })
847
- }
848
- }
849
-
850
- /**
851
- * Ensures the originator has label usage permission.
852
- * If no valid (unexpired) permission token is found, triggers a permission request flow.
853
- */
854
- public async ensureLabelAccess({
855
- originator,
856
- label,
857
- reason,
858
- seekPermission = true,
859
- usageType
860
- }: {
861
- originator: string
862
- label: string
863
- reason?: string
864
- seekPermission?: boolean
865
- usageType: 'apply' | 'list'
866
- }): Promise<boolean> {
867
- // 1) adminOriginator can do anything
868
- if (this.isAdminOriginator(originator)) return true
869
-
870
- // 2) If label is admin-reserved, block
871
- if (this.isAdminLabel(label)) {
872
- throw new Error(`Label “${label}” is admin-only.`)
873
- }
874
-
875
- if (
876
- usageType === 'apply' &&
877
- !this.config.seekPermissionWhenApplyingActionLabels
878
- ) {
879
- return true
880
- }
881
- if (
882
- usageType === 'list' &&
883
- !this.config.seekPermissionWhenListingActionsByLabel
884
- ) {
885
- return true
886
- }
887
-
888
- // 3) Let ensureProtocolPermission handle the rest.
889
- return await this.ensureProtocolPermission({
890
- originator,
891
- privileged: false,
892
- protocolID: [1, `action label ${label}`],
893
- counterparty: 'self',
894
- reason,
895
- seekPermission,
896
- usageType: 'generic'
897
- })
898
- }
899
-
900
- /**
901
- * A central method that triggers the permission request flow.
902
- * - It checks if there's already an active request for the same key
903
- * - If so, we wait on that existing request rather than creating a duplicative one
904
- * - Otherwise we create a new request queue, call the relevant "onXXXRequested" event,
905
- * and return a promise that resolves once permission is granted or rejects if denied.
906
- */
907
- private async requestPermissionFlow(r: PermissionRequest): Promise<boolean> {
908
- const key = this.buildRequestKey(r)
909
-
910
- // If there's already a queue for the same resource, we piggyback on it
911
- const existingQueue = this.activeRequests.get(key)
912
- if (existingQueue && existingQueue.pending.length > 0) {
913
- return new Promise<boolean>((resolve, reject) => {
914
- existingQueue.pending.push({ resolve, reject })
915
- })
916
- }
917
-
918
- // Otherwise, create a new queue with a single entry
919
- // Return a promise that resolves or rejects once the user grants/denies
920
- return new Promise<boolean>(async (resolve, reject) => {
921
- this.activeRequests.set(key, {
922
- request: r,
923
- pending: [{ resolve, reject }]
924
- })
925
-
926
- // Fire the relevant onXXXRequested event (which one depends on r.type)
927
- switch (r.type) {
928
- case 'protocol':
929
- await this.callEvent('onProtocolPermissionRequested', {
930
- ...r,
931
- requestID: key
932
- })
933
- break
934
- case 'basket':
935
- await this.callEvent('onBasketAccessRequested', {
936
- ...r,
937
- requestID: key
938
- })
939
- break
940
- case 'certificate':
941
- await this.callEvent('onCertificateAccessRequested', {
942
- ...r,
943
- requestID: key
944
- })
945
- break
946
- case 'spending':
947
- await this.callEvent('onSpendingAuthorizationRequested', {
948
- ...r,
949
- requestID: key
950
- })
951
- break
952
- }
953
- })
954
- }
955
-
956
- /* ---------------------------------------------------------------------
957
- * 4) SEARCH / DECODE / DECRYPT ON-CHAIN TOKENS (PushDrop Scripts)
958
- * --------------------------------------------------------------------- */
959
-
960
- /**
961
- * We will use a administrative "permission token encryption" protocol to store fields
962
- * in each permission's PushDrop script. This ensures that only the user's wallet
963
- * can decrypt them. In practice, this data is not super sensitive, but we still
964
- * follow the principle of least exposure.
965
- */
966
- private static readonly PERM_TOKEN_ENCRYPTION_PROTOCOL: [
967
- 2,
968
- 'admin permission token encryption'
969
- ] = [2, 'admin permission token encryption']
970
-
971
- /**
972
- * Similarly, we will use a "metadata encryption" protocol to preserve the confidentiality
973
- * of transaction descriptions and input/output descriptions from lower storage layers.
974
- */
975
- private static readonly METADATA_ENCRYPTION_PROTOCOL: [
976
- 2,
977
- 'admin metadata encryption'
978
- ] = [2, 'admin metadata encryption']
979
-
980
- /** We always use `keyID="1"` and `counterparty="self"` for these encryption ops. */
981
- private async encryptPermissionTokenField(
982
- plaintext: string | number[]
983
- ): Promise<number[]> {
984
- const data =
985
- typeof plaintext === 'string'
986
- ? Utils.toArray(plaintext, 'utf8')
987
- : plaintext
988
- const { ciphertext } = await this.underlying.encrypt(
989
- {
990
- plaintext: data,
991
- protocolID: WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
992
- keyID: '1'
993
- },
994
- this.adminOriginator
995
- )
996
- return ciphertext
997
- }
998
-
999
- private async decryptPermissionTokenField(
1000
- ciphertext: number[]
1001
- ): Promise<number[]> {
1002
- const { plaintext } = await this.underlying.decrypt(
1003
- {
1004
- ciphertext,
1005
- protocolID: WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
1006
- keyID: '1'
1007
- },
1008
- this.adminOriginator
1009
- )
1010
- return plaintext
1011
- }
1012
-
1013
- /**
1014
- * Encrypts wallet metadata if configured to do so, otherwise returns the original plaintext for storage.
1015
- * @param plaintext The metadata to encrypt if configured to do so
1016
- * @returns The encrypted metadata, or the original value if encryption was disabled.
1017
- */
1018
- private async maybeEncryptMetadata(plaintext: string): Promise<string> {
1019
- if (!this.config.encryptWalletMetadata) {
1020
- return plaintext
1021
- }
1022
- const { ciphertext } = await this.underlying.encrypt(
1023
- {
1024
- plaintext: Utils.toArray(plaintext, 'utf8'),
1025
- protocolID: WalletPermissionsManager.METADATA_ENCRYPTION_PROTOCOL,
1026
- keyID: '1'
1027
- },
1028
- this.adminOriginator
1029
- )
1030
- return Utils.toUTF8(ciphertext) // Still a string, but scrambled.
1031
- }
1032
-
1033
- /**
1034
- * Attempts to decrypt metadata. if decryption fails, assumes the value is already plaintext and returns it.
1035
- * @param ciphertext The metadata to attempt decryption for.
1036
- * @returns The decrypted metadata. If decryption fails, returns the original value instead.
1037
- */
1038
- private async maybeDecryptMetadata(ciphertext: string): Promise<string> {
1039
- try {
1040
- const { plaintext } = await this.underlying.decrypt(
1041
- {
1042
- ciphertext: Utils.toArray(ciphertext, 'utf8'),
1043
- protocolID: WalletPermissionsManager.METADATA_ENCRYPTION_PROTOCOL,
1044
- keyID: '1'
1045
- },
1046
- this.adminOriginator
1047
- )
1048
- return Utils.toUTF8(plaintext)
1049
- } catch (e) {
1050
- return ciphertext
1051
- }
1052
- }
1053
-
1054
- /** Helper to see if a token's expiry is in the past. */
1055
- private isTokenExpired(expiry: number): boolean {
1056
- const now = Math.floor(Date.now() / 1000)
1057
- return expiry > 0 && expiry < now
1058
- }
1059
-
1060
- /** Looks for a DPACP permission token matching origin/domain, privileged, protocol, cpty. */
1061
- private async findProtocolToken(
1062
- originator: string,
1063
- privileged: boolean,
1064
- protocolID: [0 | 1 | 2, string],
1065
- counterparty: string,
1066
- includeExpired: boolean
1067
- ): Promise<PermissionToken | undefined> {
1068
- const [secLevel, protoName] = protocolID
1069
- const result = await this.underlying.listOutputs(
1070
- {
1071
- basket: BASKET_MAP.protocol,
1072
- tags: [
1073
- `originator ${originator}`,
1074
- `privileged ${privileged}`,
1075
- `protocolName ${protoName}`,
1076
- `protocolSecurityLevel ${secLevel}`,
1077
- `counterparty ${counterparty}`
1078
- ],
1079
- tagQueryMode: 'all',
1080
- include: 'locking scripts'
1081
- },
1082
- this.adminOriginator
1083
- )
1084
-
1085
- for (const out of result.outputs) {
1086
- const script = LockingScript.fromHex(out.lockingScript!)
1087
- const dec = PushDrop.decode(script)
1088
- if (!dec || !dec.fields || dec.fields.length < 6) continue
1089
- const domainRaw = dec.fields[0]
1090
- const expiryRaw = dec.fields[1]
1091
- const privRaw = dec.fields[2]
1092
- const secLevelRaw = dec.fields[3]
1093
- const protoNameRaw = dec.fields[4]
1094
- const counterpartyRaw = dec.fields[5]
1095
-
1096
- const domainDecoded = Utils.toUTF8(
1097
- await this.decryptPermissionTokenField(domainRaw)
1098
- )
1099
- const expiryDecoded = parseInt(
1100
- Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)),
1101
- 10
1102
- )
1103
- const privDecoded =
1104
- Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
1105
- const secLevelDecoded = parseInt(
1106
- Utils.toUTF8(await this.decryptPermissionTokenField(secLevelRaw)),
1107
- 10
1108
- ) as 0 | 1 | 2
1109
- const protoNameDecoded = Utils.toUTF8(
1110
- await this.decryptPermissionTokenField(protoNameRaw)
1111
- )
1112
- const cptyDecoded = Utils.toUTF8(
1113
- await this.decryptPermissionTokenField(counterpartyRaw)
1114
- )
1115
-
1116
- if (
1117
- domainDecoded !== originator ||
1118
- privDecoded !== privileged ||
1119
- secLevelDecoded !== secLevel ||
1120
- protoNameDecoded !== protoName ||
1121
- cptyDecoded !== counterparty
1122
- ) {
1123
- continue
1124
- }
1125
- if (!includeExpired && this.isTokenExpired(expiryDecoded)) {
1126
- continue
1127
- }
1128
- return {
1129
- txid: out.outpoint.split('.')[0],
1130
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1131
- outputScript: out.lockingScript!,
1132
- satoshis: out.satoshis,
1133
- originator,
1134
- privileged,
1135
- protocol: protoName,
1136
- securityLevel: secLevel,
1137
- expiry: expiryDecoded,
1138
- counterparty: cptyDecoded
1139
- }
1140
- }
1141
- return undefined
1142
- }
1143
-
1144
- /** Looks for a DBAP token matching (originator, basket). */
1145
- private async findBasketToken(
1146
- originator: string,
1147
- basket: string,
1148
- includeExpired: boolean
1149
- ): Promise<PermissionToken | undefined> {
1150
- const result = await this.underlying.listOutputs(
1151
- {
1152
- basket: BASKET_MAP.basket,
1153
- tags: [`originator ${originator}`, `basket ${basket}`],
1154
- tagQueryMode: 'all',
1155
- include: 'locking scripts'
1156
- },
1157
- this.adminOriginator
1158
- )
1159
-
1160
- for (const out of result.outputs) {
1161
- const dec = PushDrop.decode(LockingScript.fromHex(out.lockingScript!))
1162
- if (!dec?.fields || dec.fields.length < 3) continue
1163
- const domainRaw = dec.fields[0]
1164
- const expiryRaw = dec.fields[1]
1165
- const basketRaw = dec.fields[2]
1166
-
1167
- const domainDecoded = Utils.toUTF8(
1168
- await this.decryptPermissionTokenField(domainRaw)
1169
- )
1170
- const expiryDecoded = parseInt(
1171
- Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)),
1172
- 10
1173
- )
1174
- const basketDecoded = Utils.toUTF8(
1175
- await this.decryptPermissionTokenField(basketRaw)
1176
- )
1177
- if (domainDecoded !== originator || basketDecoded !== basket) continue
1178
- if (!includeExpired && this.isTokenExpired(expiryDecoded)) continue
1179
-
1180
- return {
1181
- txid: out.outpoint.split('.')[0],
1182
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1183
- outputScript: out.lockingScript!,
1184
- satoshis: out.satoshis,
1185
- originator,
1186
- basketName: basketDecoded,
1187
- expiry: expiryDecoded
1188
- }
1189
- }
1190
- return undefined
1191
- }
1192
-
1193
- /** Looks for a DCAP token matching (origin, privileged, verifier, certType, fields subset). */
1194
- private async findCertificateToken(
1195
- originator: string,
1196
- privileged: boolean,
1197
- verifier: string,
1198
- certType: string,
1199
- fields: string[],
1200
- includeExpired: boolean
1201
- ): Promise<PermissionToken | undefined> {
1202
- const result = await this.underlying.listOutputs(
1203
- {
1204
- basket: BASKET_MAP.certificate,
1205
- tags: [
1206
- `originator ${originator}`,
1207
- `privileged ${privileged}`,
1208
- `type ${certType}`,
1209
- `verifier ${verifier}`
1210
- ],
1211
- tagQueryMode: 'all',
1212
- include: 'locking scripts'
1213
- },
1214
- this.adminOriginator
1215
- )
1216
-
1217
- for (const out of result.outputs) {
1218
- const dec = PushDrop.decode(LockingScript.fromHex(out.lockingScript!))
1219
- if (!dec?.fields || dec.fields.length < 6) continue
1220
- const [domainRaw, expiryRaw, privRaw, typeRaw, fieldsRaw, verifierRaw] =
1221
- dec.fields
1222
-
1223
- const domainDecoded = Utils.toUTF8(
1224
- await this.decryptPermissionTokenField(domainRaw)
1225
- )
1226
- const expiryDecoded = parseInt(
1227
- Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)),
1228
- 10
1229
- )
1230
- const privDecoded =
1231
- Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
1232
- const typeDecoded = Utils.toUTF8(
1233
- await this.decryptPermissionTokenField(typeRaw)
1234
- )
1235
- const verifierDec = Utils.toUTF8(
1236
- await this.decryptPermissionTokenField(verifierRaw)
1237
- )
1238
-
1239
- const fieldsJson = await this.decryptPermissionTokenField(fieldsRaw)
1240
- const allFields = JSON.parse(Utils.toUTF8(fieldsJson)) as string[]
1241
-
1242
- if (
1243
- domainDecoded !== originator ||
1244
- privDecoded !== privileged ||
1245
- typeDecoded !== certType ||
1246
- verifierDec !== verifier
1247
- ) {
1248
- continue
1249
- }
1250
- // Check if 'fields' is a subset of 'allFields'
1251
- const setAll = new Set(allFields)
1252
- if (fields.some(f => !setAll.has(f))) {
1253
- continue
1254
- }
1255
- if (!includeExpired && this.isTokenExpired(expiryDecoded)) {
1256
- continue
1257
- }
1258
- return {
1259
- txid: out.outpoint.split('.')[0],
1260
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1261
- outputScript: out.lockingScript!,
1262
- satoshis: out.satoshis,
1263
- originator,
1264
- privileged,
1265
- verifier: verifierDec,
1266
- certType: typeDecoded,
1267
- certFields: allFields,
1268
- expiry: expiryDecoded
1269
- }
1270
- }
1271
- return undefined
1272
- }
1273
-
1274
- /** Looks for a DSAP token matching origin, returning the first one found. */
1275
- private async findSpendingToken(
1276
- originator: string
1277
- ): Promise<PermissionToken | undefined> {
1278
- const result = await this.underlying.listOutputs(
1279
- {
1280
- basket: BASKET_MAP.spending,
1281
- tags: [`originator ${originator}`],
1282
- tagQueryMode: 'all',
1283
- include: 'locking scripts'
1284
- },
1285
- this.adminOriginator
1286
- )
1287
-
1288
- for (const out of result.outputs) {
1289
- const dec = PushDrop.decode(LockingScript.fromHex(out.lockingScript!))
1290
- if (!dec?.fields || dec.fields.length < 2) continue
1291
- const domainRaw = dec.fields[0]
1292
- const amtRaw = dec.fields[1]
1293
-
1294
- const domainDecoded = Utils.toUTF8(
1295
- await this.decryptPermissionTokenField(domainRaw)
1296
- )
1297
- if (domainDecoded !== originator) continue
1298
- const amtDecodedStr = Utils.toUTF8(
1299
- await this.decryptPermissionTokenField(amtRaw)
1300
- )
1301
- const authorizedAmount = parseInt(amtDecodedStr, 10)
1302
-
1303
- return {
1304
- txid: out.outpoint.split('.')[0],
1305
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1306
- outputScript: out.lockingScript!,
1307
- satoshis: out.satoshis,
1308
- originator,
1309
- authorizedAmount,
1310
- expiry: 0 // Not time-limited, monthly authorization
1311
- }
1312
- }
1313
- return undefined
1314
- }
1315
-
1316
- /**
1317
- * Returns the current month and year in UTC as a string in the format "YYYY-MM".
1318
- *
1319
- * @returns {string} The current month and year in UTC.
1320
- */
1321
- private getCurrentMonthYearUTC(): string {
1322
- const now = new Date()
1323
- const year = now.getUTCFullYear()
1324
- const month = (now.getUTCMonth() + 1).toString().padStart(2, '0') // Ensure 2-digit month
1325
- return `${year}-${month}`
1326
- }
1327
-
1328
- /**
1329
- * Returns spending for an originator in the current calendar month.
1330
- */
1331
- public async querySpentSince(token: PermissionToken): Promise<number> {
1332
- const { actions } = await this.underlying.listActions(
1333
- {
1334
- labels: [
1335
- `admin originator ${token.originator}`,
1336
- `admin month ${this.getCurrentMonthYearUTC()}`
1337
- ],
1338
- labelQueryMode: 'all'
1339
- },
1340
- this.adminOriginator
1341
- )
1342
- return actions.reduce((a, e) => a + e.satoshis, 0)
1343
- }
1344
-
1345
- /* ---------------------------------------------------------------------
1346
- * 5) CREATE / RENEW / REVOKE PERMISSION TOKENS ON CHAIN
1347
- * --------------------------------------------------------------------- */
1348
-
1349
- /**
1350
- * Creates a brand-new permission token as a single-output PushDrop script in the relevant admin basket.
1351
- *
1352
- * The main difference between each type of token is in the "fields" we store in the PushDrop script.
1353
- *
1354
- * @param r The permission request
1355
- * @param expiry The expiry epoch time
1356
- * @param amount For DSAP, the authorized spending limit
1357
- */
1358
- private async createPermissionOnChain(
1359
- r: PermissionRequest,
1360
- expiry: number,
1361
- amount?: number
1362
- ): Promise<void> {
1363
- const basketName = BASKET_MAP[r.type]
1364
- if (!basketName) return
1365
-
1366
- // Build the array of encrypted fields for the PushDrop script
1367
- const fields: number[][] = await this.buildPushdropFields(r, expiry, amount)
1368
-
1369
- // Construct the script. We do a simple P2PK check. We ask `PushDrop.lock(...)`
1370
- // to create a script with a single OP_CHECKSIG verifying ownership to redeem.
1371
- const script = await new PushDrop(this.underlying).lock(
1372
- fields,
1373
- WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
1374
- '1',
1375
- 'self',
1376
- true,
1377
- true
1378
- )
1379
-
1380
- // Create tags
1381
- const tags = this.buildTagsForRequest(r)
1382
-
1383
- // Build a transaction with exactly one output, no explicit inputs since the wallet
1384
- // can internally fund it from its balance.
1385
- await this.createAction(
1386
- {
1387
- description: `Grant ${r.type} permission`,
1388
- outputs: [
1389
- {
1390
- lockingScript: script.toHex(),
1391
- satoshis: 1,
1392
- outputDescription: `${r.type} permission token`,
1393
- basket: basketName,
1394
- tags
1395
- }
1396
- ]
1397
- },
1398
- this.adminOriginator
1399
- )
1400
- }
1401
-
1402
- /**
1403
- * Renews a permission token by spending the old token as input and creating a new token output.
1404
- * This invalidates the old token and replaces it with a new one.
1405
- *
1406
- * @param oldToken The old token to consume
1407
- * @param r The permission request being renewed
1408
- * @param newExpiry The new expiry epoch time
1409
- * @param newAmount For DSAP, the new authorized amount
1410
- */
1411
- private async renewPermissionOnChain(
1412
- oldToken: PermissionToken,
1413
- r: PermissionRequest,
1414
- newExpiry: number,
1415
- newAmount?: number
1416
- ): Promise<void> {
1417
- // 1) build new fields
1418
- const newFields = await this.buildPushdropFields(r, newExpiry, newAmount)
1419
-
1420
- // 2) new script
1421
- const newScript = await new PushDrop(this.underlying).lock(
1422
- newFields,
1423
- WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
1424
- '1',
1425
- 'self',
1426
- true,
1427
- true
1428
- )
1429
-
1430
- const tags = this.buildTagsForRequest(r)
1431
-
1432
- // 3) For BRC-100, we do a "createAction" with a partial input referencing oldToken
1433
- // plus a single new output. We'll hydrate the template, then signAction for the wallet to finalize.
1434
- const oldOutpoint = `${oldToken.txid}.${oldToken.outputIndex}`
1435
- const { signableTransaction } = await this.createAction(
1436
- {
1437
- description: `Renew ${r.type} permission`,
1438
- inputs: [
1439
- {
1440
- outpoint: oldOutpoint,
1441
- unlockingScriptLength: 73, // length of signature
1442
- inputDescription: `Consume old ${r.type} token`
1443
- }
1444
- ],
1445
- outputs: [
1446
- {
1447
- lockingScript: newScript.toHex(),
1448
- satoshis: 1,
1449
- outputDescription: `Renewed ${r.type} permission token`,
1450
- basket: BASKET_MAP[r.type],
1451
- tags
1452
- }
1453
- ]
1454
- },
1455
- this.adminOriginator
1456
- )
1457
- const tx = Transaction.fromBEEF(signableTransaction!.tx)
1458
- const unlocker = new PushDrop(this.underlying).unlock(
1459
- WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
1460
- '1',
1461
- 'self',
1462
- 'all',
1463
- false,
1464
- 1,
1465
- LockingScript.fromHex(oldToken.outputScript)
1466
- )
1467
- const unlockingScript = await unlocker.sign(tx, 0)
1468
- await this.underlying.signAction({
1469
- reference: signableTransaction!.reference,
1470
- spends: {
1471
- 0: {
1472
- unlockingScript: unlockingScript.toHex()
1473
- }
1474
- }
1475
- })
1476
- }
1477
-
1478
- /**
1479
- * Builds the encrypted array of fields for a PushDrop permission token
1480
- * (protocol / basket / certificate / spending).
1481
- */
1482
- private async buildPushdropFields(
1483
- r: PermissionRequest,
1484
- expiry: number,
1485
- amount?: number
1486
- ): Promise<number[][]> {
1487
- switch (r.type) {
1488
- case 'protocol': {
1489
- const [secLevel, protoName] = r.protocolID!
1490
- return [
1491
- await this.encryptPermissionTokenField(r.originator), // domain
1492
- await this.encryptPermissionTokenField(String(expiry)), // expiry
1493
- await this.encryptPermissionTokenField(
1494
- r.privileged ? 'true' : 'false'
1495
- ),
1496
- await this.encryptPermissionTokenField(String(secLevel)),
1497
- await this.encryptPermissionTokenField(protoName),
1498
- await this.encryptPermissionTokenField(r.counterparty!)
1499
- ]
1500
- }
1501
- case 'basket': {
1502
- return [
1503
- await this.encryptPermissionTokenField(r.originator),
1504
- await this.encryptPermissionTokenField(String(expiry)),
1505
- await this.encryptPermissionTokenField(r.basket!)
1506
- ]
1507
- }
1508
- case 'certificate': {
1509
- const { certType, fields, verifier } = r.certificate!
1510
- return [
1511
- await this.encryptPermissionTokenField(r.originator),
1512
- await this.encryptPermissionTokenField(String(expiry)),
1513
- await this.encryptPermissionTokenField(
1514
- r.privileged ? 'true' : 'false'
1515
- ),
1516
- await this.encryptPermissionTokenField(certType),
1517
- await this.encryptPermissionTokenField(JSON.stringify(fields)),
1518
- await this.encryptPermissionTokenField(verifier)
1519
- ]
1520
- }
1521
- case 'spending': {
1522
- // DSAP
1523
- const authAmt = amount ?? (r.spending?.satoshis || 0)
1524
- return [
1525
- await this.encryptPermissionTokenField(r.originator),
1526
- await this.encryptPermissionTokenField(String(authAmt))
1527
- ]
1528
- }
1529
- }
1530
- }
1531
-
1532
- /**
1533
- * Helper to build an array of tags for the new output, matching the user request's
1534
- * origin, basket, privileged, protocol name, etc.
1535
- */
1536
- private buildTagsForRequest(r: PermissionRequest): string[] {
1537
- const tags: string[] = [`originator ${r.originator}`]
1538
- switch (r.type) {
1539
- case 'protocol': {
1540
- tags.push(`privileged ${r.privileged}`)
1541
- tags.push(`protocolName ${r.protocolID![1]}`)
1542
- tags.push(`protocolSecurityLevel ${r.protocolID![0]}`)
1543
- tags.push(`counterparty ${r.counterparty}`)
1544
- break
1545
- }
1546
- case 'basket': {
1547
- tags.push(`basket ${r.basket}`)
1548
- break
1549
- }
1550
- case 'certificate': {
1551
- tags.push(`privileged ${r.privileged}`)
1552
- tags.push(`type ${r.certificate!.certType}`)
1553
- tags.push(`verifier ${r.certificate!.verifier}`)
1554
- break
1555
- }
1556
- case 'spending': {
1557
- // Only 'originator' is strictly required as a tag.
1558
- break
1559
- }
1560
- }
1561
- return tags
1562
- }
1563
-
1564
- /* ---------------------------------------------------------------------
1565
- * 6) PUBLIC "LIST/HAS/REVOKE" METHODS
1566
- * --------------------------------------------------------------------- */
1567
-
1568
- /**
1569
- * Lists all protocol permission tokens (DPACP) for a given originator or for all if originator is undefined.
1570
- * This is a convenience method for UI or debug.
1571
- */
1572
- public async listProtocolPermissions({
1573
- originator
1574
- }: {
1575
- originator?: string
1576
- }): Promise<PermissionToken[]> {
1577
- const basketName = BASKET_MAP.protocol
1578
- const tags: string[] = originator ? [`originator ${originator}`] : []
1579
- const result = await this.underlying.listOutputs(
1580
- {
1581
- basket: basketName,
1582
- tags,
1583
- tagQueryMode: 'all',
1584
- include: 'locking scripts',
1585
- limit: 100
1586
- },
1587
- this.adminOriginator
1588
- )
1589
-
1590
- const tokens: PermissionToken[] = []
1591
- for (const out of result.outputs) {
1592
- const dec = PushDrop.decode(LockingScript.fromHex(out.lockingScript!))
1593
- if (!dec?.fields || dec.fields.length < 6) continue
1594
- const [domainRaw, expiryRaw, privRaw, secRaw, protoRaw, cptyRaw] =
1595
- dec.fields
1596
-
1597
- const domainDec = Utils.toUTF8(
1598
- await this.decryptPermissionTokenField(domainRaw)
1599
- )
1600
- const expiryDec = parseInt(
1601
- Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)),
1602
- 10
1603
- )
1604
- const privDec =
1605
- Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
1606
- const secDec = parseInt(
1607
- Utils.toUTF8(await this.decryptPermissionTokenField(secRaw)),
1608
- 10
1609
- ) as 0 | 1 | 2
1610
- const protoDec = Utils.toUTF8(
1611
- await this.decryptPermissionTokenField(protoRaw)
1612
- )
1613
- const cptyDec = Utils.toUTF8(
1614
- await this.decryptPermissionTokenField(cptyRaw)
1615
- )
1616
-
1617
- tokens.push({
1618
- txid: out.outpoint.split('.')[0],
1619
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1620
- outputScript: out.lockingScript!,
1621
- satoshis: out.satoshis,
1622
- originator: domainDec,
1623
- expiry: expiryDec,
1624
- privileged: privDec,
1625
- securityLevel: secDec,
1626
- protocol: protoDec,
1627
- counterparty: cptyDec
1628
- })
1629
- }
1630
- return tokens
1631
- }
1632
-
1633
- /**
1634
- * Returns true if the originator already holds a valid unexpired protocol permission.
1635
- * This calls `ensureProtocolPermission` with `seekPermission=false`, so it won't prompt.
1636
- */
1637
- public async hasProtocolPermission(params: {
1638
- originator: string
1639
- privileged: boolean
1640
- protocolID: [0 | 1 | 2, string]
1641
- counterparty: string
1642
- }): Promise<boolean> {
1643
- try {
1644
- await this.ensureProtocolPermission({
1645
- ...params,
1646
- reason: 'hasProtocolPermission',
1647
- seekPermission: false,
1648
- usageType: 'generic'
1649
- })
1650
- return true
1651
- } catch {
1652
- return false
1653
- }
1654
- }
1655
-
1656
- /**
1657
- * Lists basket permission tokens (DBAP) for a given originator (or for all if not specified).
1658
- */
1659
- public async listBasketAccess(params: {
1660
- originator?: string
1661
- }): Promise<PermissionToken[]> {
1662
- const basketName = BASKET_MAP.basket
1663
- const tags: string[] = []
1664
- if (params.originator) {
1665
- tags.push(`originator ${params.originator}`)
1666
- }
1667
- const result = await this.underlying.listOutputs(
1668
- {
1669
- basket: basketName,
1670
- tags,
1671
- tagQueryMode: 'all',
1672
- include: 'locking scripts',
1673
- limit: 10000
1674
- },
1675
- this.adminOriginator
1676
- )
1677
-
1678
- const tokens: PermissionToken[] = []
1679
- for (const out of result.outputs) {
1680
- const dec = PushDrop.decode(LockingScript.fromHex(out.lockingScript!))
1681
- if (!dec?.fields || dec.fields.length < 3) continue
1682
- const [domainRaw, expiryRaw, basketRaw] = dec.fields
1683
- const domainDecoded = Utils.toUTF8(
1684
- await this.decryptPermissionTokenField(domainRaw)
1685
- )
1686
- const expiryDecoded = parseInt(
1687
- Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)),
1688
- 10
1689
- )
1690
- const basketDecoded = Utils.toUTF8(
1691
- await this.decryptPermissionTokenField(basketRaw)
1692
- )
1693
- tokens.push({
1694
- txid: out.outpoint.split('.')[0],
1695
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1696
- satoshis: out.satoshis,
1697
- outputScript: out.lockingScript!,
1698
- originator: domainDecoded,
1699
- basketName: basketDecoded,
1700
- expiry: expiryDecoded
1701
- })
1702
- }
1703
- return tokens
1704
- }
1705
-
1706
- /**
1707
- * Returns `true` if the originator already holds a valid unexpired basket permission for `basket`.
1708
- */
1709
- public async hasBasketAccess(params: {
1710
- originator: string
1711
- basket: string
1712
- }): Promise<boolean> {
1713
- try {
1714
- await this.ensureBasketAccess({
1715
- originator: params.originator,
1716
- basket: params.basket,
1717
- seekPermission: false,
1718
- usageType: 'insertion' // TODO: Consider a generic case for "has"
1719
- })
1720
- return true
1721
- } catch {
1722
- return false
1723
- }
1724
- }
1725
-
1726
- /**
1727
- * Lists spending authorization tokens (DSAP) for a given originator (or all).
1728
- */
1729
- public async listSpendingAuthorizations(params: {
1730
- originator?: string
1731
- }): Promise<PermissionToken[]> {
1732
- const basketName = BASKET_MAP.spending
1733
- const tags: string[] = []
1734
- if (params.originator) {
1735
- tags.push(`originator ${params.originator}`)
1736
- }
1737
- const result = await this.underlying.listOutputs(
1738
- {
1739
- basket: basketName,
1740
- tags,
1741
- tagQueryMode: 'all',
1742
- include: 'locking scripts',
1743
- limit: 10000
1744
- },
1745
- this.adminOriginator
1746
- )
1747
-
1748
- const tokens: PermissionToken[] = []
1749
- for (const out of result.outputs) {
1750
- const dec = PushDrop.decode(LockingScript.fromHex(out.lockingScript!))
1751
- if (!dec?.fields || dec.fields.length < 2) continue
1752
- const [domainRaw, amtRaw] = dec.fields
1753
- const domainDecoded = Utils.toUTF8(
1754
- await this.decryptPermissionTokenField(domainRaw)
1755
- )
1756
- const amtDecodedStr = Utils.toUTF8(
1757
- await this.decryptPermissionTokenField(amtRaw)
1758
- )
1759
- const authorizedAmount = parseInt(amtDecodedStr, 10)
1760
- tokens.push({
1761
- txid: out.outpoint.split('.')[0],
1762
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1763
- satoshis: out.satoshis,
1764
- outputScript: out.lockingScript!,
1765
- originator: domainDecoded,
1766
- authorizedAmount,
1767
- expiry: 0
1768
- })
1769
- }
1770
- return tokens
1771
- }
1772
-
1773
- /**
1774
- * Returns `true` if the originator already holds a valid spending authorization token
1775
- * with enough available monthly spend. We do not prompt (seekPermission=false).
1776
- */
1777
- public async hasSpendingAuthorization(params: {
1778
- originator: string
1779
- satoshis: number
1780
- }): Promise<boolean> {
1781
- try {
1782
- await this.ensureSpendingAuthorization({
1783
- originator: params.originator,
1784
- satoshis: params.satoshis,
1785
- seekPermission: false
1786
- })
1787
- return true
1788
- } catch {
1789
- return false
1790
- }
1791
- }
1792
-
1793
- /**
1794
- * Lists certificate permission tokens (DCAP) for a given originator (or all).
1795
- */
1796
- public async listCertificateAccess(params: {
1797
- originator?: string
1798
- }): Promise<PermissionToken[]> {
1799
- const basketName = BASKET_MAP.certificate
1800
- const tags: string[] = []
1801
- if (params.originator) {
1802
- tags.push(`originator ${params.originator}`)
1803
- }
1804
- const result = await this.underlying.listOutputs(
1805
- {
1806
- basket: basketName,
1807
- tags,
1808
- tagQueryMode: 'all',
1809
- include: 'locking scripts',
1810
- limit: 10000
1811
- },
1812
- this.adminOriginator
1813
- )
1814
-
1815
- const tokens: PermissionToken[] = []
1816
- for (const out of result.outputs) {
1817
- const dec = PushDrop.decode(LockingScript.fromHex(out.lockingScript!))
1818
- if (!dec?.fields || dec.fields.length < 6) continue
1819
- const [domainRaw, expiryRaw, privRaw, typeRaw, fieldsRaw, verifierRaw] =
1820
- dec.fields
1821
- const domainDecoded = Utils.toUTF8(
1822
- await this.decryptPermissionTokenField(domainRaw)
1823
- )
1824
- const expiryDecoded = parseInt(
1825
- Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)),
1826
- 10
1827
- )
1828
- const privDecoded =
1829
- Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
1830
- const typeDecoded = Utils.toUTF8(
1831
- await this.decryptPermissionTokenField(typeRaw)
1832
- )
1833
- const verifierDec = Utils.toUTF8(
1834
- await this.decryptPermissionTokenField(verifierRaw)
1835
- )
1836
- const fieldsJson = await this.decryptPermissionTokenField(fieldsRaw)
1837
- const allFields = JSON.parse(Utils.toUTF8(fieldsJson)) as string[]
1838
- tokens.push({
1839
- txid: out.outpoint.split('.')[0],
1840
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1841
- satoshis: out.satoshis,
1842
- outputScript: out.lockingScript!,
1843
- originator: domainDecoded,
1844
- privileged: privDecoded,
1845
- certType: typeDecoded,
1846
- certFields: allFields,
1847
- verifier: verifierDec,
1848
- expiry: expiryDecoded
1849
- })
1850
- }
1851
- return tokens
1852
- }
1853
-
1854
- /**
1855
- * Returns `true` if the originator already holds a valid unexpired certificate access
1856
- * for the given certType/fields. Does not prompt the user.
1857
- */
1858
- public async hasCertificateAccess(params: {
1859
- originator: string
1860
- privileged: boolean
1861
- verifier: string
1862
- certType: string
1863
- fields: string[]
1864
- }): Promise<boolean> {
1865
- try {
1866
- await this.ensureCertificateAccess({
1867
- originator: params.originator,
1868
- privileged: params.privileged,
1869
- verifier: params.verifier,
1870
- certType: params.certType,
1871
- fields: params.fields,
1872
- seekPermission: false,
1873
- usageType: 'disclosure'
1874
- })
1875
- return true
1876
- } catch {
1877
- return false
1878
- }
1879
- }
1880
-
1881
- /**
1882
- * Revokes a permission token by spending it with no replacement output.
1883
- * The manager builds a BRC-100 transaction that consumes the token, effectively invalidating it.
1884
- */
1885
- public async revokePermission(oldToken: PermissionToken): Promise<void> {
1886
- const oldOutpoint = `${oldToken.txid}.${oldToken.outputIndex}`
1887
- const { signableTransaction } = await this.createAction(
1888
- {
1889
- description: `Revoke permission`,
1890
- inputs: [
1891
- {
1892
- outpoint: oldOutpoint,
1893
- unlockingScriptLength: 73, // length of signature
1894
- inputDescription: `Consume old permission token`
1895
- }
1896
- ]
1897
- },
1898
- this.adminOriginator
1899
- )
1900
- const tx = Transaction.fromBEEF(signableTransaction!.tx)
1901
- const unlocker = new PushDrop(this.underlying).unlock(
1902
- WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
1903
- '1',
1904
- 'self',
1905
- 'all',
1906
- false,
1907
- 1,
1908
- LockingScript.fromHex(oldToken.outputScript)
1909
- )
1910
- const unlockingScript = await unlocker.sign(tx, 0)
1911
- await this.underlying.signAction({
1912
- reference: signableTransaction!.reference,
1913
- spends: {
1914
- 0: {
1915
- unlockingScript: unlockingScript.toHex()
1916
- }
1917
- }
1918
- })
1919
- }
1920
-
1921
- /* ---------------------------------------------------------------------
1922
- * 7) BRC-100 WALLET INTERFACE FORWARDING WITH PERMISSION CHECKS
1923
- * --------------------------------------------------------------------- */
1924
-
1925
- public async createAction(
1926
- args: Parameters<WalletInterface['createAction']>[0],
1927
- originator?: string
1928
- ): ReturnType<WalletInterface['createAction']> {
1929
- // 1) Ensure basket and label permissions
1930
- if (args.outputs) {
1931
- for (const out of args.outputs) {
1932
- if (out.basket) {
1933
- await this.ensureBasketAccess({
1934
- originator: originator!,
1935
- basket: out.basket,
1936
- reason: args.description,
1937
- usageType: 'insertion'
1938
- })
1939
- }
1940
- }
1941
- }
1942
- if (args.labels) {
1943
- for (const lbl of args.labels) {
1944
- await this.ensureLabelAccess({
1945
- originator: originator!,
1946
- label: lbl,
1947
- reason: args.description,
1948
- usageType: 'apply'
1949
- })
1950
- }
1951
- }
1952
-
1953
- /**
1954
- * 2) Force signAndProcess=false unless the originator is admin and explicitly sets it to true.
1955
- * This ensures the underlying wallet returns a signableTransaction, letting us parse the transaction
1956
- * to determine net spending and request authorization if needed.
1957
- */
1958
- const modifiedOptions = { ...(args.options || {}) }
1959
- if (modifiedOptions.signAndProcess !== true) {
1960
- modifiedOptions.signAndProcess = false
1961
- } else if (!this.isAdminOriginator(originator!)) {
1962
- throw new Error(
1963
- 'Only the admin originator can set signAndProcess=true explicitly.'
1964
- )
1965
- }
1966
-
1967
- // 3) Encrypt transaction metadata, saving originals for use in permissions and line items.
1968
- const originalDescription = args.description
1969
- const originalInputDescriptions = {}
1970
- const originalOutputDescriptions = {}
1971
- args.description = await this.maybeEncryptMetadata(args.description)
1972
- for (let i = 0; i < (args.inputs || []).length; i++) {
1973
- if (args.inputs![i].inputDescription) {
1974
- originalInputDescriptions[i] = args.inputs![i].inputDescription
1975
- args.inputs![i].inputDescription = await this.maybeEncryptMetadata(
1976
- args.inputs![i].inputDescription
1977
- )
1978
- }
1979
- }
1980
- for (let i = 0; i < (args.outputs || []).length; i++) {
1981
- if (args.outputs![i].outputDescription) {
1982
- originalOutputDescriptions[i] = args.outputs![i].outputDescription
1983
- args.outputs![i].outputDescription = await this.maybeEncryptMetadata(
1984
- args.outputs![i].outputDescription
1985
- )
1986
- }
1987
- if (args.outputs![i].customInstructions) {
1988
- args.outputs![i].customInstructions = await this.maybeEncryptMetadata(
1989
- args.outputs![i].customInstructions!
1990
- )
1991
- }
1992
- }
1993
-
1994
- /**
1995
- * 4) Call the underlying wallet’s createAction. We add two “admin” labels:
1996
- * - "admin originator <domain>"
1997
- * - "admin month YYYY-MM"
1998
- * These labels help track the originator’s monthly spending.
1999
- */
2000
- const createResult = await this.underlying.createAction(
2001
- {
2002
- ...args,
2003
- options: modifiedOptions,
2004
- labels: [
2005
- ...(args.labels || []),
2006
- `admin originator ${originator}`,
2007
- `admin month ${this.getCurrentMonthYearUTC()}`
2008
- ]
2009
- },
2010
- originator
2011
- )
2012
-
2013
- // If there's no signableTransaction, the underlying wallet must have fully finalized it. Return as is.
2014
- if (!createResult.signableTransaction) {
2015
- return createResult
2016
- }
2017
-
2018
- /**
2019
- * 5) We have a signable transaction. Parse it to determine how much the originator is actually spending.
2020
- * We only consider inputs the originator explicitly listed in args.inputs.
2021
- * netSpent = (sum of originator-requested outputs) - (sum of matching originator inputs).
2022
- * If netSpent > 0, we need spending authorization.
2023
- */
2024
- const tx = Transaction.fromAtomicBEEF(createResult.signableTransaction.tx)
2025
- const reference = createResult.signableTransaction.reference
2026
-
2027
- let netSpent = 0
2028
- const lineItems: Array<{
2029
- type: 'input' | 'output' | 'fee'
2030
- description: string
2031
- satoshis: number
2032
- }> = []
2033
-
2034
- // Sum originator-provided inputs:
2035
- let totalInputSatoshis = 0
2036
- for (const input of tx.inputs) {
2037
- const outpoint = `${input.sourceTXID}.${input.sourceOutputIndex}`
2038
- const matchingIndex = (args.inputs || []).findIndex(
2039
- i => i.outpoint === outpoint
2040
- )
2041
- if (matchingIndex !== -1) {
2042
- const satoshis =
2043
- input.sourceTransaction!.outputs[input.sourceOutputIndex].satoshis
2044
- totalInputSatoshis += satoshis!
2045
- lineItems.push({
2046
- type: 'input',
2047
- description:
2048
- originalInputDescriptions[matchingIndex] ||
2049
- 'No input description provided',
2050
- satoshis: satoshis!
2051
- })
2052
- }
2053
- }
2054
-
2055
- // Sum originator-requested outputs:
2056
- const totalOutputSatoshis = (args.outputs || []).reduce(
2057
- (acc, out) => acc + out.satoshis,
2058
- 0
2059
- )
2060
- for (const outIndex in args.outputs || []) {
2061
- const out = args.outputs![outIndex]
2062
- lineItems.push({
2063
- type: 'output',
2064
- satoshis: out.satoshis,
2065
- description:
2066
- originalOutputDescriptions[outIndex] ||
2067
- 'No output description provided'
2068
- })
2069
- }
2070
-
2071
- // Add an entry for the transaction fee:
2072
- lineItems.push({
2073
- type: 'fee',
2074
- satoshis: tx.getFee(),
2075
- description: 'Network fee'
2076
- })
2077
-
2078
- /**
2079
- * When it comes to spending authorizations, and the computation of net spend, there are
2080
- * two types of inputs and two types of outputs:
2081
- *
2082
- * There are foreign (originator-requested) ones, and domestic (internally-provided) ones.
2083
- * The net spend is always calculated from the domestic, internal perspective. Therefore, the
2084
- * cost of funding the foreign outputs is the base cost to the domestic user, unless this is
2085
- * somehow offset.
2086
- *
2087
- * The only way to offset this cost is when the foreign inputs help carry some of the burden.
2088
- * This is why we can subtract the sum of the foreign inputs from the sum of foreign outputs,
2089
- * to gague how much of that cost needs to be born domestically by the user.
2090
- *
2091
- * The logic does not need to account for whatever domestic inputs are provided, or whatever
2092
- * domestic outputs are re-captured by the wallet back as change. The wallet could conceivably
2093
- * provide 21e8 satoshis as input and re-capture the same amount as change, but the net effect
2094
- * on actual spending would be zero. Therefore, we base net spend on total foreign outflows
2095
- * minus total foreign inflows. Fees are also considered.
2096
- */
2097
- netSpent = totalOutputSatoshis + tx.getFee() - totalInputSatoshis
2098
-
2099
- // 6) If netSpent > 0, require spending authorization. Abort if denied.
2100
- if (netSpent > 0) {
2101
- try {
2102
- await this.ensureSpendingAuthorization({
2103
- originator: originator!,
2104
- satoshis: netSpent,
2105
- lineItems,
2106
- reason: originalDescription
2107
- })
2108
- } catch (err) {
2109
- await this.underlying.abortAction({ reference })
2110
- throw err
2111
- }
2112
- }
2113
-
2114
- /**
2115
- * 7) Decide whether to finalize the transaction automatically or return the signableTransaction:
2116
- * - 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.
2117
- * - If the transaction still needs more signatures, we must return the signableTransaction.
2118
- */
2119
- const userWantedImmediate = args.options?.signAndProcess !== false
2120
- const weOverrode =
2121
- modifiedOptions.signAndProcess === false &&
2122
- args.options?.signAndProcess !== false
2123
-
2124
- // Check if all inputs already have valid unlockingScripts (no partial signatures needed)
2125
- const allInputsHaveScripts = tx.inputs.every(i => i.unlockingScript)
2126
-
2127
- // If user wanted immediate broadcast and we forcibly suppressed it, we can finalize now by calling signAction with no additional scripts:
2128
- if (allInputsHaveScripts && userWantedImmediate && weOverrode) {
2129
- const signResult = await this.underlying.signAction(
2130
- { reference, spends: {}, options: args.options },
2131
- originator
2132
- )
2133
- // Merge signResult into createResult and remove signableTransaction:
2134
- return {
2135
- ...createResult,
2136
- ...signResult,
2137
- signableTransaction: undefined
2138
- }
2139
- }
2140
-
2141
- // Otherwise, return the partial transaction so the caller can do signAction.
2142
- return createResult
2143
- }
2144
-
2145
- public async signAction(
2146
- ...args: Parameters<WalletInterface['signAction']>
2147
- ): ReturnType<WalletInterface['signAction']> {
2148
- return this.underlying.signAction(...args)
2149
- }
2150
-
2151
- public async abortAction(
2152
- ...args: Parameters<WalletInterface['abortAction']>
2153
- ): ReturnType<WalletInterface['abortAction']> {
2154
- return this.underlying.abortAction(...args)
2155
- }
2156
-
2157
- public async listActions(
2158
- ...args: Parameters<WalletInterface['listActions']>
2159
- ): ReturnType<WalletInterface['listActions']> {
2160
- const [requestArgs, originator] = args
2161
- // for each label, ensure label access
2162
- if (requestArgs.labels) {
2163
- for (const lbl of requestArgs.labels) {
2164
- await this.ensureLabelAccess({
2165
- originator: originator!,
2166
- label: lbl,
2167
- reason: 'listActions',
2168
- usageType: 'list'
2169
- })
2170
- }
2171
- }
2172
- const results = await this.underlying.listActions(...args)
2173
- // Transparently decrypt transaction metadata, if configured to do so.
2174
- if (results.actions) {
2175
- for (let i = 0; i < results.actions.length; i++) {
2176
- if (results.actions[i].description) {
2177
- results.actions[i].description = await this.maybeDecryptMetadata(
2178
- results.actions[i].description
2179
- )
2180
- }
2181
- if (results.actions[i].inputs) {
2182
- for (let j = 0; j < results.actions[i].inputs!.length; j++) {
2183
- if (results.actions[i].inputs![j].inputDescription) {
2184
- results.actions[i].inputs![j].inputDescription =
2185
- await this.maybeDecryptMetadata(
2186
- results.actions[i].inputs![j].inputDescription
2187
- )
2188
- }
2189
- }
2190
- }
2191
- if (results.actions[i].outputs) {
2192
- for (let j = 0; j < results.actions[i].outputs!.length; j++) {
2193
- if (results.actions[i].outputs![j].outputDescription) {
2194
- results.actions[i].outputs![j].outputDescription =
2195
- await this.maybeDecryptMetadata(
2196
- results.actions[i].outputs![j].outputDescription
2197
- )
2198
- }
2199
- if (results.actions[i].outputs![j].customInstructions) {
2200
- results.actions[i].outputs![j].customInstructions =
2201
- await this.maybeDecryptMetadata(
2202
- results.actions[i].outputs![j].customInstructions!
2203
- )
2204
- }
2205
- }
2206
- }
2207
- }
2208
- }
2209
- return results
2210
- }
2211
-
2212
- public async internalizeAction(
2213
- ...args: Parameters<WalletInterface['internalizeAction']>
2214
- ): ReturnType<WalletInterface['internalizeAction']> {
2215
- const [requestArgs, originator] = args
2216
- // If the transaction is inserting outputs into baskets, we also ensure basket permission
2217
- for (const outIndex in requestArgs.outputs) {
2218
- const out = requestArgs.outputs[outIndex]
2219
- if (out.protocol === 'basket insertion') {
2220
- await this.ensureBasketAccess({
2221
- originator: originator!,
2222
- basket: out.insertionRemittance!.basket,
2223
- reason: requestArgs.description,
2224
- usageType: 'insertion'
2225
- })
2226
- if (out.insertionRemittance!.customInstructions) {
2227
- requestArgs.outputs[
2228
- outIndex
2229
- ].insertionRemittance!.customInstructions =
2230
- await this.maybeEncryptMetadata(
2231
- out.insertionRemittance!.customInstructions
2232
- )
2233
- }
2234
- }
2235
- }
2236
- return this.underlying.internalizeAction(...args)
2237
- }
2238
-
2239
- public async listOutputs(
2240
- ...args: Parameters<WalletInterface['listOutputs']>
2241
- ): ReturnType<WalletInterface['listOutputs']> {
2242
- const [requestArgs, originator] = args
2243
- // Ensure the originator has permission for the basket.
2244
- await this.ensureBasketAccess({
2245
- originator: originator!,
2246
- basket: requestArgs.basket,
2247
- reason: 'listOutputs',
2248
- usageType: 'listing'
2249
- })
2250
- const results = await this.underlying.listOutputs(...args)
2251
- // Transparently decrypt transaction metadata, if configured to do so.
2252
- if (results.outputs) {
2253
- for (let i = 0; i < results.outputs.length; i++) {
2254
- if (results.outputs[i].customInstructions) {
2255
- results.outputs[i].customInstructions =
2256
- await this.maybeDecryptMetadata(
2257
- results.outputs[i].customInstructions!
2258
- )
2259
- }
2260
- }
2261
- }
2262
- return results
2263
- }
2264
-
2265
- public async relinquishOutput(
2266
- ...args: Parameters<WalletInterface['relinquishOutput']>
2267
- ): ReturnType<WalletInterface['relinquishOutput']> {
2268
- const [requestArgs, originator] = args
2269
- await this.ensureBasketAccess({
2270
- originator: originator!,
2271
- basket: requestArgs.basket,
2272
- reason: 'relinquishOutput',
2273
- usageType: 'removal'
2274
- })
2275
- return this.underlying.relinquishOutput(...args)
2276
- }
2277
-
2278
- public async getPublicKey(
2279
- ...args: Parameters<WalletInterface['getPublicKey']>
2280
- ): ReturnType<WalletInterface['getPublicKey']> {
2281
- const [requestArgs, originator] = args
2282
- if (requestArgs.protocolID) {
2283
- await this.ensureProtocolPermission({
2284
- originator: originator!,
2285
- privileged: requestArgs.privileged!,
2286
- protocolID: requestArgs.protocolID,
2287
- counterparty: requestArgs.counterparty || 'self',
2288
- reason: requestArgs.privilegedReason,
2289
- usageType: 'publicKey'
2290
- })
2291
- }
2292
- if (requestArgs.identityKey) {
2293
- // We also require a minimal protocol permission to retrieve the user's identity key
2294
- await this.ensureProtocolPermission({
2295
- originator: originator!,
2296
- privileged: requestArgs.privileged!,
2297
- protocolID: [1, 'identity key retrieval'],
2298
- counterparty: 'self',
2299
- reason: requestArgs.privilegedReason,
2300
- usageType: 'identityKey'
2301
- })
2302
- }
2303
- return this.underlying.getPublicKey(...args)
2304
- }
2305
-
2306
- public async revealCounterpartyKeyLinkage(
2307
- ...args: Parameters<WalletInterface['revealCounterpartyKeyLinkage']>
2308
- ): ReturnType<WalletInterface['revealCounterpartyKeyLinkage']> {
2309
- const [requestArgs, originator] = args
2310
- await this.ensureProtocolPermission({
2311
- originator: originator!,
2312
- privileged: requestArgs.privileged!,
2313
- protocolID: [
2314
- 2,
2315
- `counterparty key linkage revelation ${requestArgs.counterparty}`
2316
- ],
2317
- counterparty: requestArgs.verifier,
2318
- reason: requestArgs.privilegedReason,
2319
- usageType: 'linkageRevelation'
2320
- })
2321
- return this.underlying.revealCounterpartyKeyLinkage(...args)
2322
- }
2323
-
2324
- public async revealSpecificKeyLinkage(
2325
- ...args: Parameters<WalletInterface['revealSpecificKeyLinkage']>
2326
- ): ReturnType<WalletInterface['revealSpecificKeyLinkage']> {
2327
- const [requestArgs, originator] = args
2328
- await this.ensureProtocolPermission({
2329
- originator: originator!,
2330
- privileged: requestArgs.privileged!,
2331
- protocolID: [
2332
- 2,
2333
- `specific key linkage revelation ${requestArgs.protocolID[1]} ${requestArgs.protocolID[0] === 2 ? requestArgs.keyID : 'all'}`
2334
- ],
2335
- counterparty: requestArgs.verifier,
2336
- reason: requestArgs.privilegedReason,
2337
- usageType: 'linkageRevelation'
2338
- })
2339
- return this.underlying.revealSpecificKeyLinkage(...args)
2340
- }
2341
-
2342
- public async encrypt(
2343
- ...args: Parameters<WalletInterface['encrypt']>
2344
- ): ReturnType<WalletInterface['encrypt']> {
2345
- const [requestArgs, originator] = args
2346
- await this.ensureProtocolPermission({
2347
- originator: originator!,
2348
- protocolID: requestArgs.protocolID,
2349
- privileged: requestArgs.privileged!,
2350
- counterparty: requestArgs.counterparty || 'self',
2351
- reason: requestArgs.privilegedReason,
2352
- usageType: 'encrypting'
2353
- })
2354
- return this.underlying.encrypt(...args)
2355
- }
2356
-
2357
- public async decrypt(
2358
- ...args: Parameters<WalletInterface['decrypt']>
2359
- ): ReturnType<WalletInterface['decrypt']> {
2360
- const [requestArgs, originator] = args
2361
- await this.ensureProtocolPermission({
2362
- originator: originator!,
2363
- privileged: requestArgs.privileged!,
2364
- protocolID: requestArgs.protocolID,
2365
- counterparty: requestArgs.counterparty || 'self',
2366
- reason: requestArgs.privilegedReason,
2367
- usageType: 'encrypting'
2368
- })
2369
- return this.underlying.decrypt(...args)
2370
- }
2371
-
2372
- public async createHmac(
2373
- ...args: Parameters<WalletInterface['createHmac']>
2374
- ): ReturnType<WalletInterface['createHmac']> {
2375
- const [requestArgs, originator] = args
2376
- await this.ensureProtocolPermission({
2377
- originator: originator!,
2378
- privileged: requestArgs.privileged!,
2379
- protocolID: requestArgs.protocolID,
2380
- counterparty: requestArgs.counterparty || 'self',
2381
- reason: requestArgs.privilegedReason,
2382
- usageType: 'hmac'
2383
- })
2384
- return this.underlying.createHmac(...args)
2385
- }
2386
-
2387
- public async verifyHmac(
2388
- ...args: Parameters<WalletInterface['verifyHmac']>
2389
- ): ReturnType<WalletInterface['verifyHmac']> {
2390
- const [requestArgs, originator] = args
2391
- await this.ensureProtocolPermission({
2392
- originator: originator!,
2393
- privileged: requestArgs.privileged!,
2394
- protocolID: requestArgs.protocolID,
2395
- counterparty: requestArgs.counterparty || 'self',
2396
- reason: requestArgs.privilegedReason,
2397
- usageType: 'hmac'
2398
- })
2399
- return this.underlying.verifyHmac(...args)
2400
- }
2401
-
2402
- public async createSignature(
2403
- ...args: Parameters<WalletInterface['createSignature']>
2404
- ): ReturnType<WalletInterface['createSignature']> {
2405
- const [requestArgs, originator] = args
2406
- await this.ensureProtocolPermission({
2407
- originator: originator!,
2408
- privileged: requestArgs.privileged!,
2409
- protocolID: requestArgs.protocolID,
2410
- counterparty: requestArgs.counterparty || 'self',
2411
- reason: requestArgs.privilegedReason,
2412
- usageType: 'signing'
2413
- })
2414
- return this.underlying.createSignature(...args)
2415
- }
2416
-
2417
- public async verifySignature(
2418
- ...args: Parameters<WalletInterface['verifySignature']>
2419
- ): ReturnType<WalletInterface['verifySignature']> {
2420
- const [requestArgs, originator] = args
2421
- await this.ensureProtocolPermission({
2422
- originator: originator!,
2423
- privileged: requestArgs.privileged!,
2424
- protocolID: requestArgs.protocolID,
2425
- counterparty: requestArgs.counterparty || 'self',
2426
- reason: requestArgs.privilegedReason,
2427
- usageType: 'signing'
2428
- })
2429
- return this.underlying.verifySignature(...args)
2430
- }
2431
-
2432
- public async acquireCertificate(
2433
- ...args: Parameters<WalletInterface['acquireCertificate']>
2434
- ): ReturnType<WalletInterface['acquireCertificate']> {
2435
- const [requestArgs, originator] = args
2436
- if (this.config.seekCertificateAcquisitionPermissions) {
2437
- await this.ensureProtocolPermission({
2438
- originator: originator!,
2439
- privileged: requestArgs.privileged!,
2440
- protocolID: [1, `certificate acquisition ${requestArgs.type}`],
2441
- counterparty: 'self',
2442
- reason: requestArgs.privilegedReason,
2443
- usageType: 'generic'
2444
- })
2445
- }
2446
- return this.underlying.acquireCertificate(...args)
2447
- }
2448
-
2449
- public async listCertificates(
2450
- ...args: Parameters<WalletInterface['listCertificates']>
2451
- ): ReturnType<WalletInterface['listCertificates']> {
2452
- const [requestArgs, originator] = args
2453
- if (this.config.seekCertificateListingPermissions) {
2454
- await this.ensureProtocolPermission({
2455
- originator: originator!,
2456
- privileged: requestArgs.privileged!,
2457
- protocolID: [1, `certificate list`],
2458
- counterparty: 'self',
2459
- reason: requestArgs.privilegedReason,
2460
- usageType: 'generic'
2461
- })
2462
- }
2463
- return this.underlying.listCertificates(...args)
2464
- }
2465
-
2466
- public async proveCertificate(
2467
- ...args: Parameters<WalletInterface['proveCertificate']>
2468
- ): ReturnType<WalletInterface['proveCertificate']> {
2469
- const [requestArgs, originator] = args
2470
- await this.ensureCertificateAccess({
2471
- originator: originator!,
2472
- privileged: requestArgs.privileged!,
2473
- verifier: requestArgs.verifier,
2474
- certType: requestArgs.certificate.type!,
2475
- fields: requestArgs.fieldsToReveal,
2476
- reason: 'proveCertificate',
2477
- usageType: 'disclosure'
2478
- })
2479
- return this.underlying.proveCertificate(...args)
2480
- }
2481
-
2482
- public async relinquishCertificate(
2483
- ...args: Parameters<WalletInterface['relinquishCertificate']>
2484
- ): ReturnType<WalletInterface['relinquishCertificate']> {
2485
- const [requestArgs, originator] = args
2486
- if (this.config.seekCertificateRelinquishmentPermissions) {
2487
- await this.ensureProtocolPermission({
2488
- originator: originator!,
2489
- privileged: (requestArgs as any).privileged ? true : false,
2490
- protocolID: [1, `certificate relinquishment ${requestArgs.type}`],
2491
- counterparty: 'self',
2492
- reason:
2493
- (requestArgs as any).privilegedReason || 'relinquishCertificate',
2494
- usageType: 'generic'
2495
- })
2496
- }
2497
- return this.underlying.relinquishCertificate(...args)
2498
- }
2499
-
2500
- public async discoverByIdentityKey(
2501
- ...args: Parameters<WalletInterface['discoverByIdentityKey']>
2502
- ): ReturnType<WalletInterface['discoverByIdentityKey']> {
2503
- const [_, originator] = args
2504
- if (this.config.seekPermissionsForIdentityResolution) {
2505
- await this.ensureProtocolPermission({
2506
- originator: originator!,
2507
- privileged: false,
2508
- protocolID: [1, `identity resolution`],
2509
- counterparty: 'self',
2510
- reason: 'discoverByIdentityKey',
2511
- usageType: 'generic'
2512
- })
2513
- }
2514
- return this.underlying.discoverByIdentityKey(...args)
2515
- }
2516
-
2517
- public async discoverByAttributes(
2518
- ...args: Parameters<WalletInterface['discoverByAttributes']>
2519
- ): ReturnType<WalletInterface['discoverByAttributes']> {
2520
- const [_, originator] = args
2521
- if (this.config.seekPermissionsForIdentityResolution) {
2522
- await this.ensureProtocolPermission({
2523
- originator: originator!,
2524
- privileged: false,
2525
- protocolID: [1, `identity resolution`],
2526
- counterparty: 'self',
2527
- reason: 'discoverByAttributes',
2528
- usageType: 'generic'
2529
- })
2530
- }
2531
- return this.underlying.discoverByAttributes(...args)
2532
- }
2533
-
2534
- public async isAuthenticated(
2535
- ...args: Parameters<WalletInterface['isAuthenticated']>
2536
- ): ReturnType<WalletInterface['isAuthenticated']> {
2537
- return this.underlying.isAuthenticated(...args)
2538
- }
2539
-
2540
- public async waitForAuthentication(
2541
- ...args: Parameters<WalletInterface['waitForAuthentication']>
2542
- ): ReturnType<WalletInterface['waitForAuthentication']> {
2543
- return this.underlying.waitForAuthentication(...args)
2544
- }
2545
-
2546
- public async getHeight(
2547
- ...args: Parameters<WalletInterface['getHeight']>
2548
- ): ReturnType<WalletInterface['getHeight']> {
2549
- return this.underlying.getHeight(...args)
2550
- }
2551
-
2552
- public async getHeaderForHeight(
2553
- ...args: Parameters<WalletInterface['getHeaderForHeight']>
2554
- ): ReturnType<WalletInterface['getHeaderForHeight']> {
2555
- return this.underlying.getHeaderForHeight(...args)
2556
- }
2557
-
2558
- public async getNetwork(
2559
- ...args: Parameters<WalletInterface['getNetwork']>
2560
- ): ReturnType<WalletInterface['getNetwork']> {
2561
- return this.underlying.getNetwork(...args)
2562
- }
2563
-
2564
- public async getVersion(
2565
- ...args: Parameters<WalletInterface['getVersion']>
2566
- ): ReturnType<WalletInterface['getVersion']> {
2567
- return this.underlying.getVersion(...args)
2568
- }
2569
-
2570
- /* ---------------------------------------------------------------------
2571
- * 8) INTERNAL HELPER UTILITIES
2572
- * --------------------------------------------------------------------- */
2573
-
2574
- /** Returns true if the specified origin is the admin originator. */
2575
- private isAdminOriginator(originator: string): boolean {
2576
- return originator === this.adminOriginator
2577
- }
2578
-
2579
- /**
2580
- * Checks if the given protocol is admin-reserved per BRC-100 rules:
2581
- *
2582
- * - Must not start with `admin` (admin-reserved)
2583
- * - Must not start with `p ` (allows for future specially permissioned protocols)
2584
- *
2585
- * If it violates these rules and the caller is not admin, we consider it "admin-only."
2586
- */
2587
- private isAdminProtocol(proto: [0 | 1 | 2, string]): boolean {
2588
- const protocolName = proto[1]
2589
- if (protocolName.startsWith('admin') || protocolName.startsWith('p ')) {
2590
- return true
2591
- }
2592
- return false
2593
- }
2594
-
2595
- /**
2596
- * Checks if the given label is admin-reserved per BRC-100 rules:
2597
- *
2598
- * - Must not start with `admin` (admin-reserved)
2599
- *
2600
- * If it violates these rules and the caller is not admin, we consider it "admin-only."
2601
- */
2602
- private isAdminLabel(label: string): boolean {
2603
- if (label.startsWith('admin')) {
2604
- return true
2605
- }
2606
- return false
2607
- }
2608
-
2609
- /**
2610
- * Checks if the given basket is admin-reserved per BRC-100 rules:
2611
- *
2612
- * - Must not start with `admin`
2613
- * - Must not be `default` (some wallets use this for internal operations)
2614
- * - Must not start with `p ` (future specially permissioned baskets)
2615
- */
2616
- private isAdminBasket(basket: string): boolean {
2617
- if (basket === 'default') return true
2618
- if (basket.startsWith('admin')) return true
2619
- if (basket.startsWith('p ')) return true
2620
- return false
2621
- }
2622
-
2623
- /**
2624
- * Builds a "map key" string so that identical requests (e.g. "protocol:domain:true:protoName:counterparty")
2625
- * do not produce multiple user prompts.
2626
- */
2627
- private buildRequestKey(r: PermissionRequest): string {
2628
- switch (r.type) {
2629
- case 'protocol':
2630
- return `proto:${r.originator}:${r.privileged}:${r.protocolID?.join(',')}:${r.counterparty}`
2631
- case 'basket':
2632
- return `basket:${r.originator}:${r.basket}`
2633
- case 'certificate':
2634
- return `cert:${r.originator}:${r.privileged}:${r.certificate?.verifier}:${r.certificate?.certType}:${r.certificate?.fields.join('|')}`
2635
- case 'spending':
2636
- return `spend:${r.originator}`
2637
- }
2638
- }
2639
- }