@bsv/wallet-toolbox 1.1.24 → 1.1.25

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 (194) hide show
  1. package/docs/client.md +2319 -84
  2. package/docs/wallet.md +2319 -84
  3. package/out/src/CWIStyleWalletManager.d.ts +411 -0
  4. package/out/src/CWIStyleWalletManager.d.ts.map +1 -0
  5. package/out/src/CWIStyleWalletManager.js +1131 -0
  6. package/out/src/CWIStyleWalletManager.js.map +1 -0
  7. package/out/src/SetupClient.d.ts +249 -0
  8. package/out/src/SetupClient.d.ts.map +1 -0
  9. package/out/src/SetupClient.js +252 -0
  10. package/out/src/SetupClient.js.map +1 -0
  11. package/out/src/SimpleWalletManager.d.ts +169 -0
  12. package/out/src/SimpleWalletManager.d.ts.map +1 -0
  13. package/out/src/SimpleWalletManager.js +315 -0
  14. package/out/src/SimpleWalletManager.js.map +1 -0
  15. package/out/src/Wallet.d.ts +6 -1
  16. package/out/src/Wallet.d.ts.map +1 -1
  17. package/out/src/Wallet.js +29 -2
  18. package/out/src/Wallet.js.map +1 -1
  19. package/out/src/WalletAuthenticationManager.d.ts +33 -0
  20. package/out/src/WalletAuthenticationManager.d.ts.map +1 -0
  21. package/out/src/WalletAuthenticationManager.js +107 -0
  22. package/out/src/WalletAuthenticationManager.js.map +1 -0
  23. package/out/src/WalletPermissionsManager.d.ts +575 -0
  24. package/out/src/WalletPermissionsManager.d.ts.map +1 -0
  25. package/out/src/WalletPermissionsManager.js +1807 -0
  26. package/out/src/WalletPermissionsManager.js.map +1 -0
  27. package/out/src/WalletSettingsManager.d.ts +59 -0
  28. package/out/src/WalletSettingsManager.d.ts.map +1 -0
  29. package/out/src/WalletSettingsManager.js +168 -0
  30. package/out/src/WalletSettingsManager.js.map +1 -0
  31. package/out/src/__tests/CWIStyleWalletManager.test.d.ts +2 -0
  32. package/out/src/__tests/CWIStyleWalletManager.test.d.ts.map +1 -0
  33. package/out/src/__tests/CWIStyleWalletManager.test.js +472 -0
  34. package/out/src/__tests/CWIStyleWalletManager.test.js.map +1 -0
  35. package/out/src/__tests/WalletPermissionsManager.callbacks.test.d.ts +2 -0
  36. package/out/src/__tests/WalletPermissionsManager.callbacks.test.d.ts.map +1 -0
  37. package/out/src/__tests/WalletPermissionsManager.callbacks.test.js +239 -0
  38. package/out/src/__tests/WalletPermissionsManager.callbacks.test.js.map +1 -0
  39. package/out/src/__tests/WalletPermissionsManager.checks.test.d.ts +2 -0
  40. package/out/src/__tests/WalletPermissionsManager.checks.test.d.ts.map +1 -0
  41. package/out/src/__tests/WalletPermissionsManager.checks.test.js +644 -0
  42. package/out/src/__tests/WalletPermissionsManager.checks.test.js.map +1 -0
  43. package/out/src/__tests/WalletPermissionsManager.encryption.test.d.ts +2 -0
  44. package/out/src/__tests/WalletPermissionsManager.encryption.test.d.ts.map +1 -0
  45. package/out/src/__tests/WalletPermissionsManager.encryption.test.js +295 -0
  46. package/out/src/__tests/WalletPermissionsManager.encryption.test.js.map +1 -0
  47. package/out/src/__tests/WalletPermissionsManager.fixtures.d.ts +82 -0
  48. package/out/src/__tests/WalletPermissionsManager.fixtures.d.ts.map +1 -0
  49. package/out/src/__tests/WalletPermissionsManager.fixtures.js +260 -0
  50. package/out/src/__tests/WalletPermissionsManager.fixtures.js.map +1 -0
  51. package/out/src/__tests/WalletPermissionsManager.flows.test.d.ts +2 -0
  52. package/out/src/__tests/WalletPermissionsManager.flows.test.d.ts.map +1 -0
  53. package/out/src/__tests/WalletPermissionsManager.flows.test.js +389 -0
  54. package/out/src/__tests/WalletPermissionsManager.flows.test.js.map +1 -0
  55. package/out/src/__tests/WalletPermissionsManager.initialization.test.d.ts +2 -0
  56. package/out/src/__tests/WalletPermissionsManager.initialization.test.d.ts.map +1 -0
  57. package/out/src/__tests/WalletPermissionsManager.initialization.test.js +227 -0
  58. package/out/src/__tests/WalletPermissionsManager.initialization.test.js.map +1 -0
  59. package/out/src/__tests/WalletPermissionsManager.proxying.test.d.ts +2 -0
  60. package/out/src/__tests/WalletPermissionsManager.proxying.test.d.ts.map +1 -0
  61. package/out/src/__tests/WalletPermissionsManager.proxying.test.js +566 -0
  62. package/out/src/__tests/WalletPermissionsManager.proxying.test.js.map +1 -0
  63. package/out/src/__tests/WalletPermissionsManager.tokens.test.d.ts +2 -0
  64. package/out/src/__tests/WalletPermissionsManager.tokens.test.d.ts.map +1 -0
  65. package/out/src/__tests/WalletPermissionsManager.tokens.test.js +460 -0
  66. package/out/src/__tests/WalletPermissionsManager.tokens.test.js.map +1 -0
  67. package/out/src/index.all.d.ts +9 -0
  68. package/out/src/index.all.d.ts.map +1 -1
  69. package/out/src/index.all.js +9 -0
  70. package/out/src/index.all.js.map +1 -1
  71. package/out/src/index.client.d.ts +9 -0
  72. package/out/src/index.client.d.ts.map +1 -1
  73. package/out/src/index.client.js +9 -0
  74. package/out/src/index.client.js.map +1 -1
  75. package/out/src/utility/identityUtils.d.ts +31 -0
  76. package/out/src/utility/identityUtils.d.ts.map +1 -0
  77. package/out/src/utility/identityUtils.js +114 -0
  78. package/out/src/utility/identityUtils.js.map +1 -0
  79. package/out/src/wab-client/WABClient.d.ts +38 -0
  80. package/out/src/wab-client/WABClient.d.ts.map +1 -0
  81. package/out/src/wab-client/WABClient.js +95 -0
  82. package/out/src/wab-client/WABClient.js.map +1 -0
  83. package/out/src/wab-client/__tests/WABClient.test.d.ts +2 -0
  84. package/out/src/wab-client/__tests/WABClient.test.d.ts.map +1 -0
  85. package/out/src/wab-client/__tests/WABClient.test.js +47 -0
  86. package/out/src/wab-client/__tests/WABClient.test.js.map +1 -0
  87. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts +34 -0
  88. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts.map +1 -0
  89. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js +16 -0
  90. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js.map +1 -0
  91. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.d.ts +7 -0
  92. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.d.ts.map +1 -0
  93. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.js +40 -0
  94. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.js.map +1 -0
  95. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts +28 -0
  96. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts.map +1 -0
  97. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js +73 -0
  98. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js.map +1 -0
  99. package/out/test/Wallet/action/abortAction.test.d.ts.map +1 -0
  100. package/out/test/{wallet → Wallet}/action/abortAction.test.js.map +1 -1
  101. package/out/test/Wallet/action/createAction.test.d.ts.map +1 -0
  102. package/out/test/{wallet → Wallet}/action/createAction.test.js.map +1 -1
  103. package/out/test/{wallet → Wallet}/action/createAction2.test.d.ts.map +1 -1
  104. package/out/test/{wallet → Wallet}/action/createAction2.test.js.map +1 -1
  105. package/out/test/Wallet/action/createActionToGenerateBeefs.man.test.d.ts.map +1 -0
  106. package/out/test/{wallet → Wallet}/action/createActionToGenerateBeefs.man.test.js.map +1 -1
  107. package/out/test/Wallet/action/internalizeAction.test.d.ts.map +1 -0
  108. package/out/test/{wallet → Wallet}/action/internalizeAction.test.js.map +1 -1
  109. package/out/test/Wallet/action/relinquishOutput.test.d.ts.map +1 -0
  110. package/out/test/{wallet → Wallet}/action/relinquishOutput.test.js.map +1 -1
  111. package/out/test/Wallet/construct/Wallet.constructor.test.d.ts.map +1 -0
  112. package/out/test/{wallet → Wallet}/construct/Wallet.constructor.test.js.map +1 -1
  113. package/out/test/Wallet/list/listActions.test.d.ts.map +1 -0
  114. package/out/test/{wallet → Wallet}/list/listActions.test.js.map +1 -1
  115. package/out/test/Wallet/list/listActions2.test.d.ts.map +1 -0
  116. package/out/test/{wallet → Wallet}/list/listActions2.test.js.map +1 -1
  117. package/out/test/Wallet/list/listCertificates.test.d.ts.map +1 -0
  118. package/out/test/{wallet → Wallet}/list/listCertificates.test.js.map +1 -1
  119. package/out/test/Wallet/list/listOutputs.test.d.ts.map +1 -0
  120. package/out/test/{wallet → Wallet}/list/listOutputs.test.js.map +1 -1
  121. package/out/test/Wallet/sync/Wallet.sync.test.d.ts.map +1 -0
  122. package/out/test/{wallet → Wallet}/sync/Wallet.sync.test.js.map +1 -1
  123. package/out/tsconfig.all.tsbuildinfo +1 -1
  124. package/package.json +3 -3
  125. package/src/CWIStyleWalletManager.ts +1891 -0
  126. package/src/SimpleWalletManager.ts +553 -0
  127. package/src/Wallet.ts +47 -3
  128. package/src/WalletAuthenticationManager.ts +183 -0
  129. package/src/WalletPermissionsManager.ts +2639 -0
  130. package/src/WalletSettingsManager.ts +241 -0
  131. package/src/__tests/CWIStyleWalletManager.test.ts +709 -0
  132. package/src/__tests/WalletPermissionsManager.callbacks.test.ts +328 -0
  133. package/src/__tests/WalletPermissionsManager.checks.test.ts +857 -0
  134. package/src/__tests/WalletPermissionsManager.encryption.test.ts +407 -0
  135. package/src/__tests/WalletPermissionsManager.fixtures.ts +283 -0
  136. package/src/__tests/WalletPermissionsManager.flows.test.ts +490 -0
  137. package/src/__tests/WalletPermissionsManager.initialization.test.ts +333 -0
  138. package/src/__tests/WalletPermissionsManager.proxying.test.ts +753 -0
  139. package/src/__tests/WalletPermissionsManager.tokens.test.ts +584 -0
  140. package/src/index.all.ts +9 -0
  141. package/src/index.client.ts +9 -0
  142. package/src/utility/identityUtils.ts +170 -0
  143. package/src/wab-client/WABClient.ts +103 -0
  144. package/src/wab-client/__tests/WABClient.test.ts +58 -0
  145. package/src/wab-client/auth-method-interactors/AuthMethodInteractor.ts +47 -0
  146. package/src/wab-client/auth-method-interactors/PersonaIDInteractor.ts +45 -0
  147. package/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.ts +82 -0
  148. package/out/test/wallet/action/abortAction.test.d.ts.map +0 -1
  149. package/out/test/wallet/action/createAction.test.d.ts.map +0 -1
  150. package/out/test/wallet/action/createActionToGenerateBeefs.man.test.d.ts.map +0 -1
  151. package/out/test/wallet/action/internalizeAction.test.d.ts.map +0 -1
  152. package/out/test/wallet/action/relinquishOutput.test.d.ts.map +0 -1
  153. package/out/test/wallet/construct/Wallet.constructor.test.d.ts.map +0 -1
  154. package/out/test/wallet/list/listActions.test.d.ts.map +0 -1
  155. package/out/test/wallet/list/listActions2.test.d.ts.map +0 -1
  156. package/out/test/wallet/list/listCertificates.test.d.ts.map +0 -1
  157. package/out/test/wallet/list/listOutputs.test.d.ts.map +0 -1
  158. package/out/test/wallet/sync/Wallet.sync.test.d.ts.map +0 -1
  159. /package/out/test/{wallet → Wallet}/action/abortAction.test.d.ts +0 -0
  160. /package/out/test/{wallet → Wallet}/action/abortAction.test.js +0 -0
  161. /package/out/test/{wallet → Wallet}/action/createAction.test.d.ts +0 -0
  162. /package/out/test/{wallet → Wallet}/action/createAction.test.js +0 -0
  163. /package/out/test/{wallet → Wallet}/action/createAction2.test.d.ts +0 -0
  164. /package/out/test/{wallet → Wallet}/action/createAction2.test.js +0 -0
  165. /package/out/test/{wallet → Wallet}/action/createActionToGenerateBeefs.man.test.d.ts +0 -0
  166. /package/out/test/{wallet → Wallet}/action/createActionToGenerateBeefs.man.test.js +0 -0
  167. /package/out/test/{wallet → Wallet}/action/internalizeAction.test.d.ts +0 -0
  168. /package/out/test/{wallet → Wallet}/action/internalizeAction.test.js +0 -0
  169. /package/out/test/{wallet → Wallet}/action/relinquishOutput.test.d.ts +0 -0
  170. /package/out/test/{wallet → Wallet}/action/relinquishOutput.test.js +0 -0
  171. /package/out/test/{wallet → Wallet}/construct/Wallet.constructor.test.d.ts +0 -0
  172. /package/out/test/{wallet → Wallet}/construct/Wallet.constructor.test.js +0 -0
  173. /package/out/test/{wallet → Wallet}/list/listActions.test.d.ts +0 -0
  174. /package/out/test/{wallet → Wallet}/list/listActions.test.js +0 -0
  175. /package/out/test/{wallet → Wallet}/list/listActions2.test.d.ts +0 -0
  176. /package/out/test/{wallet → Wallet}/list/listActions2.test.js +0 -0
  177. /package/out/test/{wallet → Wallet}/list/listCertificates.test.d.ts +0 -0
  178. /package/out/test/{wallet → Wallet}/list/listCertificates.test.js +0 -0
  179. /package/out/test/{wallet → Wallet}/list/listOutputs.test.d.ts +0 -0
  180. /package/out/test/{wallet → Wallet}/list/listOutputs.test.js +0 -0
  181. /package/out/test/{wallet → Wallet}/sync/Wallet.sync.test.d.ts +0 -0
  182. /package/out/test/{wallet → Wallet}/sync/Wallet.sync.test.js +0 -0
  183. /package/test/{wallet → Wallet}/action/abortAction.test.ts +0 -0
  184. /package/test/{wallet → Wallet}/action/createAction.test.ts +0 -0
  185. /package/test/{wallet → Wallet}/action/createAction2.test.ts +0 -0
  186. /package/test/{wallet → Wallet}/action/createActionToGenerateBeefs.man.test.ts +0 -0
  187. /package/test/{wallet → Wallet}/action/internalizeAction.test.ts +0 -0
  188. /package/test/{wallet → Wallet}/action/relinquishOutput.test.ts +0 -0
  189. /package/test/{wallet → Wallet}/construct/Wallet.constructor.test.ts +0 -0
  190. /package/test/{wallet → Wallet}/list/listActions.test.ts +0 -0
  191. /package/test/{wallet → Wallet}/list/listActions2.test.ts +0 -0
  192. /package/test/{wallet → Wallet}/list/listCertificates.test.ts +0 -0
  193. /package/test/{wallet → Wallet}/list/listOutputs.test.ts +0 -0
  194. /package/test/{wallet → Wallet}/sync/Wallet.sync.test.ts +0 -0
@@ -0,0 +1,407 @@
1
+ import {
2
+ mockUnderlyingWallet,
3
+ MockedBSV_SDK
4
+ } from './WalletPermissionsManager.fixtures'
5
+ import { WalletPermissionsManager } from '../WalletPermissionsManager'
6
+ import { jest } from '@jest/globals'
7
+
8
+ jest.mock('@bsv/sdk', () => MockedBSV_SDK)
9
+
10
+ describe('WalletPermissionsManager - Metadata Encryption & Decryption', () => {
11
+ let underlying: ReturnType<typeof mockUnderlyingWallet>
12
+
13
+ beforeEach(() => {
14
+ // Create a fresh underlying mock wallet before each test
15
+ underlying = mockUnderlyingWallet()
16
+ })
17
+
18
+ afterEach(() => {
19
+ jest.clearAllMocks()
20
+ })
21
+
22
+ describe('Unit Tests for metadata encryption helpers', () => {
23
+ it('should call underlying.encrypt() with the correct protocol and key when encryptWalletMetadata=true', async () => {
24
+ const manager = new WalletPermissionsManager(
25
+ underlying,
26
+ 'admin.domain.com',
27
+ {
28
+ encryptWalletMetadata: true
29
+ }
30
+ )
31
+
32
+ const plaintext = 'Hello, world!'
33
+ await manager['maybeEncryptMetadata'](plaintext)
34
+
35
+ // We expect underlying.encrypt() to have been called exactly once
36
+ expect(underlying.encrypt).toHaveBeenCalledTimes(1)
37
+
38
+ // Check that the call was with the correct protocol ID and key
39
+ expect(underlying.encrypt).toHaveBeenCalledWith(
40
+ {
41
+ plaintext: expect.any(Array), // byte array version of 'Hello, world!'
42
+ protocolID: [2, 'admin metadata encryption'],
43
+ keyID: '1'
44
+ },
45
+ 'admin.domain.com'
46
+ )
47
+ })
48
+
49
+ it('should NOT call underlying.encrypt() if encryptWalletMetadata=false', async () => {
50
+ const manager = new WalletPermissionsManager(
51
+ underlying,
52
+ 'admin.domain.com',
53
+ {
54
+ encryptWalletMetadata: false
55
+ }
56
+ )
57
+
58
+ const plaintext = 'No encryption needed!'
59
+ const result = await manager['maybeEncryptMetadata'](plaintext)
60
+
61
+ expect(result).toBe(plaintext)
62
+ expect(underlying.encrypt).not.toHaveBeenCalled()
63
+ })
64
+
65
+ it('should call underlying.decrypt() with correct protocol and key, returning plaintext on success', async () => {
66
+ const manager = new WalletPermissionsManager(
67
+ underlying,
68
+ 'admin.domain.com',
69
+ {
70
+ encryptWalletMetadata: true
71
+ }
72
+ )
73
+
74
+ // Underlying decrypt mock returns { plaintext: [42, 42] } by default
75
+ // which would become "**" if using our ASCII interpretation
76
+ ;(underlying.decrypt as any).mockResolvedValueOnce({
77
+ plaintext: [72, 105] // 'Hi'
78
+ })
79
+
80
+ const ciphertext = 'random-string-representing-ciphertext'
81
+ const result = await manager['maybeDecryptMetadata'](ciphertext)
82
+
83
+ // We expect underlying.decrypt() to have been called
84
+ expect(underlying.decrypt).toHaveBeenCalledTimes(1)
85
+ expect(underlying.decrypt).toHaveBeenCalledWith(
86
+ {
87
+ ciphertext: expect.any(Array), // byte array version of ciphertext
88
+ protocolID: [2, 'admin metadata encryption'],
89
+ keyID: '1'
90
+ },
91
+ 'admin.domain.com'
92
+ )
93
+
94
+ // The manager returns the decrypted UTF-8 string
95
+ expect(result).toBe('Hi')
96
+ })
97
+
98
+ it('should fallback to original string if underlying.decrypt() fails', async () => {
99
+ const manager = new WalletPermissionsManager(
100
+ underlying,
101
+ 'admin.domain.com',
102
+ {
103
+ encryptWalletMetadata: true
104
+ }
105
+ )
106
+
107
+ // Make underlying.decrypt() throw an error to simulate failure
108
+ ;(underlying.decrypt as any).mockImplementationOnce(() => {
109
+ throw new Error('Decryption error')
110
+ })
111
+
112
+ const ciphertext = 'this-was-not-valid-for-decryption'
113
+ const result = await manager['maybeDecryptMetadata'](ciphertext)
114
+
115
+ // The manager should return the original ciphertext if decryption throws
116
+ expect(result).toBe(ciphertext)
117
+ })
118
+ })
119
+
120
+ describe('Integration Tests for createAction + listActions (round-trip encryption)', () => {
121
+ it('should encrypt metadata fields in createAction when encryptWalletMetadata=true, then decrypt them in listActions', async () => {
122
+ const manager = new WalletPermissionsManager(
123
+ underlying,
124
+ 'admin.domain.com',
125
+ {
126
+ encryptWalletMetadata: true
127
+ }
128
+ )
129
+ manager.bindCallback('onSpendingAuthorizationRequested', x => {
130
+ manager.grantPermission({ requestID: x.requestID, ephemeral: true })
131
+ })
132
+
133
+ // We prepare an action with multiple metadata fields
134
+ const actionDescription = 'User Action #1: Doing something important'
135
+ const inputDesc = 'Some input desc'
136
+ const outputDesc = 'Some output desc'
137
+ const customInstr = 'Some custom instructions'
138
+
139
+ // Our createAction call
140
+ await manager.createAction(
141
+ {
142
+ description: actionDescription,
143
+ inputs: [
144
+ {
145
+ outpoint: 'txid1.0',
146
+ unlockingScriptLength: 73,
147
+ inputDescription: inputDesc
148
+ }
149
+ ],
150
+ outputs: [
151
+ {
152
+ lockingScript: 'OP_RETURN 1234',
153
+ satoshis: 500,
154
+ outputDescription: outputDesc,
155
+ customInstructions: customInstr
156
+ }
157
+ ]
158
+ },
159
+ 'nonadmin.com'
160
+ )
161
+
162
+ // 1) Confirm underlying.encrypt() was called for each field that is non-empty:
163
+ // - description
164
+ // - inputDescription
165
+ // - outputDescription
166
+ // - customInstructions
167
+ // (We can't be certain how many times exactly, but we can check that it was at least 4.)
168
+ expect(underlying.encrypt).toHaveBeenCalledTimes(4)
169
+
170
+ // 2) Now we simulate listing actions. We'll have the manager call underlying.listActions.
171
+ // Our mock underlying wallet returns an empty array by default, so let's override it
172
+ // to return the "encrypted" data that the manager gave it.
173
+ // But the manager doesn't store that data in the underlying wallet mock automatically.
174
+ // We'll just pretend that the wallet returns some data, and ensure the manager tries to decrypt it.
175
+ ;(underlying.listActions as any).mockResolvedValueOnce({
176
+ totalActions: 1,
177
+ actions: [
178
+ {
179
+ description: 'fake-encrypted-string-for-description',
180
+ inputs: [
181
+ {
182
+ outpoint: 'txid1.0',
183
+ inputDescription: 'fake-encrypted-string-for-inputDesc'
184
+ }
185
+ ],
186
+ outputs: [
187
+ {
188
+ lockingScript: 'OP_RETURN 1234',
189
+ satoshis: 500,
190
+ outputDescription: 'fake-encrypted-string-for-outputDesc',
191
+ customInstructions: 'fake-encrypted-string-for-customInstr'
192
+ }
193
+ ]
194
+ }
195
+ ]
196
+ })
197
+
198
+ // Also mock decrypt calls to simulate a correct round-trip
199
+ const decryptMock = underlying.decrypt as any
200
+ decryptMock.mockResolvedValueOnce({
201
+ plaintext: Array.from(actionDescription).map(c => c.charCodeAt(0))
202
+ })
203
+ decryptMock.mockResolvedValueOnce({
204
+ plaintext: Array.from(inputDesc).map(c => c.charCodeAt(0))
205
+ })
206
+ decryptMock.mockResolvedValueOnce({
207
+ plaintext: Array.from(outputDesc).map(c => c.charCodeAt(0))
208
+ })
209
+ decryptMock.mockResolvedValueOnce({
210
+ plaintext: Array.from(customInstr).map(c => c.charCodeAt(0))
211
+ })
212
+
213
+ const result = await (manager as any).listActions({}, 'nonadmin.com')
214
+
215
+ // We should get exactly 1 action
216
+ expect(result.actions.length).toBe(1)
217
+ const action = result.actions[0]
218
+
219
+ // The manager is expected to have decrypted each field
220
+ expect(action.description).toBe(actionDescription)
221
+ expect(action.inputs[0].inputDescription).toBe(inputDesc)
222
+ expect(action.outputs[0].outputDescription).toBe(outputDesc)
223
+ expect(action.outputs[0].customInstructions).toBe(customInstr)
224
+ })
225
+
226
+ it('should not encrypt metadata if encryptWalletMetadata=false, storing and retrieving plaintext', async () => {
227
+ const manager = new WalletPermissionsManager(
228
+ underlying,
229
+ 'admin.domain.com',
230
+ {
231
+ encryptWalletMetadata: false
232
+ }
233
+ )
234
+ manager.bindCallback('onSpendingAuthorizationRequested', x => {
235
+ manager.grantPermission({ requestID: x.requestID, ephemeral: true })
236
+ })
237
+
238
+ const actionDescription = 'Plaintext action description'
239
+ const inputDesc = 'Plaintext input desc'
240
+ const outputDesc = 'Plaintext output desc'
241
+ const customInstr = 'Plaintext instructions'
242
+ await manager.createAction(
243
+ {
244
+ description: actionDescription,
245
+ inputs: [
246
+ {
247
+ outpoint: 'someTx.0',
248
+ unlockingScriptLength: 73,
249
+ inputDescription: inputDesc
250
+ }
251
+ ],
252
+ outputs: [
253
+ {
254
+ lockingScript: 'OP_RETURN ABCD',
255
+ satoshis: 123,
256
+ outputDescription: outputDesc,
257
+ customInstructions: customInstr
258
+ }
259
+ ]
260
+ },
261
+ 'nonadmin.com'
262
+ )
263
+
264
+ // Because encryption is disabled, underlying.encrypt() is not called
265
+ expect(underlying.encrypt).not.toHaveBeenCalled()
266
+
267
+ // Simulate that the wallet actually stored them in plaintext and is returning them as-is
268
+ ;(underlying.listActions as any).mockResolvedValue({
269
+ totalActions: 1,
270
+ actions: [
271
+ {
272
+ description: actionDescription,
273
+ inputs: [
274
+ {
275
+ outpoint: 'someTx.0',
276
+ inputDescription: inputDesc
277
+ }
278
+ ],
279
+ outputs: [
280
+ {
281
+ lockingScript: 'OP_RETURN ABCD',
282
+ satoshis: 123,
283
+ outputDescription: outputDesc,
284
+ customInstructions: customInstr
285
+ }
286
+ ]
287
+ }
288
+ ]
289
+ })
290
+
291
+ // Decrypt is still called, because we try to decrypt regardless of whether encryption is enabled.
292
+ // This allows us to disable it on a wallet that had it in the past. The result is that when not encrypted,
293
+ // the plaintext is returned if decryption fails. If it was encrypted from metadata encryption being enabled in
294
+ // the past (even when not enabled now), we will still decrypt and see the correct plaintext rather than garbage.
295
+ // To simulate, we make decryption pass through.
296
+ underlying.decrypt.mockImplementation(x => x)
297
+ const listResult = await (manager as any).listActions({}, 'nonadmin.com')
298
+ expect(underlying.decrypt).toHaveBeenCalledTimes(4)
299
+
300
+ // Confirm the returned data is the same as originally provided (plaintext)
301
+ const [first] = listResult.actions
302
+ expect(first.description).toBe(actionDescription)
303
+ expect(first.inputs[0].inputDescription).toBe(inputDesc)
304
+ expect(first.outputs[0].outputDescription).toBe(outputDesc)
305
+ expect(first.outputs[0].customInstructions).toBe(customInstr)
306
+ })
307
+ })
308
+
309
+ describe('Integration Test for listOutputs decryption', () => {
310
+ it('should decrypt customInstructions in listOutputs if encryptWalletMetadata=true', async () => {
311
+ const manager = new WalletPermissionsManager(
312
+ underlying,
313
+ 'admin.domain.com',
314
+ {
315
+ encryptWalletMetadata: true
316
+ }
317
+ )
318
+ manager.bindCallback('onBasketAccessRequested', x => {
319
+ manager.grantPermission({ requestID: x.requestID, ephemeral: true })
320
+ })
321
+
322
+ // Suppose we have an output with custom instructions that was stored encrypted
323
+ ;(underlying.listOutputs as any).mockResolvedValue({
324
+ totalOutputs: 1,
325
+ outputs: [
326
+ {
327
+ outpoint: 'fakeTxid.0',
328
+ satoshis: 999,
329
+ lockingScript: 'OP_RETURN something',
330
+ basket: 'some-basket',
331
+ customInstructions: 'fake-encrypted-instructions-string'
332
+ }
333
+ ]
334
+ })
335
+
336
+ const originalInstr = 'Please do not reveal this data.'
337
+ // We'll mock decrypt() to interpret 'fake-encrypted-instructions-string' as a success
338
+ ;(underlying.decrypt as any).mockResolvedValueOnce({
339
+ plaintext: Array.from(originalInstr).map(ch => ch.charCodeAt(0))
340
+ })
341
+
342
+ const outputsResult = await manager.listOutputs(
343
+ {
344
+ basket: 'some-basket'
345
+ },
346
+ 'some-origin.com'
347
+ )
348
+
349
+ expect(outputsResult.outputs.length).toBe(1)
350
+ expect(outputsResult.outputs[0].customInstructions).toBe(originalInstr)
351
+
352
+ // Confirm we tried to decrypt
353
+ expect(underlying.decrypt).toHaveBeenCalledTimes(1)
354
+ expect(underlying.decrypt).toHaveBeenCalledWith(
355
+ {
356
+ ciphertext: expect.any(Array),
357
+ protocolID: [2, 'admin metadata encryption'],
358
+ keyID: '1'
359
+ },
360
+ 'admin.domain.com'
361
+ )
362
+ })
363
+
364
+ it('should fallback to the original ciphertext if decrypt fails in listOutputs', async () => {
365
+ const manager = new WalletPermissionsManager(
366
+ underlying,
367
+ 'admin.domain.com',
368
+ {
369
+ encryptWalletMetadata: true
370
+ }
371
+ )
372
+ manager.bindCallback('onBasketAccessRequested', x => {
373
+ manager.grantPermission({ requestID: x.requestID, ephemeral: true })
374
+ })
375
+ ;(underlying.listOutputs as any).mockResolvedValue({
376
+ totalOutputs: 1,
377
+ outputs: [
378
+ {
379
+ outpoint: 'fakeTxid.1',
380
+ satoshis: 500,
381
+ lockingScript: 'OP_RETURN something',
382
+ basket: 'some-basket',
383
+ customInstructions: 'bad-ciphertext-of-some-kind'
384
+ }
385
+ ]
386
+ })
387
+
388
+ // Force an error from decrypt
389
+ ;(underlying.decrypt as any).mockImplementationOnce(() => {
390
+ throw new Error('Failed to decrypt')
391
+ })
392
+
393
+ const outputsResult = await manager.listOutputs(
394
+ {
395
+ basket: 'some-basket'
396
+ },
397
+ 'some-origin.com'
398
+ )
399
+
400
+ expect(outputsResult.outputs.length).toBe(1)
401
+ // Should fall back to the original 'bad-ciphertext-of-some-kind'
402
+ expect(outputsResult.outputs[0].customInstructions).toBe(
403
+ 'bad-ciphertext-of-some-kind'
404
+ )
405
+ })
406
+ })
407
+ })
@@ -0,0 +1,283 @@
1
+ /**
2
+ * A permissions manager testing mock/stub file for:
3
+ * 1) The `@bsv/sdk` library: Transaction, LockingScript, PushDrop, Utils, Random, etc.
4
+ * 2) A BRC-100 `WalletInterface` (the underlying wallet).
5
+ *
6
+ * This file bypasses real validation/logic in `@bsv/sdk`, returning placeholders and
7
+ * stubs to prevent test-time errors such as "Invalid Atomic BEEF prefix."
8
+ */
9
+
10
+ /* ---------------------------------------------------------------------------
11
+ * 1) Partial Mocks for @bsv/sdk
12
+ * -------------------------------------------------------------------------*/
13
+
14
+ /**
15
+ * A minimal mock for `Transaction` that won't throw "Invalid Atomic BEEF prefix."
16
+ * We override the static methods so they do not do real parsing/validation.
17
+ */
18
+ export class MockTransaction {
19
+ public inputs: any[] = [{}]
20
+ public outputs: any[] = []
21
+ public fee: number = 0
22
+
23
+ constructor() {}
24
+ static fromAtomicBEEF() {
25
+ // Mocked below
26
+ }
27
+
28
+ static fromBEEF(beef: number[]): MockTransaction {
29
+ // Same approach as above
30
+ return new MockTransaction()
31
+ }
32
+
33
+ getFee(): number {
34
+ return this.fee
35
+ }
36
+ }
37
+
38
+ ;(MockTransaction as any).fromAtomicBEEF = jest.fn(() => {
39
+ // We skip real validation, returning a MockTransaction with minimal structure.
40
+ const tx = new MockTransaction()
41
+ return tx
42
+ })
43
+
44
+ /**
45
+ * Mocks for `LockingScript`. If your code calls e.g. LockingScript.fromHex, we can just
46
+ * store the hex and do nothing else.
47
+ */
48
+ export class MockLockingScript {
49
+ hex: string
50
+ constructor(hex: string) {
51
+ this.hex = hex
52
+ }
53
+ public toHex(): string {
54
+ return this.hex
55
+ }
56
+ static fromHex(hex: string): MockLockingScript {
57
+ return new MockLockingScript(hex)
58
+ }
59
+ }
60
+
61
+ /**
62
+ * We stub out all methods: `decode()`, `lock()`, `unlock()`.
63
+ */
64
+ export class MockPushDrop {
65
+ // Typically we might store the wallet reference, but we can skip for now.
66
+ constructor() {
67
+ //
68
+ }
69
+
70
+ // Decodes a LockingScript into some {fields: number[][], protocol...} or undefined
71
+ static decode(script: MockLockingScript): { fields: number[][] } | undefined {
72
+ // If you rely on a real format, parse or store a pattern.
73
+ // For now, returning a minimal stub: empty fields
74
+ if (!script || !script.hex) return undefined
75
+ if (script.hex.includes('some script')) {
76
+ // When needed, return some fields
77
+ return {
78
+ fields: [
79
+ [],
80
+ [],
81
+ [],
82
+ [],
83
+ [],
84
+ [],
85
+ [] // 7 fields should always be enough...
86
+ ]
87
+ }
88
+ }
89
+ // Just pretend we always decode to a single empty field array
90
+ return { fields: [] }
91
+ }
92
+
93
+ lock(
94
+ fields: number[][],
95
+ protocolID: [number, string],
96
+ keyID: string,
97
+ counterparty: string,
98
+ singleSignature: boolean,
99
+ anyoneCanPay: boolean
100
+ ): MockLockingScript {
101
+ return new MockLockingScript('deadbeef')
102
+ }
103
+
104
+ unlock(
105
+ protocolID: [number, string],
106
+ keyID: string,
107
+ counterparty: string,
108
+ sighashType: string,
109
+ enforceReplayProtection: boolean,
110
+ sigSize: number,
111
+ lockingScript: MockLockingScript
112
+ ): {
113
+ sign: (tx: MockTransaction, vin: number) => Promise<MockLockingScript>
114
+ } {
115
+ // In real usage, it would handle signature logic. We'll return a minimal stub.
116
+ return {
117
+ sign: async (tx: MockTransaction, vin: number) => {
118
+ // produce a minimal unlocking script
119
+ return new MockLockingScript('mockUnlockingScript')
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Mocks for Utils, e.g. toHex, toUTF8, fromUTF8, etc.
127
+ * We can provide minimal stubs that won't break your code.
128
+ */
129
+ export const MockUtils = {
130
+ toHex: (data: number[]) => {
131
+ // Converts an array of numbers to a hexadecimal string.
132
+ return data.map(num => num.toString(16).padStart(2, '0')).join('')
133
+ },
134
+
135
+ toArray: (str: string, encoding = 'utf8') => {
136
+ // Converts a string to an array of numbers based on the encoding.
137
+ if (encoding === 'hex') {
138
+ const arr: number[] = []
139
+ for (let i = 0; i < str.length; i += 2) {
140
+ arr.push(parseInt(str.substr(i, 2), 16))
141
+ }
142
+ return arr
143
+ } else if (encoding === 'base64') {
144
+ const binaryStr = atob(str)
145
+ return Array.from(binaryStr, char => char.charCodeAt(0))
146
+ } else if (encoding === 'utf8') {
147
+ return Array.from(str, char => char.charCodeAt(0))
148
+ } else {
149
+ throw new Error('Unsupported encoding: ' + encoding)
150
+ }
151
+ },
152
+
153
+ toUTF8: (arr: number[]) => {
154
+ // Converts an array of numbers to a UTF-8 string.
155
+ return String.fromCharCode(...arr)
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Mocks for Random
161
+ */
162
+ export const MockRandom = (size: number): number[] => {
163
+ return [...require('crypto').randomBytes(size)]
164
+ }
165
+
166
+ /**
167
+ * Overriding the real classes with our mocks.
168
+ */
169
+ export const MockedBSV_SDK = {
170
+ Transaction: MockTransaction,
171
+ LockingScript: MockLockingScript,
172
+ PushDrop: MockPushDrop,
173
+ Utils: MockUtils,
174
+ Random: MockRandom
175
+ }
176
+
177
+ /* ---------------------------------------------------------------------------
178
+ * 2) A full mock for the BRC-100 WalletInterface
179
+ * -------------------------------------------------------------------------*/
180
+
181
+ /**
182
+ * A helper function returning a Jest-mocked `WalletInterface`.
183
+ * This ensures all required methods exist and return plausible values.
184
+ *
185
+ * - By default, `createAction` returns a signableTransaction with empty arrays,
186
+ * so that the manager can call `Transaction.fromAtomicBEEF([])` without throwing.
187
+ * - You can override or chain .mockResolvedValueOnce(...) inside individual tests
188
+ * if you want more specific behavior in certain test steps.
189
+ */
190
+ export function mockUnderlyingWallet(): jest.Mocked<any> {
191
+ return {
192
+ getPublicKey: jest.fn().mockResolvedValue({ publicKey: '029999...' }),
193
+ revealCounterpartyKeyLinkage: jest.fn().mockResolvedValue({
194
+ encryptedLinkage: [1, 2, 3],
195
+ encryptedLinkageProof: [4, 5, 6],
196
+ prover: '02abcdef...',
197
+ verifier: '02cccccc...',
198
+ counterparty: '02bbbbbb...',
199
+ revelationTime: new Date().toISOString()
200
+ }),
201
+ revealSpecificKeyLinkage: jest.fn().mockResolvedValue({
202
+ encryptedLinkage: [1, 2, 3],
203
+ encryptedLinkageProof: [4, 5, 6],
204
+ prover: '02abcdef...',
205
+ verifier: '02cccccc...',
206
+ counterparty: '02bbbbbb...',
207
+ protocolID: [1, 'test-protocol'],
208
+ keyID: 'testKey',
209
+ proofType: 1
210
+ }),
211
+ encrypt: jest.fn().mockResolvedValue({ ciphertext: [42, 42, 42] }),
212
+ decrypt: jest.fn().mockResolvedValue({ plaintext: [42, 42] }),
213
+ createHmac: jest.fn().mockResolvedValue({ hmac: [0xaa] }),
214
+ verifyHmac: jest.fn().mockResolvedValue({ valid: true }),
215
+ createSignature: jest.fn().mockResolvedValue({ signature: [0x30, 0x44] }),
216
+ verifySignature: jest.fn().mockResolvedValue({ valid: true }),
217
+
218
+ createAction: jest.fn(async x => {
219
+ if (x.options && x.options.signAndProcess === true) {
220
+ return {
221
+ tx: []
222
+ }
223
+ }
224
+ return {
225
+ signableTransaction: {
226
+ tx: [],
227
+ reference: 'mockReference'
228
+ }
229
+ }
230
+ }),
231
+ signAction: jest.fn().mockResolvedValue({
232
+ txid: 'fake-txid',
233
+ tx: []
234
+ }),
235
+ abortAction: jest.fn().mockResolvedValue({ aborted: true }),
236
+ listActions: jest.fn().mockResolvedValue({
237
+ totalActions: 0,
238
+ actions: []
239
+ }),
240
+ internalizeAction: jest.fn().mockResolvedValue({ accepted: true }),
241
+ listOutputs: jest.fn().mockResolvedValue({
242
+ totalOutputs: 0,
243
+ outputs: []
244
+ }),
245
+ relinquishOutput: jest.fn().mockResolvedValue({ relinquished: true }),
246
+
247
+ acquireCertificate: jest.fn().mockResolvedValue({
248
+ type: 'some-cert-type',
249
+ subject: '02aaaaaaaaaa...',
250
+ serialNumber: 'serial123',
251
+ certifier: '02ccccccccccc...',
252
+ revocationOutpoint: 'some.txid.1',
253
+ signature: 'deadbeef',
254
+ fields: { name: 'Alice', dob: '1990-01-01' }
255
+ }),
256
+ listCertificates: jest.fn().mockResolvedValue({
257
+ totalCertificates: 0,
258
+ certificates: []
259
+ }),
260
+ proveCertificate: jest.fn().mockResolvedValue({
261
+ keyringForVerifier: {},
262
+ certificate: undefined,
263
+ verifier: undefined
264
+ }),
265
+ relinquishCertificate: jest.fn().mockResolvedValue({ relinquished: true }),
266
+ discoverByIdentityKey: jest.fn().mockResolvedValue({
267
+ totalCertificates: 0,
268
+ certificates: []
269
+ }),
270
+ discoverByAttributes: jest.fn().mockResolvedValue({
271
+ totalCertificates: 0,
272
+ certificates: []
273
+ }),
274
+ isAuthenticated: jest.fn().mockResolvedValue({ authenticated: true }),
275
+ waitForAuthentication: jest.fn().mockResolvedValue({ authenticated: true }),
276
+ getHeight: jest.fn().mockResolvedValue({ height: 777777 }),
277
+ getHeaderForHeight: jest.fn().mockResolvedValue({
278
+ header: '000000000000abc...'
279
+ }),
280
+ getNetwork: jest.fn().mockResolvedValue({ network: 'testnet' }),
281
+ getVersion: jest.fn().mockResolvedValue({ version: 'vendor-1.0.0' })
282
+ }
283
+ }