@bsv/wallet-toolbox 1.1.62 → 1.2.1

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 (185) hide show
  1. package/docs/client.md +2339 -182
  2. package/docs/wallet.md +2339 -182
  3. package/out/src/CWIStyleWalletManager.d.ts +417 -0
  4. package/out/src/CWIStyleWalletManager.d.ts.map +1 -0
  5. package/out/src/CWIStyleWalletManager.js +1153 -0
  6. package/out/src/CWIStyleWalletManager.js.map +1 -0
  7. package/out/src/SimpleWalletManager.d.ts +169 -0
  8. package/out/src/SimpleWalletManager.d.ts.map +1 -0
  9. package/out/src/SimpleWalletManager.js +315 -0
  10. package/out/src/SimpleWalletManager.js.map +1 -0
  11. package/out/src/Wallet.d.ts +6 -1
  12. package/out/src/Wallet.d.ts.map +1 -1
  13. package/out/src/Wallet.js +39 -7
  14. package/out/src/Wallet.js.map +1 -1
  15. package/out/src/WalletAuthenticationManager.d.ts +33 -0
  16. package/out/src/WalletAuthenticationManager.d.ts.map +1 -0
  17. package/out/src/WalletAuthenticationManager.js +110 -0
  18. package/out/src/WalletAuthenticationManager.js.map +1 -0
  19. package/out/src/WalletPermissionsManager.d.ts +575 -0
  20. package/out/src/WalletPermissionsManager.d.ts.map +1 -0
  21. package/out/src/WalletPermissionsManager.js +1789 -0
  22. package/out/src/WalletPermissionsManager.js.map +1 -0
  23. package/out/src/WalletSettingsManager.d.ts +59 -0
  24. package/out/src/WalletSettingsManager.d.ts.map +1 -0
  25. package/out/src/WalletSettingsManager.js +189 -0
  26. package/out/src/WalletSettingsManager.js.map +1 -0
  27. package/out/src/__tests/CWIStyleWalletManager.test.d.ts +2 -0
  28. package/out/src/__tests/CWIStyleWalletManager.test.d.ts.map +1 -0
  29. package/out/src/__tests/CWIStyleWalletManager.test.js +471 -0
  30. package/out/src/__tests/CWIStyleWalletManager.test.js.map +1 -0
  31. package/out/src/__tests/WalletPermissionsManager.callbacks.test.d.ts +2 -0
  32. package/out/src/__tests/WalletPermissionsManager.callbacks.test.d.ts.map +1 -0
  33. package/out/src/__tests/WalletPermissionsManager.callbacks.test.js +239 -0
  34. package/out/src/__tests/WalletPermissionsManager.callbacks.test.js.map +1 -0
  35. package/out/src/__tests/WalletPermissionsManager.checks.test.d.ts +2 -0
  36. package/out/src/__tests/WalletPermissionsManager.checks.test.d.ts.map +1 -0
  37. package/out/src/__tests/WalletPermissionsManager.checks.test.js +637 -0
  38. package/out/src/__tests/WalletPermissionsManager.checks.test.js.map +1 -0
  39. package/out/src/__tests/WalletPermissionsManager.encryption.test.d.ts +2 -0
  40. package/out/src/__tests/WalletPermissionsManager.encryption.test.d.ts.map +1 -0
  41. package/out/src/__tests/WalletPermissionsManager.encryption.test.js +295 -0
  42. package/out/src/__tests/WalletPermissionsManager.encryption.test.js.map +1 -0
  43. package/out/src/__tests/WalletPermissionsManager.fixtures.d.ts +83 -0
  44. package/out/src/__tests/WalletPermissionsManager.fixtures.d.ts.map +1 -0
  45. package/out/src/__tests/WalletPermissionsManager.fixtures.js +261 -0
  46. package/out/src/__tests/WalletPermissionsManager.fixtures.js.map +1 -0
  47. package/out/src/__tests/WalletPermissionsManager.flows.test.d.ts +2 -0
  48. package/out/src/__tests/WalletPermissionsManager.flows.test.d.ts.map +1 -0
  49. package/out/src/__tests/WalletPermissionsManager.flows.test.js +377 -0
  50. package/out/src/__tests/WalletPermissionsManager.flows.test.js.map +1 -0
  51. package/out/src/__tests/WalletPermissionsManager.initialization.test.d.ts +2 -0
  52. package/out/src/__tests/WalletPermissionsManager.initialization.test.d.ts.map +1 -0
  53. package/out/src/__tests/WalletPermissionsManager.initialization.test.js +227 -0
  54. package/out/src/__tests/WalletPermissionsManager.initialization.test.js.map +1 -0
  55. package/out/src/__tests/WalletPermissionsManager.proxying.test.d.ts +2 -0
  56. package/out/src/__tests/WalletPermissionsManager.proxying.test.d.ts.map +1 -0
  57. package/out/src/__tests/WalletPermissionsManager.proxying.test.js +566 -0
  58. package/out/src/__tests/WalletPermissionsManager.proxying.test.js.map +1 -0
  59. package/out/src/__tests/WalletPermissionsManager.tokens.test.d.ts +2 -0
  60. package/out/src/__tests/WalletPermissionsManager.tokens.test.d.ts.map +1 -0
  61. package/out/src/__tests/WalletPermissionsManager.tokens.test.js +454 -0
  62. package/out/src/__tests/WalletPermissionsManager.tokens.test.js.map +1 -0
  63. package/out/src/index.all.d.ts +9 -0
  64. package/out/src/index.all.d.ts.map +1 -1
  65. package/out/src/index.all.js +9 -0
  66. package/out/src/index.all.js.map +1 -1
  67. package/out/src/index.client.d.ts +9 -0
  68. package/out/src/index.client.d.ts.map +1 -1
  69. package/out/src/index.client.js +9 -0
  70. package/out/src/index.client.js.map +1 -1
  71. package/out/src/sdk/CertOpsWallet.d.ts +7 -0
  72. package/out/src/sdk/CertOpsWallet.d.ts.map +1 -0
  73. package/out/src/sdk/CertOpsWallet.js +3 -0
  74. package/out/src/sdk/CertOpsWallet.js.map +1 -0
  75. package/out/src/sdk/__test/CertificateLifeCycle.test.js +19 -82
  76. package/out/src/sdk/__test/CertificateLifeCycle.test.js.map +1 -1
  77. package/out/src/sdk/index.d.ts +1 -1
  78. package/out/src/sdk/index.d.ts.map +1 -1
  79. package/out/src/sdk/index.js +1 -1
  80. package/out/src/sdk/index.js.map +1 -1
  81. package/out/src/sdk/validationHelpers.d.ts.map +1 -1
  82. package/out/src/sdk/validationHelpers.js +13 -12
  83. package/out/src/sdk/validationHelpers.js.map +1 -1
  84. package/out/src/services/__tests/bitrails.test.js +7 -2
  85. package/out/src/services/__tests/bitrails.test.js.map +1 -1
  86. package/out/src/services/providers/__tests/WhatsOnChain.test.js +3 -3
  87. package/out/src/services/providers/__tests/WhatsOnChain.test.js.map +1 -1
  88. package/out/src/signer/methods/proveCertificate.d.ts.map +1 -1
  89. package/out/src/signer/methods/proveCertificate.js +3 -19
  90. package/out/src/signer/methods/proveCertificate.js.map +1 -1
  91. package/out/src/storage/__test/WalletStorageManager.test.js +1 -1
  92. package/out/src/storage/__test/WalletStorageManager.test.js.map +1 -1
  93. package/out/src/storage/remoting/StorageClient.d.ts +2 -2
  94. package/out/src/storage/remoting/StorageClient.d.ts.map +1 -1
  95. package/out/src/storage/remoting/StorageClient.js +1 -1
  96. package/out/src/storage/remoting/StorageClient.js.map +1 -1
  97. package/out/src/utility/identityUtils.d.ts +31 -0
  98. package/out/src/utility/identityUtils.d.ts.map +1 -0
  99. package/out/src/utility/identityUtils.js +116 -0
  100. package/out/src/utility/identityUtils.js.map +1 -0
  101. package/out/src/wab-client/WABClient.d.ts +49 -0
  102. package/out/src/wab-client/WABClient.d.ts.map +1 -0
  103. package/out/src/wab-client/WABClient.js +83 -0
  104. package/out/src/wab-client/WABClient.js.map +1 -0
  105. package/out/src/wab-client/__tests/WABClient.man.test.d.ts +2 -0
  106. package/out/src/wab-client/__tests/WABClient.man.test.d.ts.map +1 -0
  107. package/out/src/wab-client/__tests/WABClient.man.test.js +52 -0
  108. package/out/src/wab-client/__tests/WABClient.man.test.js.map +1 -0
  109. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts +34 -0
  110. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts.map +1 -0
  111. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js +16 -0
  112. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js.map +1 -0
  113. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.d.ts +7 -0
  114. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.d.ts.map +1 -0
  115. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.js +36 -0
  116. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.js.map +1 -0
  117. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts +28 -0
  118. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts.map +1 -0
  119. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js +69 -0
  120. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js.map +1 -0
  121. package/out/test/Wallet/action/internalizeAction.a.test.js +1 -1
  122. package/out/test/Wallet/action/internalizeAction.a.test.js.map +1 -1
  123. package/out/test/Wallet/certificate/acquireCertificate.test.js +26 -29
  124. package/out/test/Wallet/certificate/acquireCertificate.test.js.map +1 -1
  125. package/out/test/storage/KnexMigrations.test.js +1 -1
  126. package/out/test/storage/KnexMigrations.test.js.map +1 -1
  127. package/out/test/storage/update.test.js +1 -1
  128. package/out/test/storage/update.test.js.map +1 -1
  129. package/out/test/utils/TestUtilsWalletStorage.d.ts +9 -5
  130. package/out/test/utils/TestUtilsWalletStorage.d.ts.map +1 -1
  131. package/out/test/utils/TestUtilsWalletStorage.js +15 -9
  132. package/out/test/utils/TestUtilsWalletStorage.js.map +1 -1
  133. package/out/test/wallet/action/internalizeAction.test.js +1 -1
  134. package/out/test/wallet/action/internalizeAction.test.js.map +1 -1
  135. package/out/test/wallet/list/listActions2.test.js +1 -1
  136. package/out/test/wallet/list/listActions2.test.js.map +1 -1
  137. package/out/test/wallet/sync/Wallet.sync.test.js +1 -1
  138. package/out/test/wallet/sync/Wallet.sync.test.js.map +1 -1
  139. package/out/tsconfig.all.tsbuildinfo +1 -1
  140. package/package.json +3 -4
  141. package/src/CWIStyleWalletManager.ts +1738 -0
  142. package/src/SimpleWalletManager.ts +526 -0
  143. package/src/Wallet.ts +70 -7
  144. package/src/WalletAuthenticationManager.ts +150 -0
  145. package/src/WalletPermissionsManager.ts +2424 -0
  146. package/src/WalletSettingsManager.ts +243 -0
  147. package/src/__tests/CWIStyleWalletManager.test.ts +604 -0
  148. package/src/__tests/WalletPermissionsManager.callbacks.test.ts +323 -0
  149. package/src/__tests/WalletPermissionsManager.checks.test.ts +839 -0
  150. package/src/__tests/WalletPermissionsManager.encryption.test.ts +370 -0
  151. package/src/__tests/WalletPermissionsManager.fixtures.ts +284 -0
  152. package/src/__tests/WalletPermissionsManager.flows.test.ts +457 -0
  153. package/src/__tests/WalletPermissionsManager.initialization.test.ts +300 -0
  154. package/src/__tests/WalletPermissionsManager.proxying.test.ts +706 -0
  155. package/src/__tests/WalletPermissionsManager.tokens.test.ts +546 -0
  156. package/src/index.all.ts +9 -0
  157. package/src/index.client.ts +9 -0
  158. package/src/sdk/CertOpsWallet.ts +18 -0
  159. package/src/sdk/__test/CertificateLifeCycle.test.ts +66 -113
  160. package/src/sdk/index.ts +1 -1
  161. package/src/sdk/validationHelpers.ts +12 -11
  162. package/src/services/__tests/bitrails.test.ts +7 -2
  163. package/src/services/providers/__tests/WhatsOnChain.test.ts +3 -3
  164. package/src/signer/methods/proveCertificate.ts +14 -21
  165. package/src/storage/__test/WalletStorageManager.test.ts +1 -1
  166. package/src/storage/remoting/StorageClient.ts +4 -4
  167. package/src/utility/identityUtils.ts +159 -0
  168. package/src/wab-client/WABClient.ts +94 -0
  169. package/src/wab-client/__tests/WABClient.man.test.ts +59 -0
  170. package/src/wab-client/auth-method-interactors/AuthMethodInteractor.ts +47 -0
  171. package/src/wab-client/auth-method-interactors/PersonaIDInteractor.ts +35 -0
  172. package/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.ts +72 -0
  173. package/test/Wallet/action/internalizeAction.a.test.ts +1 -1
  174. package/test/Wallet/certificate/acquireCertificate.test.ts +89 -30
  175. package/test/storage/KnexMigrations.test.ts +1 -1
  176. package/test/storage/update.test.ts +1 -1
  177. package/test/utils/TestUtilsWalletStorage.ts +24 -13
  178. package/test/wallet/action/internalizeAction.test.ts +1 -1
  179. package/test/wallet/list/listActions2.test.ts +1 -1
  180. package/test/wallet/sync/Wallet.sync.test.ts +1 -1
  181. package/out/src/sdk/CertOps.d.ts +0 -66
  182. package/out/src/sdk/CertOps.d.ts.map +0 -1
  183. package/out/src/sdk/CertOps.js +0 -190
  184. package/out/src/sdk/CertOps.js.map +0 -1
  185. package/src/sdk/CertOps.ts +0 -274
@@ -0,0 +1,370 @@
1
+ import { mockUnderlyingWallet, MockedBSV_SDK } from './WalletPermissionsManager.fixtures'
2
+ import { WalletPermissionsManager } from '../WalletPermissionsManager'
3
+ import { jest } from '@jest/globals'
4
+
5
+ jest.mock('@bsv/sdk', () => MockedBSV_SDK)
6
+
7
+ describe('WalletPermissionsManager - Metadata Encryption & Decryption', () => {
8
+ let underlying: ReturnType<typeof mockUnderlyingWallet>
9
+
10
+ beforeEach(() => {
11
+ // Create a fresh underlying mock wallet before each test
12
+ underlying = mockUnderlyingWallet()
13
+ })
14
+
15
+ afterEach(() => {
16
+ jest.clearAllMocks()
17
+ })
18
+
19
+ describe('Unit Tests for metadata encryption helpers', () => {
20
+ it('should call underlying.encrypt() with the correct protocol and key when encryptWalletMetadata=true', async () => {
21
+ const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', {
22
+ encryptWalletMetadata: true
23
+ })
24
+
25
+ const plaintext = 'Hello, world!'
26
+ await manager['maybeEncryptMetadata'](plaintext)
27
+
28
+ // We expect underlying.encrypt() to have been called exactly once
29
+ expect(underlying.encrypt).toHaveBeenCalledTimes(1)
30
+
31
+ // Check that the call was with the correct protocol ID and key
32
+ expect(underlying.encrypt).toHaveBeenCalledWith(
33
+ {
34
+ plaintext: expect.any(Array), // byte array version of 'Hello, world!'
35
+ protocolID: [2, 'admin metadata encryption'],
36
+ keyID: '1'
37
+ },
38
+ 'admin.domain.com'
39
+ )
40
+ })
41
+
42
+ it('should NOT call underlying.encrypt() if encryptWalletMetadata=false', async () => {
43
+ const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', {
44
+ encryptWalletMetadata: false
45
+ })
46
+
47
+ const plaintext = 'No encryption needed!'
48
+ const result = await manager['maybeEncryptMetadata'](plaintext)
49
+
50
+ expect(result).toBe(plaintext)
51
+ expect(underlying.encrypt).not.toHaveBeenCalled()
52
+ })
53
+
54
+ it('should call underlying.decrypt() with correct protocol and key, returning plaintext on success', async () => {
55
+ const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', {
56
+ encryptWalletMetadata: true
57
+ })
58
+
59
+ // Underlying decrypt mock returns { plaintext: [42, 42] } by default
60
+ // which would become "**" if using our ASCII interpretation
61
+ ;(underlying.decrypt as any).mockResolvedValueOnce({
62
+ plaintext: [72, 105] // 'Hi'
63
+ })
64
+
65
+ const ciphertext = 'random-string-representing-ciphertext'
66
+ const result = await manager['maybeDecryptMetadata'](ciphertext)
67
+
68
+ // We expect underlying.decrypt() to have been called
69
+ expect(underlying.decrypt).toHaveBeenCalledTimes(1)
70
+ expect(underlying.decrypt).toHaveBeenCalledWith(
71
+ {
72
+ ciphertext: expect.any(Array), // byte array version of ciphertext
73
+ protocolID: [2, 'admin metadata encryption'],
74
+ keyID: '1'
75
+ },
76
+ 'admin.domain.com'
77
+ )
78
+
79
+ // The manager returns the decrypted UTF-8 string
80
+ expect(result).toBe('Hi')
81
+ })
82
+
83
+ it('should fallback to original string if underlying.decrypt() fails', async () => {
84
+ const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', {
85
+ encryptWalletMetadata: true
86
+ })
87
+
88
+ // Make underlying.decrypt() throw an error to simulate failure
89
+ ;(underlying.decrypt as any).mockImplementationOnce(() => {
90
+ throw new Error('Decryption error')
91
+ })
92
+
93
+ const ciphertext = 'this-was-not-valid-for-decryption'
94
+ const result = await manager['maybeDecryptMetadata'](ciphertext)
95
+
96
+ // The manager should return the original ciphertext if decryption throws
97
+ expect(result).toBe(ciphertext)
98
+ })
99
+ })
100
+
101
+ describe('Integration Tests for createAction + listActions (round-trip encryption)', () => {
102
+ it('should encrypt metadata fields in createAction when encryptWalletMetadata=true, then decrypt them in listActions', async () => {
103
+ const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', {
104
+ encryptWalletMetadata: true
105
+ })
106
+ manager.bindCallback('onSpendingAuthorizationRequested', x => {
107
+ manager.grantPermission({ requestID: x.requestID, ephemeral: true })
108
+ })
109
+
110
+ // We prepare an action with multiple metadata fields
111
+ const actionDescription = 'User Action #1: Doing something important'
112
+ const inputDesc = 'Some input desc'
113
+ const outputDesc = 'Some output desc'
114
+ const customInstr = 'Some custom instructions'
115
+
116
+ // Our createAction call
117
+ await manager.createAction(
118
+ {
119
+ description: actionDescription,
120
+ inputs: [
121
+ {
122
+ outpoint: '0231.0',
123
+ unlockingScriptLength: 73,
124
+ inputDescription: inputDesc
125
+ }
126
+ ],
127
+ outputs: [
128
+ {
129
+ lockingScript: '561234',
130
+ satoshis: 500,
131
+ outputDescription: outputDesc,
132
+ customInstructions: customInstr
133
+ }
134
+ ]
135
+ },
136
+ 'nonadmin.com'
137
+ )
138
+
139
+ // 1) Confirm underlying.encrypt() was called for each field that is non-empty:
140
+ // - description
141
+ // - inputDescription
142
+ // - outputDescription
143
+ // - customInstructions
144
+ // (We can't be certain how many times exactly, but we can check that it was at least 4.)
145
+ expect(underlying.encrypt).toHaveBeenCalledTimes(4)
146
+
147
+ // 2) Now we simulate listing actions. We'll have the manager call underlying.listActions.
148
+ // Our mock underlying wallet returns an empty array by default, so let's override it
149
+ // to return the "encrypted" data that the manager gave it.
150
+ // But the manager doesn't store that data in the underlying wallet mock automatically.
151
+ // We'll just pretend that the wallet returns some data, and ensure the manager tries to decrypt it.
152
+ ;(underlying.listActions as any).mockResolvedValueOnce({
153
+ totalActions: 1,
154
+ actions: [
155
+ {
156
+ description: 'fake-encrypted-string-for-description',
157
+ inputs: [
158
+ {
159
+ outpoint: 'txid1.0',
160
+ inputDescription: 'fake-encrypted-string-for-inputDesc'
161
+ }
162
+ ],
163
+ outputs: [
164
+ {
165
+ lockingScript: 'OP_RETURN 1234',
166
+ satoshis: 500,
167
+ outputDescription: 'fake-encrypted-string-for-outputDesc',
168
+ customInstructions: 'fake-encrypted-string-for-customInstr'
169
+ }
170
+ ]
171
+ }
172
+ ]
173
+ })
174
+
175
+ // Also mock decrypt calls to simulate a correct round-trip
176
+ const decryptMock = underlying.decrypt as any
177
+ decryptMock.mockResolvedValueOnce({
178
+ plaintext: Array.from(actionDescription).map(c => c.charCodeAt(0))
179
+ })
180
+ decryptMock.mockResolvedValueOnce({
181
+ plaintext: Array.from(inputDesc).map(c => c.charCodeAt(0))
182
+ })
183
+ decryptMock.mockResolvedValueOnce({
184
+ plaintext: Array.from(outputDesc).map(c => c.charCodeAt(0))
185
+ })
186
+ decryptMock.mockResolvedValueOnce({
187
+ plaintext: Array.from(customInstr).map(c => c.charCodeAt(0))
188
+ })
189
+
190
+ const result = await (manager as any).listActions({}, 'nonadmin.com')
191
+
192
+ // We should get exactly 1 action
193
+ expect(result.actions.length).toBe(1)
194
+ const action = result.actions[0]
195
+
196
+ // The manager is expected to have decrypted each field
197
+ expect(action.description).toBe(actionDescription)
198
+ expect(action.inputs[0].inputDescription).toBe(inputDesc)
199
+ expect(action.outputs[0].outputDescription).toBe(outputDesc)
200
+ expect(action.outputs[0].customInstructions).toBe(customInstr)
201
+ })
202
+
203
+ it('should not encrypt metadata if encryptWalletMetadata=false, storing and retrieving plaintext', async () => {
204
+ const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', {
205
+ encryptWalletMetadata: false
206
+ })
207
+ manager.bindCallback('onSpendingAuthorizationRequested', x => {
208
+ manager.grantPermission({ requestID: x.requestID, ephemeral: true })
209
+ })
210
+
211
+ const actionDescription = 'Plaintext action description'
212
+ const inputDesc = 'Plaintext input desc'
213
+ const outputDesc = 'Plaintext output desc'
214
+ const customInstr = 'Plaintext instructions'
215
+ await manager.createAction(
216
+ {
217
+ description: actionDescription,
218
+ inputs: [
219
+ {
220
+ outpoint: '9876.0',
221
+ unlockingScriptLength: 73,
222
+ inputDescription: inputDesc
223
+ }
224
+ ],
225
+ outputs: [
226
+ {
227
+ lockingScript: 'ABCD',
228
+ satoshis: 123,
229
+ outputDescription: outputDesc,
230
+ customInstructions: customInstr
231
+ }
232
+ ]
233
+ },
234
+ 'nonadmin.com'
235
+ )
236
+
237
+ // Because encryption is disabled, underlying.encrypt() is not called
238
+ expect(underlying.encrypt).not.toHaveBeenCalled()
239
+
240
+ // Simulate that the wallet actually stored them in plaintext and is returning them as-is
241
+ ;(underlying.listActions as any).mockResolvedValue({
242
+ totalActions: 1,
243
+ actions: [
244
+ {
245
+ description: actionDescription,
246
+ inputs: [
247
+ {
248
+ outpoint: '0123.0',
249
+ inputDescription: inputDesc
250
+ }
251
+ ],
252
+ outputs: [
253
+ {
254
+ lockingScript: 'ABCD',
255
+ satoshis: 123,
256
+ outputDescription: outputDesc,
257
+ customInstructions: customInstr
258
+ }
259
+ ]
260
+ }
261
+ ]
262
+ })
263
+
264
+ // Decrypt is still called, because we try to decrypt regardless of whether encryption is enabled.
265
+ // This allows us to disable it on a wallet that had it in the past. The result is that when not encrypted,
266
+ // the plaintext is returned if decryption fails. If it was encrypted from metadata encryption being enabled in
267
+ // the past (even when not enabled now), we will still decrypt and see the correct plaintext rather than garbage.
268
+ // To simulate, we make decryption pass through.
269
+ underlying.decrypt.mockImplementation(x => x)
270
+ const listResult = await (manager as any).listActions({}, 'nonadmin.com')
271
+ expect(underlying.decrypt).toHaveBeenCalledTimes(4)
272
+
273
+ // Confirm the returned data is the same as originally provided (plaintext)
274
+ const [first] = listResult.actions
275
+ expect(first.description).toBe(actionDescription)
276
+ expect(first.inputs[0].inputDescription).toBe(inputDesc)
277
+ expect(first.outputs[0].outputDescription).toBe(outputDesc)
278
+ expect(first.outputs[0].customInstructions).toBe(customInstr)
279
+ })
280
+ })
281
+
282
+ describe('Integration Test for listOutputs decryption', () => {
283
+ it('should decrypt customInstructions in listOutputs if encryptWalletMetadata=true', async () => {
284
+ const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', {
285
+ encryptWalletMetadata: true
286
+ })
287
+ manager.bindCallback('onBasketAccessRequested', x => {
288
+ manager.grantPermission({ requestID: x.requestID, ephemeral: true })
289
+ })
290
+
291
+ // Suppose we have an output with custom instructions that was stored encrypted
292
+ ;(underlying.listOutputs as any).mockResolvedValue({
293
+ totalOutputs: 1,
294
+ outputs: [
295
+ {
296
+ outpoint: 'fakeTxid.0',
297
+ satoshis: 999,
298
+ lockingScript: 'OP_RETURN something',
299
+ basket: 'some-basket',
300
+ customInstructions: 'fake-encrypted-instructions-string'
301
+ }
302
+ ]
303
+ })
304
+
305
+ const originalInstr = 'Please do not reveal this data.'
306
+ // We'll mock decrypt() to interpret 'fake-encrypted-instructions-string' as a success
307
+ ;(underlying.decrypt as any).mockResolvedValueOnce({
308
+ plaintext: Array.from(originalInstr).map(ch => ch.charCodeAt(0))
309
+ })
310
+
311
+ const outputsResult = await manager.listOutputs(
312
+ {
313
+ basket: 'some-basket'
314
+ },
315
+ 'some-origin.com'
316
+ )
317
+
318
+ expect(outputsResult.outputs.length).toBe(1)
319
+ expect(outputsResult.outputs[0].customInstructions).toBe(originalInstr)
320
+
321
+ // Confirm we tried to decrypt
322
+ expect(underlying.decrypt).toHaveBeenCalledTimes(1)
323
+ expect(underlying.decrypt).toHaveBeenCalledWith(
324
+ {
325
+ ciphertext: expect.any(Array),
326
+ protocolID: [2, 'admin metadata encryption'],
327
+ keyID: '1'
328
+ },
329
+ 'admin.domain.com'
330
+ )
331
+ })
332
+
333
+ it('should fallback to the original ciphertext if decrypt fails in listOutputs', async () => {
334
+ const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', {
335
+ encryptWalletMetadata: true
336
+ })
337
+ manager.bindCallback('onBasketAccessRequested', x => {
338
+ manager.grantPermission({ requestID: x.requestID, ephemeral: true })
339
+ })
340
+ ;(underlying.listOutputs as any).mockResolvedValue({
341
+ totalOutputs: 1,
342
+ outputs: [
343
+ {
344
+ outpoint: 'fakeTxid.1',
345
+ satoshis: 500,
346
+ lockingScript: 'OP_RETURN something',
347
+ basket: 'some-basket',
348
+ customInstructions: 'bad-ciphertext-of-some-kind'
349
+ }
350
+ ]
351
+ })
352
+
353
+ // Force an error from decrypt
354
+ ;(underlying.decrypt as any).mockImplementationOnce(() => {
355
+ throw new Error('Failed to decrypt')
356
+ })
357
+
358
+ const outputsResult = await manager.listOutputs(
359
+ {
360
+ basket: 'some-basket'
361
+ },
362
+ 'some-origin.com'
363
+ )
364
+
365
+ expect(outputsResult.outputs.length).toBe(1)
366
+ // Should fall back to the original 'bad-ciphertext-of-some-kind'
367
+ expect(outputsResult.outputs[0].customInstructions).toBe('bad-ciphertext-of-some-kind')
368
+ })
369
+ })
370
+ })
@@ -0,0 +1,284 @@
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
+ Certificate: null
176
+ }
177
+
178
+ /* ---------------------------------------------------------------------------
179
+ * 2) A full mock for the BRC-100 WalletInterface
180
+ * -------------------------------------------------------------------------*/
181
+
182
+ /**
183
+ * A helper function returning a Jest-mocked `WalletInterface`.
184
+ * This ensures all required methods exist and return plausible values.
185
+ *
186
+ * - By default, `createAction` returns a signableTransaction with empty arrays,
187
+ * so that the manager can call `Transaction.fromAtomicBEEF([])` without throwing.
188
+ * - You can override or chain .mockResolvedValueOnce(...) inside individual tests
189
+ * if you want more specific behavior in certain test steps.
190
+ */
191
+ export function mockUnderlyingWallet(): jest.Mocked<any> {
192
+ return {
193
+ getPublicKey: jest.fn().mockResolvedValue({ publicKey: '029999...' }),
194
+ revealCounterpartyKeyLinkage: jest.fn().mockResolvedValue({
195
+ encryptedLinkage: [1, 2, 3],
196
+ encryptedLinkageProof: [4, 5, 6],
197
+ prover: '02abcdef...',
198
+ verifier: '02cccccc...',
199
+ counterparty: '02bbbbbb...',
200
+ revelationTime: new Date().toISOString()
201
+ }),
202
+ revealSpecificKeyLinkage: jest.fn().mockResolvedValue({
203
+ encryptedLinkage: [1, 2, 3],
204
+ encryptedLinkageProof: [4, 5, 6],
205
+ prover: '02abcdef...',
206
+ verifier: '02cccccc...',
207
+ counterparty: '02bbbbbb...',
208
+ protocolID: [1, 'test-protocol'],
209
+ keyID: 'testKey',
210
+ proofType: 1
211
+ }),
212
+ encrypt: jest.fn().mockResolvedValue({ ciphertext: [42, 42, 42, 42, 42, 42, 42] }),
213
+ decrypt: jest.fn().mockResolvedValue({ plaintext: [42, 42, 42, 42, 42] }),
214
+ createHmac: jest.fn().mockResolvedValue({ hmac: [0xaa] }),
215
+ verifyHmac: jest.fn().mockResolvedValue({ valid: true }),
216
+ createSignature: jest.fn().mockResolvedValue({ signature: [0x30, 0x44] }),
217
+ verifySignature: jest.fn().mockResolvedValue({ valid: true }),
218
+
219
+ createAction: jest.fn(async x => {
220
+ if (x.options && x.options.signAndProcess === true) {
221
+ return {
222
+ tx: []
223
+ }
224
+ }
225
+ return {
226
+ signableTransaction: {
227
+ tx: [],
228
+ reference: 'mockReference'
229
+ }
230
+ }
231
+ }),
232
+ signAction: jest.fn().mockResolvedValue({
233
+ txid: 'fake-txid',
234
+ tx: []
235
+ }),
236
+ abortAction: jest.fn().mockResolvedValue({ aborted: true }),
237
+ listActions: jest.fn().mockResolvedValue({
238
+ totalActions: 0,
239
+ actions: []
240
+ }),
241
+ internalizeAction: jest.fn().mockResolvedValue({ accepted: true }),
242
+ listOutputs: jest.fn().mockResolvedValue({
243
+ totalOutputs: 0,
244
+ outputs: []
245
+ }),
246
+ relinquishOutput: jest.fn().mockResolvedValue({ relinquished: true }),
247
+
248
+ acquireCertificate: jest.fn().mockResolvedValue({
249
+ type: 'some-cert-type',
250
+ subject: '02aaaaaaaaaa...',
251
+ serialNumber: 'serial123',
252
+ certifier: '02ccccccccccc...',
253
+ revocationOutpoint: 'some.txid.1',
254
+ signature: 'deadbeef',
255
+ fields: { name: 'Alice', dob: '1990-01-01' }
256
+ }),
257
+ listCertificates: jest.fn().mockResolvedValue({
258
+ totalCertificates: 0,
259
+ certificates: []
260
+ }),
261
+ proveCertificate: jest.fn().mockResolvedValue({
262
+ keyringForVerifier: {},
263
+ certificate: undefined,
264
+ verifier: undefined
265
+ }),
266
+ relinquishCertificate: jest.fn().mockResolvedValue({ relinquished: true }),
267
+ discoverByIdentityKey: jest.fn().mockResolvedValue({
268
+ totalCertificates: 0,
269
+ certificates: []
270
+ }),
271
+ discoverByAttributes: jest.fn().mockResolvedValue({
272
+ totalCertificates: 0,
273
+ certificates: []
274
+ }),
275
+ isAuthenticated: jest.fn().mockResolvedValue({ authenticated: true }),
276
+ waitForAuthentication: jest.fn().mockResolvedValue({ authenticated: true }),
277
+ getHeight: jest.fn().mockResolvedValue({ height: 777777 }),
278
+ getHeaderForHeight: jest.fn().mockResolvedValue({
279
+ header: '000000000000abc...'
280
+ }),
281
+ getNetwork: jest.fn().mockResolvedValue({ network: 'testnet' }),
282
+ getVersion: jest.fn().mockResolvedValue({ version: 'vendor-1.0.0' })
283
+ }
284
+ }