@bsv/wallet-toolbox 1.6.40 → 1.6.42

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 (43) hide show
  1. package/docs/client.md +70 -30
  2. package/docs/services.md +1 -1
  3. package/docs/storage.md +34 -8
  4. package/docs/wallet.md +70 -30
  5. package/mobile/out/src/WalletPermissionsManager.d.ts +29 -0
  6. package/mobile/out/src/WalletPermissionsManager.d.ts.map +1 -1
  7. package/mobile/out/src/WalletPermissionsManager.js +597 -379
  8. package/mobile/out/src/WalletPermissionsManager.js.map +1 -1
  9. package/mobile/out/src/WalletSettingsManager.d.ts +1 -0
  10. package/mobile/out/src/WalletSettingsManager.d.ts.map +1 -1
  11. package/mobile/out/src/WalletSettingsManager.js +2 -1
  12. package/mobile/out/src/WalletSettingsManager.js.map +1 -1
  13. package/mobile/out/src/sdk/WalletError.d.ts.map +1 -1
  14. package/mobile/out/src/sdk/WalletError.js +3 -1
  15. package/mobile/out/src/sdk/WalletError.js.map +1 -1
  16. package/mobile/out/src/storage/WalletStorageManager.d.ts.map +1 -1
  17. package/mobile/out/src/storage/WalletStorageManager.js +3 -1
  18. package/mobile/out/src/storage/WalletStorageManager.js.map +1 -1
  19. package/mobile/package-lock.json +6 -6
  20. package/mobile/package.json +2 -2
  21. package/out/src/WalletPermissionsManager.d.ts +29 -0
  22. package/out/src/WalletPermissionsManager.d.ts.map +1 -1
  23. package/out/src/WalletPermissionsManager.js +597 -379
  24. package/out/src/WalletPermissionsManager.js.map +1 -1
  25. package/out/src/WalletSettingsManager.d.ts +1 -0
  26. package/out/src/WalletSettingsManager.d.ts.map +1 -1
  27. package/out/src/WalletSettingsManager.js +2 -1
  28. package/out/src/WalletSettingsManager.js.map +1 -1
  29. package/out/src/__tests/WalletPermissionsManager.tokens.test.js +1 -80
  30. package/out/src/__tests/WalletPermissionsManager.tokens.test.js.map +1 -1
  31. package/out/src/sdk/WalletError.d.ts.map +1 -1
  32. package/out/src/sdk/WalletError.js +3 -1
  33. package/out/src/sdk/WalletError.js.map +1 -1
  34. package/out/src/storage/WalletStorageManager.d.ts.map +1 -1
  35. package/out/src/storage/WalletStorageManager.js +3 -1
  36. package/out/src/storage/WalletStorageManager.js.map +1 -1
  37. package/out/tsconfig.all.tsbuildinfo +1 -1
  38. package/package.json +2 -2
  39. package/src/WalletPermissionsManager.ts +698 -454
  40. package/src/WalletSettingsManager.ts +3 -1
  41. package/src/__tests/WalletPermissionsManager.tokens.test.ts +1 -83
  42. package/src/sdk/WalletError.ts +9 -3
  43. package/src/storage/WalletStorageManager.ts +7 -5
@@ -103,6 +103,7 @@ export type GroupedPermissionEventHandler = (request: GroupedPermissionRequest)
103
103
  export interface PermissionRequest {
104
104
  type: 'protocol' | 'basket' | 'certificate' | 'spending'
105
105
  originator: string // The domain or FQDN of the requesting application
106
+ displayOriginator?: string // Optional raw/original originator string for UI purposes
106
107
  privileged?: boolean // For "protocol" or "certificate" usage, indicating privileged key usage
107
108
  protocolID?: WalletProtocol // For type='protocol': BRC-43 style (securityLevel, protocolName)
108
109
  counterparty?: string // For type='protocol': e.g. target public key or "self"/"anyone"
@@ -166,6 +167,13 @@ export interface PermissionToken {
166
167
  /** The originator domain or FQDN that is allowed to use this permission. */
167
168
  originator: string
168
169
 
170
+ /**
171
+ * The raw, unnormalized originator string captured at the time the permission
172
+ * token was created. This is preserved so we can continue to recognize legacy
173
+ * permissions that were stored with different casing or explicit default ports.
174
+ */
175
+ rawOriginator?: string
176
+
169
177
  /** The expiration time for this token in UNIX epoch seconds. (0 or omitted for spending authorizations, which are indefinite) */
170
178
  expiry: number
171
179
 
@@ -405,9 +413,18 @@ export class WalletPermissionsManager implements WalletInterface {
405
413
 
406
414
  /** Cache recently confirmed permissions to avoid repeated lookups. */
407
415
  private permissionCache: Map<string, { expiry: number; cachedAt: number }> = new Map()
416
+ private recentGrants: Map<string, number> = new Map()
408
417
 
409
418
  /** How long a cached permission remains valid (5 minutes). */
410
419
  private static readonly CACHE_TTL_MS = 5 * 60 * 1000
420
+ /** Window during which freshly granted permissions are auto-allowed (except spending). */
421
+ private static readonly RECENT_GRANT_COVER_MS = 15 * 1000
422
+
423
+ /** Default ports used when normalizing originator values. */
424
+ private static readonly DEFAULT_PORTS: Record<string, string> = {
425
+ 'http:': '80',
426
+ 'https:': '443'
427
+ }
411
428
 
412
429
  /**
413
430
  * Configuration that determines whether to skip or apply various checks and encryption.
@@ -423,7 +440,7 @@ export class WalletPermissionsManager implements WalletInterface {
423
440
  */
424
441
  constructor(underlyingWallet: WalletInterface, adminOriginator: string, config: PermissionsManagerConfig = {}) {
425
442
  this.underlying = underlyingWallet
426
- this.adminOriginator = adminOriginator
443
+ this.adminOriginator = this.normalizeOriginator(adminOriginator) || adminOriginator
427
444
 
428
445
  // Default all config options to true unless specified
429
446
  this.config = {
@@ -557,7 +574,7 @@ export class WalletPermissionsManager implements WalletInterface {
557
574
  // brand-new permission token
558
575
  await this.createPermissionOnChain(
559
576
  request,
560
- params.expiry || Math.floor(Date.now() / 1000) + 3600 * 24 * 30, // default 30-day expiry
577
+ params.expiry || 0, // default: never expires
561
578
  params.amount
562
579
  )
563
580
  } else {
@@ -565,7 +582,7 @@ export class WalletPermissionsManager implements WalletInterface {
565
582
  await this.renewPermissionOnChain(
566
583
  request.previousToken!,
567
584
  request,
568
- params.expiry || Math.floor(Date.now() / 1000) + 3600 * 24 * 30,
585
+ params.expiry || 0, // default: never expires
569
586
  params.amount
570
587
  )
571
588
  }
@@ -574,9 +591,10 @@ export class WalletPermissionsManager implements WalletInterface {
574
591
  // Only cache non-ephemeral permissions
575
592
  // Ephemeral permissions should not be cached as they are one-time authorizations
576
593
  if (!params.ephemeral) {
577
- const expiry = params.expiry || Math.floor(Date.now() / 1000) + 3600 * 24 * 30
594
+ const expiry = params.expiry || 0 // default: never expires
578
595
  const key = this.buildRequestKey(matching.request as PermissionRequest)
579
596
  this.cachePermission(key, expiry)
597
+ this.markRecentGrant(matching.request as PermissionRequest)
580
598
  }
581
599
  }
582
600
 
@@ -616,8 +634,13 @@ export class WalletPermissionsManager implements WalletInterface {
616
634
  throw new Error('Request ID not found.')
617
635
  }
618
636
 
619
- const originalRequest = matching.request as { originator: string; permissions: GroupedPermissions }
620
- const { originator, permissions: requestedPermissions } = originalRequest
637
+ const originalRequest = matching.request as {
638
+ originator: string
639
+ permissions: GroupedPermissions
640
+ displayOriginator?: string
641
+ }
642
+ const { originator, permissions: requestedPermissions, displayOriginator } = originalRequest
643
+ const originLookupValues = this.buildOriginatorLookupValues(displayOriginator, originator)
621
644
 
622
645
  // --- Validation: Ensure granted permissions are a subset of what was requested ---
623
646
  if (
@@ -643,7 +666,7 @@ export class WalletPermissionsManager implements WalletInterface {
643
666
  }
644
667
  // --- End Validation ---
645
668
 
646
- const expiry = params.expiry || Math.floor(Date.now() / 1000) + 3600 * 24 * 30 // 30-day default
669
+ const expiry = params.expiry || 0 // default: never expires
647
670
 
648
671
  if (params.granted.spendingAuthorization) {
649
672
  await this.createPermissionOnChain(
@@ -663,56 +686,52 @@ export class WalletPermissionsManager implements WalletInterface {
663
686
  false, // No privileged protocols allowed in groups for added security.
664
687
  p.protocolID,
665
688
  p.counterparty || 'self',
666
- true
689
+ true,
690
+ originLookupValues
667
691
  )
668
692
  if (token) {
669
- await this.renewPermissionOnChain(
670
- token,
671
- {
672
- type: 'protocol',
673
- originator,
674
- privileged: false, // No privileged protocols allowed in groups for added security.
675
- protocolID: p.protocolID,
676
- counterparty: p.counterparty || 'self',
677
- reason: p.description
678
- },
679
- expiry
680
- )
693
+ const request: PermissionRequest = {
694
+ type: 'protocol',
695
+ originator,
696
+ privileged: false, // No privileged protocols allowed in groups for added security.
697
+ protocolID: p.protocolID,
698
+ counterparty: p.counterparty || 'self',
699
+ reason: p.description
700
+ }
701
+ await this.renewPermissionOnChain(token, request, expiry)
702
+ this.markRecentGrant(request)
681
703
  } else {
682
- await this.createPermissionOnChain(
683
- {
684
- type: 'protocol',
685
- originator,
686
- privileged: false, // No privileged protocols allowed in groups for added security.
687
- protocolID: p.protocolID,
688
- counterparty: p.counterparty || 'self',
689
- reason: p.description
690
- },
691
- expiry
692
- )
704
+ const request: PermissionRequest = {
705
+ type: 'protocol',
706
+ originator,
707
+ privileged: false, // No privileged protocols allowed in groups for added security.
708
+ protocolID: p.protocolID,
709
+ counterparty: p.counterparty || 'self',
710
+ reason: p.description
711
+ }
712
+ await this.createPermissionOnChain(request, expiry)
713
+ this.markRecentGrant(request)
693
714
  }
694
715
  }
695
716
  for (const b of params.granted.basketAccess || []) {
696
- await this.createPermissionOnChain(
697
- { type: 'basket', originator, basket: b.basket, reason: b.description },
698
- expiry
699
- )
717
+ const request: PermissionRequest = { type: 'basket', originator, basket: b.basket, reason: b.description }
718
+ await this.createPermissionOnChain(request, expiry)
719
+ this.markRecentGrant(request)
700
720
  }
701
721
  for (const c of params.granted.certificateAccess || []) {
702
- await this.createPermissionOnChain(
703
- {
704
- type: 'certificate',
705
- originator,
706
- privileged: false, // No certificates on the privileged identity are allowed as part of groups.
707
- certificate: {
708
- verifier: c.verifierPublicKey,
709
- certType: c.type,
710
- fields: c.fields
711
- },
712
- reason: c.description
722
+ const request: PermissionRequest = {
723
+ type: 'certificate',
724
+ originator,
725
+ privileged: false, // No certificates on the privileged identity are allowed as part of groups.
726
+ certificate: {
727
+ verifier: c.verifierPublicKey,
728
+ certType: c.type,
729
+ fields: c.fields
713
730
  },
714
- expiry
715
- )
731
+ reason: c.description
732
+ }
733
+ await this.createPermissionOnChain(request, expiry)
734
+ this.markRecentGrant(request)
716
735
  }
717
736
 
718
737
  // Resolve all pending promises for this request
@@ -764,6 +783,8 @@ export class WalletPermissionsManager implements WalletInterface {
764
783
  seekPermission?: boolean
765
784
  usageType: 'signing' | 'encrypting' | 'hmac' | 'publicKey' | 'identityKey' | 'linkageRevelation' | 'generic'
766
785
  }): Promise<boolean> {
786
+ const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator)
787
+ originator = normalizedOriginator
767
788
  // 1) adminOriginator can do anything
768
789
  if (this.isAdminOriginator(originator)) return true
769
790
 
@@ -809,6 +830,9 @@ export class WalletPermissionsManager implements WalletInterface {
809
830
  if (this.isPermissionCached(cacheKey)) {
810
831
  return true
811
832
  }
833
+ if (this.isRecentlyGranted(cacheKey)) {
834
+ return true
835
+ }
812
836
 
813
837
  // 4) Attempt to find a valid token in the internal basket
814
838
  const token = await this.findProtocolToken(
@@ -816,7 +840,8 @@ export class WalletPermissionsManager implements WalletInterface {
816
840
  privileged,
817
841
  protocolID,
818
842
  counterparty,
819
- /*includeExpired=*/ true
843
+ /*includeExpired=*/ true,
844
+ lookupValues
820
845
  )
821
846
  if (token) {
822
847
  if (!this.isTokenExpired(token.expiry)) {
@@ -874,6 +899,8 @@ export class WalletPermissionsManager implements WalletInterface {
874
899
  seekPermission?: boolean
875
900
  usageType: 'insertion' | 'removal' | 'listing'
876
901
  }): Promise<boolean> {
902
+ const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator)
903
+ originator = normalizedOriginator
877
904
  if (this.isAdminOriginator(originator)) return true
878
905
  if (this.isAdminBasket(basket)) {
879
906
  throw new Error(`Basket “${basket}” is admin-only.`)
@@ -885,7 +912,10 @@ export class WalletPermissionsManager implements WalletInterface {
885
912
  if (this.isPermissionCached(cacheKey)) {
886
913
  return true
887
914
  }
888
- const token = await this.findBasketToken(originator, basket, true)
915
+ if (this.isRecentlyGranted(cacheKey)) {
916
+ return true
917
+ }
918
+ const token = await this.findBasketToken(originator, basket, true, lookupValues)
889
919
  if (token) {
890
920
  if (!this.isTokenExpired(token.expiry)) {
891
921
  this.cachePermission(cacheKey, token.expiry)
@@ -942,6 +972,8 @@ export class WalletPermissionsManager implements WalletInterface {
942
972
  seekPermission?: boolean
943
973
  usageType: 'disclosure'
944
974
  }): Promise<boolean> {
975
+ const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator)
976
+ originator = normalizedOriginator
945
977
  if (this.isAdminOriginator(originator)) return true
946
978
  if (usageType === 'disclosure' && !this.config.seekCertificateDisclosurePermissions) {
947
979
  return true
@@ -958,13 +990,17 @@ export class WalletPermissionsManager implements WalletInterface {
958
990
  if (this.isPermissionCached(cacheKey)) {
959
991
  return true
960
992
  }
993
+ if (this.isRecentlyGranted(cacheKey)) {
994
+ return true
995
+ }
961
996
  const token = await this.findCertificateToken(
962
997
  originator,
963
998
  privileged,
964
999
  verifier,
965
1000
  certType,
966
1001
  fields,
967
- /*includeExpired=*/ true
1002
+ /*includeExpired=*/ true,
1003
+ lookupValues
968
1004
  )
969
1005
  if (token) {
970
1006
  if (!this.isTokenExpired(token.expiry)) {
@@ -1021,6 +1057,8 @@ export class WalletPermissionsManager implements WalletInterface {
1021
1057
  reason?: string
1022
1058
  seekPermission?: boolean
1023
1059
  }): Promise<boolean> {
1060
+ const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator)
1061
+ originator = normalizedOriginator
1024
1062
  if (this.isAdminOriginator(originator)) return true
1025
1063
  if (!this.config.seekSpendingPermissions) {
1026
1064
  // We skip spending permission entirely
@@ -1030,7 +1068,7 @@ export class WalletPermissionsManager implements WalletInterface {
1030
1068
  if (this.isPermissionCached(cacheKey)) {
1031
1069
  return true
1032
1070
  }
1033
- const token = await this.findSpendingToken(originator)
1071
+ const token = await this.findSpendingToken(originator, lookupValues)
1034
1072
  if (token?.authorizedAmount) {
1035
1073
  // Check how much has been spent so far
1036
1074
  const spentSoFar = await this.querySpentSince(token)
@@ -1085,6 +1123,8 @@ export class WalletPermissionsManager implements WalletInterface {
1085
1123
  seekPermission?: boolean
1086
1124
  usageType: 'apply' | 'list'
1087
1125
  }): Promise<boolean> {
1126
+ const { normalized: normalizedOriginator } = this.prepareOriginator(originator)
1127
+ originator = normalizedOriginator
1088
1128
  // 1) adminOriginator can do anything
1089
1129
  if (this.isAdminOriginator(originator)) return true
1090
1130
 
@@ -1131,7 +1171,13 @@ export class WalletPermissionsManager implements WalletInterface {
1131
1171
  * and return a promise that resolves once permission is granted or rejects if denied.
1132
1172
  */
1133
1173
  private async requestPermissionFlow(r: PermissionRequest): Promise<boolean> {
1134
- const key = this.buildRequestKey(r)
1174
+ const normalizedOriginator = this.normalizeOriginator(r.originator) || r.originator
1175
+ const preparedRequest: PermissionRequest = {
1176
+ ...r,
1177
+ originator: normalizedOriginator,
1178
+ displayOriginator: r.displayOriginator ?? r.originator
1179
+ }
1180
+ const key = this.buildRequestKey(preparedRequest)
1135
1181
 
1136
1182
  // If there's already a queue for the same resource, we piggyback on it
1137
1183
  const existingQueue = this.activeRequests.get(key)
@@ -1145,33 +1191,33 @@ export class WalletPermissionsManager implements WalletInterface {
1145
1191
  // Return a promise that resolves or rejects once the user grants/denies
1146
1192
  return new Promise<boolean>(async (resolve, reject) => {
1147
1193
  this.activeRequests.set(key, {
1148
- request: r,
1194
+ request: preparedRequest,
1149
1195
  pending: [{ resolve, reject }]
1150
1196
  })
1151
1197
 
1152
1198
  // Fire the relevant onXXXRequested event (which one depends on r.type)
1153
- switch (r.type) {
1199
+ switch (preparedRequest.type) {
1154
1200
  case 'protocol':
1155
1201
  await this.callEvent('onProtocolPermissionRequested', {
1156
- ...r,
1202
+ ...preparedRequest,
1157
1203
  requestID: key
1158
1204
  })
1159
1205
  break
1160
1206
  case 'basket':
1161
1207
  await this.callEvent('onBasketAccessRequested', {
1162
- ...r,
1208
+ ...preparedRequest,
1163
1209
  requestID: key
1164
1210
  })
1165
1211
  break
1166
1212
  case 'certificate':
1167
1213
  await this.callEvent('onCertificateAccessRequested', {
1168
- ...r,
1214
+ ...preparedRequest,
1169
1215
  requestID: key
1170
1216
  })
1171
1217
  break
1172
1218
  case 'spending':
1173
1219
  await this.callEvent('onSpendingAuthorizationRequested', {
1174
- ...r,
1220
+ ...preparedRequest,
1175
1221
  requestID: key
1176
1222
  })
1177
1223
  break
@@ -1286,74 +1332,85 @@ export class WalletPermissionsManager implements WalletInterface {
1286
1332
  privileged: boolean,
1287
1333
  protocolID: WalletProtocol,
1288
1334
  counterparty: string,
1289
- includeExpired: boolean
1335
+ includeExpired: boolean,
1336
+ originatorLookupValues?: string[]
1290
1337
  ): Promise<PermissionToken | undefined> {
1291
1338
  const [secLevel, protoName] = protocolID
1292
- const tags = [
1293
- `originator ${originator}`,
1294
- `privileged ${!!privileged}`,
1295
- `protocolName ${protoName}`,
1296
- `protocolSecurityLevel ${secLevel}`
1297
- ]
1298
- if (secLevel === 2) {
1299
- tags.push(`counterparty ${counterparty}`)
1300
- }
1301
- const result = await this.underlying.listOutputs(
1302
- {
1303
- basket: BASKET_MAP.protocol,
1304
- tags,
1305
- tagQueryMode: 'all',
1306
- include: 'entire transactions'
1307
- },
1308
- this.adminOriginator
1309
- )
1339
+ const originsToTry = originatorLookupValues?.length ? originatorLookupValues : [originator]
1340
+
1341
+ for (const originTag of originsToTry) {
1342
+ const tags = [
1343
+ `originator ${originTag}`,
1344
+ `privileged ${!!privileged}`,
1345
+ `protocolName ${protoName}`,
1346
+ `protocolSecurityLevel ${secLevel}`
1347
+ ]
1348
+ if (secLevel === 2) {
1349
+ tags.push(`counterparty ${counterparty}`)
1350
+ }
1310
1351
 
1311
- for (const out of result.outputs) {
1312
- const [txid, outputIndexStr] = out.outpoint.split('.')
1313
- const tx = Transaction.fromBEEF(result.BEEF!, txid)
1314
- const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
1315
- if (!dec || !dec.fields || dec.fields.length < 6) continue
1316
- const domainRaw = dec.fields[0]
1317
- const expiryRaw = dec.fields[1]
1318
- const privRaw = dec.fields[2]
1319
- const secLevelRaw = dec.fields[3]
1320
- const protoNameRaw = dec.fields[4]
1321
- const counterpartyRaw = dec.fields[5]
1352
+ const result = await this.underlying.listOutputs(
1353
+ {
1354
+ basket: BASKET_MAP.protocol,
1355
+ tags,
1356
+ tagQueryMode: 'all',
1357
+ include: 'entire transactions'
1358
+ },
1359
+ this.adminOriginator
1360
+ )
1322
1361
 
1323
- const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
1324
- const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
1325
- const privDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
1326
- const secLevelDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(secLevelRaw)), 10) as
1327
- | 0
1328
- | 1
1329
- | 2
1330
- const protoNameDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(protoNameRaw))
1331
- const cptyDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(counterpartyRaw))
1332
-
1333
- if (
1334
- domainDecoded !== originator ||
1335
- privDecoded !== !!privileged ||
1336
- secLevelDecoded !== secLevel ||
1337
- protoNameDecoded !== protoName ||
1338
- (secLevelDecoded === 2 && cptyDecoded !== counterparty)
1339
- ) {
1340
- continue
1341
- }
1342
- if (!includeExpired && this.isTokenExpired(expiryDecoded)) {
1343
- continue
1344
- }
1345
- return {
1346
- tx: tx.toBEEF(),
1347
- txid: out.outpoint.split('.')[0],
1348
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1349
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1350
- satoshis: out.satoshis,
1351
- originator,
1352
- privileged,
1353
- protocol: protoName,
1354
- securityLevel: secLevel,
1355
- expiry: expiryDecoded,
1356
- counterparty: cptyDecoded
1362
+ for (const out of result.outputs) {
1363
+ const [txid, outputIndexStr] = out.outpoint.split('.')
1364
+ const tx = Transaction.fromBEEF(result.BEEF!, txid)
1365
+ const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
1366
+ if (!dec || !dec.fields || dec.fields.length < 6) continue
1367
+ const domainRaw = dec.fields[0]
1368
+ const expiryRaw = dec.fields[1]
1369
+ const privRaw = dec.fields[2]
1370
+ const secLevelRaw = dec.fields[3]
1371
+ const protoNameRaw = dec.fields[4]
1372
+ const counterpartyRaw = dec.fields[5]
1373
+
1374
+ const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
1375
+ const normalizedDomain = this.normalizeOriginator(domainDecoded)
1376
+ if (normalizedDomain !== originator) {
1377
+ continue
1378
+ }
1379
+
1380
+ const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
1381
+ const privDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
1382
+ const secLevelDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(secLevelRaw)), 10) as
1383
+ | 0
1384
+ | 1
1385
+ | 2
1386
+ const protoNameDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(protoNameRaw))
1387
+ const cptyDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(counterpartyRaw))
1388
+
1389
+ if (
1390
+ privDecoded !== !!privileged ||
1391
+ secLevelDecoded !== secLevel ||
1392
+ protoNameDecoded !== protoName ||
1393
+ (secLevelDecoded === 2 && cptyDecoded !== counterparty)
1394
+ ) {
1395
+ continue
1396
+ }
1397
+ if (!includeExpired && this.isTokenExpired(expiryDecoded)) {
1398
+ continue
1399
+ }
1400
+ return {
1401
+ tx: tx.toBEEF(),
1402
+ txid: out.outpoint.split('.')[0],
1403
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1404
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1405
+ satoshis: out.satoshis,
1406
+ originator,
1407
+ rawOriginator: domainDecoded,
1408
+ privileged,
1409
+ protocol: protoName,
1410
+ securityLevel: secLevel,
1411
+ expiry: expiryDecoded,
1412
+ counterparty: cptyDecoded
1413
+ }
1357
1414
  }
1358
1415
  }
1359
1416
  return undefined
@@ -1364,80 +1421,90 @@ export class WalletPermissionsManager implements WalletInterface {
1364
1421
  originator: string,
1365
1422
  privileged: boolean,
1366
1423
  protocolID: WalletProtocol,
1367
- counterparty: string
1424
+ counterparty: string,
1425
+ originatorLookupValues?: string[]
1368
1426
  ): Promise<PermissionToken[]> {
1369
1427
  const [secLevel, protoName] = protocolID
1370
- const tags = [
1371
- `originator ${originator}`,
1372
- `privileged ${!!privileged}`,
1373
- `protocolName ${protoName}`,
1374
- `protocolSecurityLevel ${secLevel}`
1375
- ]
1376
- if (secLevel === 2) {
1377
- tags.push(`counterparty ${counterparty}`)
1378
- }
1428
+ const originsToTry = originatorLookupValues?.length ? originatorLookupValues : [originator]
1429
+ const matches: PermissionToken[] = []
1430
+ const seen = new Set<string>()
1431
+
1432
+ for (const originTag of originsToTry) {
1433
+ const tags = [
1434
+ `originator ${originTag}`,
1435
+ `privileged ${!!privileged}`,
1436
+ `protocolName ${protoName}`,
1437
+ `protocolSecurityLevel ${secLevel}`
1438
+ ]
1439
+ if (secLevel === 2) {
1440
+ tags.push(`counterparty ${counterparty}`)
1441
+ }
1379
1442
 
1380
- const result = await this.underlying.listOutputs(
1381
- {
1382
- basket: BASKET_MAP.protocol,
1383
- tags,
1384
- tagQueryMode: 'all',
1385
- include: 'entire transactions'
1386
- },
1387
- this.adminOriginator
1388
- )
1443
+ const result = await this.underlying.listOutputs(
1444
+ {
1445
+ basket: BASKET_MAP.protocol,
1446
+ tags,
1447
+ tagQueryMode: 'all',
1448
+ include: 'entire transactions'
1449
+ },
1450
+ this.adminOriginator
1451
+ )
1389
1452
 
1390
- const matches: PermissionToken[] = []
1453
+ for (const out of result.outputs) {
1454
+ if (seen.has(out.outpoint)) continue
1455
+ const [txid, outputIndexStr] = out.outpoint.split('.')
1456
+ const tx = Transaction.fromBEEF(result.BEEF!, txid)
1457
+ const vout = Number(outputIndexStr)
1458
+ const dec = PushDrop.decode(tx.outputs[vout].lockingScript)
1459
+ if (!dec || !dec.fields || dec.fields.length < 6) continue
1460
+
1461
+ const domainRaw = dec.fields[0]
1462
+ const expiryRaw = dec.fields[1]
1463
+ const privRaw = dec.fields[2]
1464
+ const secLevelRaw = dec.fields[3]
1465
+ const protoNameRaw = dec.fields[4]
1466
+ const counterpartyRaw = dec.fields[5]
1467
+
1468
+ const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
1469
+ const normalizedDomain = this.normalizeOriginator(domainDecoded)
1470
+ if (normalizedDomain !== originator) {
1471
+ continue
1472
+ }
1391
1473
 
1392
- for (const out of result.outputs) {
1393
- const [txid, outputIndexStr] = out.outpoint.split('.')
1394
- const tx = Transaction.fromBEEF(result.BEEF!, txid)
1395
- const vout = Number(outputIndexStr)
1396
- const dec = PushDrop.decode(tx.outputs[vout].lockingScript)
1397
- if (!dec || !dec.fields || dec.fields.length < 6) continue
1398
-
1399
- const domainRaw = dec.fields[0]
1400
- const expiryRaw = dec.fields[1]
1401
- const privRaw = dec.fields[2]
1402
- const secLevelRaw = dec.fields[3]
1403
- const protoNameRaw = dec.fields[4]
1404
- const counterpartyRaw = dec.fields[5]
1405
-
1406
- // Decrypt all fields
1407
- const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
1408
- const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
1409
- const privDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
1410
- const secLevelDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(secLevelRaw)), 10) as
1411
- | 0
1412
- | 1
1413
- | 2
1414
- const protoNameDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(protoNameRaw))
1415
- const cptyDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(counterpartyRaw))
1416
-
1417
- // Strict attribute match; NO expiry filtering
1418
- if (
1419
- domainDecoded !== originator ||
1420
- privDecoded !== !!privileged ||
1421
- secLevelDecoded !== secLevel ||
1422
- protoNameDecoded !== protoName ||
1423
- (secLevelDecoded === 2 && cptyDecoded !== counterparty)
1424
- ) {
1425
- continue
1426
- }
1474
+ const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
1475
+ const privDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
1476
+ const secLevelDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(secLevelRaw)), 10) as
1477
+ | 0
1478
+ | 1
1479
+ | 2
1480
+ const protoNameDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(protoNameRaw))
1481
+ const cptyDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(counterpartyRaw))
1482
+
1483
+ if (
1484
+ privDecoded !== !!privileged ||
1485
+ secLevelDecoded !== secLevel ||
1486
+ protoNameDecoded !== protoName ||
1487
+ (secLevelDecoded === 2 && cptyDecoded !== counterparty)
1488
+ ) {
1489
+ continue
1490
+ }
1427
1491
 
1428
- matches.push({
1429
- tx: tx.toBEEF(),
1430
- txid,
1431
- outputIndex: vout,
1432
- outputScript: tx.outputs[vout].lockingScript.toHex(),
1433
- satoshis: out.satoshis,
1434
- originator,
1435
- privileged,
1436
- protocol: protoName,
1437
- securityLevel: secLevel,
1438
- expiry: expiryDecoded,
1439
- counterparty: cptyDecoded
1440
- })
1492
+ seen.add(out.outpoint)
1493
+ matches.push({
1494
+ tx: tx.toBEEF(),
1495
+ txid,
1496
+ outputIndex: vout,
1497
+ outputScript: tx.outputs[vout].lockingScript.toHex(),
1498
+ satoshis: out.satoshis,
1499
+ originator,
1500
+ rawOriginator: domainDecoded,
1501
+ privileged,
1502
+ protocol: protoName,
1503
+ securityLevel: secLevel,
1504
+ expiry: expiryDecoded,
1505
+ counterparty: cptyDecoded
1506
+ })
1507
+ }
1441
1508
  }
1442
1509
 
1443
1510
  return matches
@@ -1446,42 +1513,53 @@ export class WalletPermissionsManager implements WalletInterface {
1446
1513
  private async findBasketToken(
1447
1514
  originator: string,
1448
1515
  basket: string,
1449
- includeExpired: boolean
1516
+ includeExpired: boolean,
1517
+ originatorLookupValues?: string[]
1450
1518
  ): Promise<PermissionToken | undefined> {
1451
- const result = await this.underlying.listOutputs(
1452
- {
1453
- basket: BASKET_MAP.basket,
1454
- tags: [`originator ${originator}`, `basket ${basket}`],
1455
- tagQueryMode: 'all',
1456
- include: 'entire transactions'
1457
- },
1458
- this.adminOriginator
1459
- )
1519
+ const originsToTry = originatorLookupValues?.length ? originatorLookupValues : [originator]
1460
1520
 
1461
- for (const out of result.outputs) {
1462
- const [txid, outputIndexStr] = out.outpoint.split('.')
1463
- const tx = Transaction.fromBEEF(result.BEEF!, txid)
1464
- const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
1465
- if (!dec?.fields || dec.fields.length < 3) continue
1466
- const domainRaw = dec.fields[0]
1467
- const expiryRaw = dec.fields[1]
1468
- const basketRaw = dec.fields[2]
1521
+ for (const originTag of originsToTry) {
1522
+ const result = await this.underlying.listOutputs(
1523
+ {
1524
+ basket: BASKET_MAP.basket,
1525
+ tags: [`originator ${originTag}`, `basket ${basket}`],
1526
+ tagQueryMode: 'all',
1527
+ include: 'entire transactions'
1528
+ },
1529
+ this.adminOriginator
1530
+ )
1469
1531
 
1470
- const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
1471
- const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
1472
- const basketDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(basketRaw))
1473
- if (domainDecoded !== originator || basketDecoded !== basket) continue
1474
- if (!includeExpired && this.isTokenExpired(expiryDecoded)) continue
1532
+ for (const out of result.outputs) {
1533
+ const [txid, outputIndexStr] = out.outpoint.split('.')
1534
+ const tx = Transaction.fromBEEF(result.BEEF!, txid)
1535
+ const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
1536
+ if (!dec?.fields || dec.fields.length < 3) continue
1537
+ const domainRaw = dec.fields[0]
1538
+ const expiryRaw = dec.fields[1]
1539
+ const basketRaw = dec.fields[2]
1540
+
1541
+ const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
1542
+ const normalizedDomain = this.normalizeOriginator(domainDecoded)
1543
+ if (normalizedDomain !== originator) {
1544
+ continue
1545
+ }
1475
1546
 
1476
- return {
1477
- tx: tx.toBEEF(),
1478
- txid: out.outpoint.split('.')[0],
1479
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1480
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1481
- satoshis: out.satoshis,
1482
- originator,
1483
- basketName: basketDecoded,
1484
- expiry: expiryDecoded
1547
+ const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
1548
+ const basketDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(basketRaw))
1549
+ if (basketDecoded !== basket) continue
1550
+ if (!includeExpired && this.isTokenExpired(expiryDecoded)) continue
1551
+
1552
+ return {
1553
+ tx: tx.toBEEF(),
1554
+ txid: out.outpoint.split('.')[0],
1555
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1556
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1557
+ satoshis: out.satoshis,
1558
+ originator,
1559
+ rawOriginator: domainDecoded,
1560
+ basketName: basketDecoded,
1561
+ expiry: expiryDecoded
1562
+ }
1485
1563
  }
1486
1564
  }
1487
1565
  return undefined
@@ -1494,101 +1572,116 @@ export class WalletPermissionsManager implements WalletInterface {
1494
1572
  verifier: string,
1495
1573
  certType: string,
1496
1574
  fields: string[],
1497
- includeExpired: boolean
1575
+ includeExpired: boolean,
1576
+ originatorLookupValues?: string[]
1498
1577
  ): Promise<PermissionToken | undefined> {
1499
- const result = await this.underlying.listOutputs(
1500
- {
1501
- basket: BASKET_MAP.certificate,
1502
- tags: [`originator ${originator}`, `privileged ${!!privileged}`, `type ${certType}`, `verifier ${verifier}`],
1503
- tagQueryMode: 'all',
1504
- include: 'entire transactions'
1505
- },
1506
- this.adminOriginator
1507
- )
1578
+ const originsToTry = originatorLookupValues?.length ? originatorLookupValues : [originator]
1508
1579
 
1509
- for (const out of result.outputs) {
1510
- const [txid, outputIndexStr] = out.outpoint.split('.')
1511
- const tx = Transaction.fromBEEF(result.BEEF!, txid)
1512
- const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
1513
- if (!dec?.fields || dec.fields.length < 6) continue
1514
- const [domainRaw, expiryRaw, privRaw, typeRaw, fieldsRaw, verifierRaw] = dec.fields
1580
+ for (const originTag of originsToTry) {
1581
+ const result = await this.underlying.listOutputs(
1582
+ {
1583
+ basket: BASKET_MAP.certificate,
1584
+ tags: [`originator ${originTag}`, `privileged ${!!privileged}`, `type ${certType}`, `verifier ${verifier}`],
1585
+ tagQueryMode: 'all',
1586
+ include: 'entire transactions'
1587
+ },
1588
+ this.adminOriginator
1589
+ )
1515
1590
 
1516
- const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
1517
- const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
1518
- const privDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
1519
- const typeDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(typeRaw))
1520
- const verifierDec = Utils.toUTF8(await this.decryptPermissionTokenField(verifierRaw))
1521
-
1522
- const fieldsJson = await this.decryptPermissionTokenField(fieldsRaw)
1523
- const allFields = JSON.parse(Utils.toUTF8(fieldsJson)) as string[]
1524
-
1525
- if (
1526
- domainDecoded !== originator ||
1527
- privDecoded !== !!privileged ||
1528
- typeDecoded !== certType ||
1529
- verifierDec !== verifier
1530
- ) {
1531
- continue
1532
- }
1533
- // Check if 'fields' is a subset of 'allFields'
1534
- const setAll = new Set(allFields)
1535
- if (fields.some(f => !setAll.has(f))) {
1536
- continue
1537
- }
1538
- if (!includeExpired && this.isTokenExpired(expiryDecoded)) {
1539
- continue
1540
- }
1541
- return {
1542
- tx: tx.toBEEF(),
1543
- txid: out.outpoint.split('.')[0],
1544
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1545
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1546
- satoshis: out.satoshis,
1547
- originator,
1548
- privileged,
1549
- verifier: verifierDec,
1550
- certType: typeDecoded,
1551
- certFields: allFields,
1552
- expiry: expiryDecoded
1591
+ for (const out of result.outputs) {
1592
+ const [txid, outputIndexStr] = out.outpoint.split('.')
1593
+ const tx = Transaction.fromBEEF(result.BEEF!, txid)
1594
+ const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
1595
+ if (!dec?.fields || dec.fields.length < 6) continue
1596
+ const [domainRaw, expiryRaw, privRaw, typeRaw, fieldsRaw, verifierRaw] = dec.fields
1597
+
1598
+ const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
1599
+ const normalizedDomain = this.normalizeOriginator(domainDecoded)
1600
+ if (normalizedDomain !== originator) {
1601
+ continue
1602
+ }
1603
+
1604
+ const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
1605
+ const privDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
1606
+ const typeDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(typeRaw))
1607
+ const verifierDec = Utils.toUTF8(await this.decryptPermissionTokenField(verifierRaw))
1608
+
1609
+ const fieldsJson = await this.decryptPermissionTokenField(fieldsRaw)
1610
+ const allFields = JSON.parse(Utils.toUTF8(fieldsJson)) as string[]
1611
+
1612
+ if (privDecoded !== !!privileged || typeDecoded !== certType || verifierDec !== verifier) {
1613
+ continue
1614
+ }
1615
+ // Check if 'fields' is a subset of 'allFields'
1616
+ const setAll = new Set(allFields)
1617
+ if (fields.some(f => !setAll.has(f))) {
1618
+ continue
1619
+ }
1620
+ if (!includeExpired && this.isTokenExpired(expiryDecoded)) {
1621
+ continue
1622
+ }
1623
+ return {
1624
+ tx: tx.toBEEF(),
1625
+ txid: out.outpoint.split('.')[0],
1626
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1627
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1628
+ satoshis: out.satoshis,
1629
+ originator,
1630
+ rawOriginator: domainDecoded,
1631
+ privileged,
1632
+ verifier: verifierDec,
1633
+ certType: typeDecoded,
1634
+ certFields: allFields,
1635
+ expiry: expiryDecoded
1636
+ }
1553
1637
  }
1554
1638
  }
1555
1639
  return undefined
1556
1640
  }
1557
1641
 
1558
1642
  /** Looks for a DSAP token matching origin, returning the first one found. */
1559
- private async findSpendingToken(originator: string): Promise<PermissionToken | undefined> {
1560
- const result = await this.underlying.listOutputs(
1561
- {
1562
- basket: BASKET_MAP.spending,
1563
- tags: [`originator ${originator}`],
1564
- tagQueryMode: 'all',
1565
- include: 'entire transactions'
1566
- },
1567
- this.adminOriginator
1568
- )
1569
-
1570
- for (const out of result.outputs) {
1571
- const [txid, outputIndexStr] = out.outpoint.split('.')
1572
- const tx = Transaction.fromBEEF(result.BEEF!, txid)
1573
- const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
1574
- if (!dec?.fields || dec.fields.length < 2) continue
1575
- const domainRaw = dec.fields[0]
1576
- const amtRaw = dec.fields[1]
1643
+ private async findSpendingToken(
1644
+ originator: string,
1645
+ originatorLookupValues?: string[]
1646
+ ): Promise<PermissionToken | undefined> {
1647
+ const originsToTry = originatorLookupValues?.length ? originatorLookupValues : [originator]
1577
1648
 
1578
- const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
1579
- if (domainDecoded !== originator) continue
1580
- const amtDecodedStr = Utils.toUTF8(await this.decryptPermissionTokenField(amtRaw))
1581
- const authorizedAmount = parseInt(amtDecodedStr, 10)
1649
+ for (const originTag of originsToTry) {
1650
+ const result = await this.underlying.listOutputs(
1651
+ {
1652
+ basket: BASKET_MAP.spending,
1653
+ tags: [`originator ${originTag}`],
1654
+ tagQueryMode: 'all',
1655
+ include: 'entire transactions'
1656
+ },
1657
+ this.adminOriginator
1658
+ )
1582
1659
 
1583
- return {
1584
- tx: tx.toBEEF(),
1585
- txid: out.outpoint.split('.')[0],
1586
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1587
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1588
- satoshis: out.satoshis,
1589
- originator,
1590
- authorizedAmount,
1591
- expiry: 0 // Not time-limited, monthly authorization
1660
+ for (const out of result.outputs) {
1661
+ const [txid, outputIndexStr] = out.outpoint.split('.')
1662
+ const tx = Transaction.fromBEEF(result.BEEF!, txid)
1663
+ const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
1664
+ if (!dec?.fields || dec.fields.length < 2) continue
1665
+ const domainRaw = dec.fields[0]
1666
+ const amtRaw = dec.fields[1]
1667
+
1668
+ const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
1669
+ const normalizedDomain = this.normalizeOriginator(domainDecoded)
1670
+ if (normalizedDomain !== originator) continue
1671
+ const amtDecodedStr = Utils.toUTF8(await this.decryptPermissionTokenField(amtRaw))
1672
+ const authorizedAmount = parseInt(amtDecodedStr, 10)
1673
+
1674
+ return {
1675
+ tx: tx.toBEEF(),
1676
+ txid: out.outpoint.split('.')[0],
1677
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1678
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1679
+ satoshis: out.satoshis,
1680
+ originator,
1681
+ rawOriginator: domainDecoded,
1682
+ authorizedAmount,
1683
+ expiry: 0 // Not time-limited, monthly authorization
1684
+ }
1592
1685
  }
1593
1686
  }
1594
1687
  return undefined
@@ -1610,14 +1703,21 @@ export class WalletPermissionsManager implements WalletInterface {
1610
1703
  * Returns spending for an originator in the current calendar month.
1611
1704
  */
1612
1705
  public async querySpentSince(token: PermissionToken): Promise<number> {
1613
- const { actions } = await this.underlying.listActions(
1614
- {
1615
- labels: [`admin originator ${token.originator}`, `admin month ${this.getCurrentMonthYearUTC()}`],
1616
- labelQueryMode: 'all'
1617
- },
1618
- this.adminOriginator
1619
- )
1620
- return actions.reduce((a, e) => a + e.satoshis, 0)
1706
+ const labelOrigins = this.buildOriginatorLookupValues(token.rawOriginator, token.originator)
1707
+ let total = 0
1708
+
1709
+ for (const labelOrigin of labelOrigins) {
1710
+ const { actions } = await this.underlying.listActions(
1711
+ {
1712
+ labels: [`admin originator ${labelOrigin}`, `admin month ${this.getCurrentMonthYearUTC()}`],
1713
+ labelQueryMode: 'all'
1714
+ },
1715
+ this.adminOriginator
1716
+ )
1717
+ total += actions.reduce((a, e) => a + e.satoshis, 0)
1718
+ }
1719
+
1720
+ return total
1621
1721
  }
1622
1722
 
1623
1723
  /* ---------------------------------------------------------------------
@@ -1634,6 +1734,8 @@ export class WalletPermissionsManager implements WalletInterface {
1634
1734
  * @param amount For DSAP, the authorized spending limit
1635
1735
  */
1636
1736
  private async createPermissionOnChain(r: PermissionRequest, expiry: number, amount?: number): Promise<void> {
1737
+ const normalizedOriginator = this.normalizeOriginator(r.originator) || r.originator
1738
+ r.originator = normalizedOriginator
1637
1739
  const basketName = BASKET_MAP[r.type]
1638
1740
  if (!basketName) return
1639
1741
 
@@ -1687,11 +1789,17 @@ export class WalletPermissionsManager implements WalletInterface {
1687
1789
  ): Promise<string> {
1688
1790
  if (!oldTokens?.length) throw new Error('No permission tokens to coalesce')
1689
1791
  if (oldTokens.length < 2) throw new Error('Need at least 2 tokens to coalesce')
1690
-
1691
1792
  // 1) Create a signable action with N inputs and a single renewed output
1793
+ // Merge all input token BEEFs into a single BEEF structure
1794
+ const inputBeef = new Beef()
1795
+ for (const token of oldTokens) {
1796
+ inputBeef.mergeBeef(Beef.fromBinary(token.tx))
1797
+ }
1798
+
1692
1799
  const { signableTransaction } = await this.createAction(
1693
1800
  {
1694
1801
  description: opts?.description ?? `Coalesce ${oldTokens.length} permission tokens`,
1802
+ inputBEEF: inputBeef.toBinary(),
1695
1803
  inputs: oldTokens.map((t, i) => ({
1696
1804
  outpoint: `${t.txid}.${t.outputIndex}`,
1697
1805
  unlockingScriptLength: 74,
@@ -1719,14 +1827,23 @@ export class WalletPermissionsManager implements WalletInterface {
1719
1827
  throw new Error('Failed to create signable transaction')
1720
1828
  }
1721
1829
 
1722
- // 2) Sign each input
1830
+ // 2) Sign each input - each token needs its own unlocker with the correct locking script
1723
1831
  const partialTx = Transaction.fromAtomicBEEF(signableTransaction.tx)
1724
1832
  const pushdrop = new PushDrop(this.underlying)
1725
- const unlocker = pushdrop.unlock(WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL, '1', 'self')
1726
1833
 
1727
1834
  const spends: Record<number, { unlockingScript: string }> = {}
1728
1835
  for (let i = 0; i < oldTokens.length; i++) {
1729
- // The signable transaction already contains the necessary prevout context
1836
+ const token = oldTokens[i]
1837
+ // Each token requires its own unlocker with the specific locking script
1838
+ const unlocker = pushdrop.unlock(
1839
+ WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
1840
+ '1',
1841
+ 'self',
1842
+ 'all',
1843
+ false,
1844
+ 1,
1845
+ LockingScript.fromHex(token.outputScript)
1846
+ )
1730
1847
  const unlockingScript = await unlocker.sign(partialTx, i)
1731
1848
  spends[i] = { unlockingScript: unlockingScript.toHex() }
1732
1849
  }
@@ -1736,7 +1853,6 @@ export class WalletPermissionsManager implements WalletInterface {
1736
1853
  reference: signableTransaction.reference,
1737
1854
  spends
1738
1855
  })
1739
-
1740
1856
  if (!txid) throw new Error('Failed to finalize coalescing transaction')
1741
1857
  return txid
1742
1858
  }
@@ -1755,6 +1871,7 @@ export class WalletPermissionsManager implements WalletInterface {
1755
1871
  newExpiry: number,
1756
1872
  newAmount?: number
1757
1873
  ): Promise<void> {
1874
+ r.originator = this.normalizeOriginator(r.originator) || r.originator
1758
1875
  // 1) build new fields
1759
1876
  const newFields = await this.buildPushdropFields(r, newExpiry, newAmount)
1760
1877
 
@@ -1773,17 +1890,17 @@ export class WalletPermissionsManager implements WalletInterface {
1773
1890
  oldToken.originator,
1774
1891
  oldToken.privileged!,
1775
1892
  [oldToken.securityLevel!, oldToken.protocol!],
1776
- oldToken.counterparty!
1893
+ oldToken.counterparty!,
1894
+ this.buildOriginatorLookupValues(oldToken.rawOriginator, oldToken.originator)
1777
1895
  )
1778
1896
 
1779
1897
  // If so, coalesce them into a single token first, to avoid bloat
1780
1898
  if (oldTokens.length > 1) {
1781
- const txid = await this.coalescePermissionTokens(oldTokens, newScript, {
1899
+ await this.coalescePermissionTokens(oldTokens, newScript, {
1782
1900
  tags,
1783
1901
  basket: BASKET_MAP[r.type],
1784
1902
  description: `Coalesce ${r.type} permission tokens`
1785
1903
  })
1786
- console.log('Coalesced permission tokens:', txid)
1787
1904
  } else {
1788
1905
  // Otherwise, just proceed with the single-token renewal
1789
1906
  // 3) For BRC-100, we do a "createAction" with a partial input referencing oldToken
@@ -1894,7 +2011,9 @@ export class WalletPermissionsManager implements WalletInterface {
1894
2011
  tags.push(`privileged ${!!r.privileged}`)
1895
2012
  tags.push(`protocolName ${r.protocolID![1]}`)
1896
2013
  tags.push(`protocolSecurityLevel ${r.protocolID![0]}`)
1897
- tags.push(`counterparty ${r.counterparty}`)
2014
+ if (r.protocolID![0] === 2) {
2015
+ tags.push(`counterparty ${r.counterparty}`)
2016
+ }
1898
2017
  break
1899
2018
  }
1900
2019
  case 'basket': {
@@ -1942,66 +2061,81 @@ export class WalletPermissionsManager implements WalletInterface {
1942
2061
  counterparty?: string
1943
2062
  } = {}): Promise<PermissionToken[]> {
1944
2063
  const basketName = BASKET_MAP.protocol
1945
- const tags: string[] = []
1946
-
1947
- if (originator) {
1948
- tags.push(`originator ${originator}`)
1949
- }
2064
+ const baseTags: string[] = []
1950
2065
 
1951
2066
  if (privileged !== undefined) {
1952
- tags.push(`privileged ${!!privileged}`)
2067
+ baseTags.push(`privileged ${!!privileged}`)
1953
2068
  }
1954
2069
 
1955
2070
  if (protocolName) {
1956
- tags.push(`protocolName ${protocolName}`)
2071
+ baseTags.push(`protocolName ${protocolName}`)
1957
2072
  }
1958
2073
 
1959
2074
  if (protocolSecurityLevel !== undefined) {
1960
- tags.push(`protocolSecurityLevel ${protocolSecurityLevel}`)
2075
+ baseTags.push(`protocolSecurityLevel ${protocolSecurityLevel}`)
1961
2076
  }
1962
2077
 
1963
2078
  if (counterparty) {
1964
- tags.push(`counterparty ${counterparty}`)
2079
+ baseTags.push(`counterparty ${counterparty}`)
1965
2080
  }
1966
- const result = await this.underlying.listOutputs(
1967
- {
1968
- basket: basketName,
1969
- tags,
1970
- tagQueryMode: 'all',
1971
- include: 'entire transactions',
1972
- limit: 100
1973
- },
1974
- this.adminOriginator
1975
- )
1976
2081
 
2082
+ const originFilter = originator ? this.prepareOriginator(originator) : undefined
2083
+ const originVariants = originFilter ? originFilter.lookupValues : [undefined]
2084
+ const seen = new Set<string>()
1977
2085
  const tokens: PermissionToken[] = []
1978
- for (const out of result.outputs) {
1979
- const [txid, outputIndexStr] = out.outpoint.split('.')
1980
- const tx = Transaction.fromBEEF(result.BEEF!, txid)
1981
- const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
1982
- if (!dec?.fields || dec.fields.length < 6) continue
1983
- const [domainRaw, expiryRaw, privRaw, secRaw, protoRaw, cptyRaw] = dec.fields
1984
2086
 
1985
- const domainDec = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
1986
- const expiryDec = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
1987
- const privDec = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
1988
- const secDec = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(secRaw)), 10) as 0 | 1 | 2
1989
- const protoDec = Utils.toUTF8(await this.decryptPermissionTokenField(protoRaw))
1990
- const cptyDec = Utils.toUTF8(await this.decryptPermissionTokenField(cptyRaw))
2087
+ for (const originTag of originVariants) {
2088
+ const tags = [...baseTags]
2089
+ if (originTag) {
2090
+ tags.push(`originator ${originTag}`)
2091
+ }
2092
+ const result = await this.underlying.listOutputs(
2093
+ {
2094
+ basket: basketName,
2095
+ tags,
2096
+ tagQueryMode: 'all',
2097
+ include: 'entire transactions',
2098
+ limit: 100
2099
+ },
2100
+ this.adminOriginator
2101
+ )
1991
2102
 
1992
- tokens.push({
1993
- tx: tx.toBEEF(),
1994
- txid: out.outpoint.split('.')[0],
1995
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1996
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1997
- satoshis: out.satoshis,
1998
- originator: domainDec,
1999
- expiry: expiryDec,
2000
- privileged: privDec,
2001
- securityLevel: secDec,
2002
- protocol: protoDec,
2003
- counterparty: cptyDec
2004
- })
2103
+ for (const out of result.outputs) {
2104
+ if (seen.has(out.outpoint)) continue
2105
+ const [txid, outputIndexStr] = out.outpoint.split('.')
2106
+ const tx = Transaction.fromBEEF(result.BEEF!, txid)
2107
+ const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
2108
+ if (!dec?.fields || dec.fields.length < 6) continue
2109
+ const [domainRaw, expiryRaw, privRaw, secRaw, protoRaw, cptyRaw] = dec.fields
2110
+
2111
+ const domainDec = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
2112
+ const normalizedDomain = this.normalizeOriginator(domainDec)
2113
+ if (originFilter && normalizedDomain !== originFilter.normalized) {
2114
+ continue
2115
+ }
2116
+
2117
+ const expiryDec = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
2118
+ const privDec = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
2119
+ const secDec = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(secRaw)), 10) as 0 | 1 | 2
2120
+ const protoDec = Utils.toUTF8(await this.decryptPermissionTokenField(protoRaw))
2121
+ const cptyDec = Utils.toUTF8(await this.decryptPermissionTokenField(cptyRaw))
2122
+
2123
+ seen.add(out.outpoint)
2124
+ tokens.push({
2125
+ tx: tx.toBEEF(),
2126
+ txid: out.outpoint.split('.')[0],
2127
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
2128
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
2129
+ satoshis: out.satoshis,
2130
+ originator: normalizedDomain,
2131
+ rawOriginator: domainDec,
2132
+ expiry: expiryDec,
2133
+ privileged: privDec,
2134
+ securityLevel: secDec,
2135
+ protocol: protoDec,
2136
+ counterparty: cptyDec
2137
+ })
2138
+ }
2005
2139
  }
2006
2140
  return tokens
2007
2141
  }
@@ -2037,46 +2171,60 @@ export class WalletPermissionsManager implements WalletInterface {
2037
2171
  */
2038
2172
  public async listBasketAccess(params: { originator?: string; basket?: string } = {}): Promise<PermissionToken[]> {
2039
2173
  const basketName = BASKET_MAP.basket
2040
- const tags: string[] = []
2041
-
2042
- if (params.originator) {
2043
- tags.push(`originator ${params.originator}`)
2044
- }
2174
+ const baseTags: string[] = []
2045
2175
 
2046
2176
  if (params.basket) {
2047
- tags.push(`basket ${params.basket}`)
2177
+ baseTags.push(`basket ${params.basket}`)
2048
2178
  }
2049
- const result = await this.underlying.listOutputs(
2050
- {
2051
- basket: basketName,
2052
- tags,
2053
- tagQueryMode: 'all',
2054
- include: 'entire transactions',
2055
- limit: 10000
2056
- },
2057
- this.adminOriginator
2058
- )
2059
2179
 
2180
+ const originFilter = params.originator ? this.prepareOriginator(params.originator) : undefined
2181
+ const originVariants = originFilter ? originFilter.lookupValues : [undefined]
2182
+ const seen = new Set<string>()
2060
2183
  const tokens: PermissionToken[] = []
2061
- for (const out of result.outputs) {
2062
- const [txid, outputIndexStr] = out.outpoint.split('.')
2063
- const tx = Transaction.fromBEEF(result.BEEF!, txid)
2064
- const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
2065
- if (!dec?.fields || dec.fields.length < 3) continue
2066
- const [domainRaw, expiryRaw, basketRaw] = dec.fields
2067
- const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
2068
- const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
2069
- const basketDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(basketRaw))
2070
- tokens.push({
2071
- tx: tx.toBEEF(),
2072
- txid: out.outpoint.split('.')[0],
2073
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
2074
- satoshis: out.satoshis,
2075
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
2076
- originator: domainDecoded,
2077
- basketName: basketDecoded,
2078
- expiry: expiryDecoded
2079
- })
2184
+
2185
+ for (const originTag of originVariants) {
2186
+ const tags = [...baseTags]
2187
+ if (originTag) {
2188
+ tags.push(`originator ${originTag}`)
2189
+ }
2190
+ const result = await this.underlying.listOutputs(
2191
+ {
2192
+ basket: basketName,
2193
+ tags,
2194
+ tagQueryMode: 'all',
2195
+ include: 'entire transactions',
2196
+ limit: 10000
2197
+ },
2198
+ this.adminOriginator
2199
+ )
2200
+
2201
+ for (const out of result.outputs) {
2202
+ if (seen.has(out.outpoint)) continue
2203
+ const [txid, outputIndexStr] = out.outpoint.split('.')
2204
+ const tx = Transaction.fromBEEF(result.BEEF!, txid)
2205
+ const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
2206
+ if (!dec?.fields || dec.fields.length < 3) continue
2207
+ const [domainRaw, expiryRaw, basketRaw] = dec.fields
2208
+ const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
2209
+ const normalizedDomain = this.normalizeOriginator(domainDecoded)
2210
+ if (originFilter && normalizedDomain !== originFilter.normalized) {
2211
+ continue
2212
+ }
2213
+ const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
2214
+ const basketDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(basketRaw))
2215
+ seen.add(out.outpoint)
2216
+ tokens.push({
2217
+ tx: tx.toBEEF(),
2218
+ txid: out.outpoint.split('.')[0],
2219
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
2220
+ satoshis: out.satoshis,
2221
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
2222
+ originator: normalizedDomain,
2223
+ rawOriginator: domainDecoded,
2224
+ basketName: basketDecoded,
2225
+ expiry: expiryDecoded
2226
+ })
2227
+ }
2080
2228
  }
2081
2229
  return tokens
2082
2230
  }
@@ -2176,61 +2324,75 @@ export class WalletPermissionsManager implements WalletInterface {
2176
2324
  } = {}
2177
2325
  ): Promise<PermissionToken[]> {
2178
2326
  const basketName = BASKET_MAP.certificate
2179
- const tags: string[] = []
2180
-
2181
- if (params.originator) {
2182
- tags.push(`originator ${params.originator}`)
2183
- }
2327
+ const baseTags: string[] = []
2184
2328
 
2185
2329
  if (params.privileged !== undefined) {
2186
- tags.push(`privileged ${!!params.privileged}`)
2330
+ baseTags.push(`privileged ${!!params.privileged}`)
2187
2331
  }
2188
2332
 
2189
2333
  if (params.certType) {
2190
- tags.push(`type ${params.certType}`)
2334
+ baseTags.push(`type ${params.certType}`)
2191
2335
  }
2192
2336
 
2193
2337
  if (params.verifier) {
2194
- tags.push(`verifier ${params.verifier}`)
2338
+ baseTags.push(`verifier ${params.verifier}`)
2195
2339
  }
2196
- const result = await this.underlying.listOutputs(
2197
- {
2198
- basket: basketName,
2199
- tags,
2200
- tagQueryMode: 'all',
2201
- include: 'entire transactions',
2202
- limit: 10000
2203
- },
2204
- this.adminOriginator
2205
- )
2206
2340
 
2341
+ const originFilter = params.originator ? this.prepareOriginator(params.originator) : undefined
2342
+ const originVariants = originFilter ? originFilter.lookupValues : [undefined]
2343
+ const seen = new Set<string>()
2207
2344
  const tokens: PermissionToken[] = []
2208
- for (const out of result.outputs) {
2209
- const [txid, outputIndexStr] = out.outpoint.split('.')
2210
- const tx = Transaction.fromBEEF(result.BEEF!, txid)
2211
- const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
2212
- if (!dec?.fields || dec.fields.length < 6) continue
2213
- const [domainRaw, expiryRaw, privRaw, typeRaw, fieldsRaw, verifierRaw] = dec.fields
2214
- const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
2215
- const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
2216
- const privDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
2217
- const typeDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(typeRaw))
2218
- const verifierDec = Utils.toUTF8(await this.decryptPermissionTokenField(verifierRaw))
2219
- const fieldsJson = await this.decryptPermissionTokenField(fieldsRaw)
2220
- const allFields = JSON.parse(Utils.toUTF8(fieldsJson)) as string[]
2221
- tokens.push({
2222
- tx: tx.toBEEF(),
2223
- txid: out.outpoint.split('.')[0],
2224
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
2225
- satoshis: out.satoshis,
2226
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
2227
- originator: domainDecoded,
2228
- privileged: privDecoded,
2229
- certType: typeDecoded,
2230
- certFields: allFields,
2231
- verifier: verifierDec,
2232
- expiry: expiryDecoded
2233
- })
2345
+
2346
+ for (const originTag of originVariants) {
2347
+ const tags = [...baseTags]
2348
+ if (originTag) {
2349
+ tags.push(`originator ${originTag}`)
2350
+ }
2351
+ const result = await this.underlying.listOutputs(
2352
+ {
2353
+ basket: basketName,
2354
+ tags,
2355
+ tagQueryMode: 'all',
2356
+ include: 'entire transactions',
2357
+ limit: 10000
2358
+ },
2359
+ this.adminOriginator
2360
+ )
2361
+
2362
+ for (const out of result.outputs) {
2363
+ if (seen.has(out.outpoint)) continue
2364
+ const [txid, outputIndexStr] = out.outpoint.split('.')
2365
+ const tx = Transaction.fromBEEF(result.BEEF!, txid)
2366
+ const dec = PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript)
2367
+ if (!dec?.fields || dec.fields.length < 6) continue
2368
+ const [domainRaw, expiryRaw, privRaw, typeRaw, fieldsRaw, verifierRaw] = dec.fields
2369
+ const domainDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw))
2370
+ const normalizedDomain = this.normalizeOriginator(domainDecoded)
2371
+ if (originFilter && normalizedDomain !== originFilter.normalized) {
2372
+ continue
2373
+ }
2374
+ const expiryDecoded = parseInt(Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10)
2375
+ const privDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'
2376
+ const typeDecoded = Utils.toUTF8(await this.decryptPermissionTokenField(typeRaw))
2377
+ const verifierDec = Utils.toUTF8(await this.decryptPermissionTokenField(verifierRaw))
2378
+ const fieldsJson = await this.decryptPermissionTokenField(fieldsRaw)
2379
+ const allFields = JSON.parse(Utils.toUTF8(fieldsJson)) as string[]
2380
+ seen.add(out.outpoint)
2381
+ tokens.push({
2382
+ tx: tx.toBEEF(),
2383
+ txid: out.outpoint.split('.')[0],
2384
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
2385
+ satoshis: out.satoshis,
2386
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
2387
+ originator: normalizedDomain,
2388
+ rawOriginator: domainDecoded,
2389
+ privileged: privDecoded,
2390
+ certType: typeDecoded,
2391
+ certFields: allFields,
2392
+ verifier: verifierDec,
2393
+ expiry: expiryDecoded
2394
+ })
2395
+ }
2234
2396
  }
2235
2397
  return tokens
2236
2398
  }
@@ -2881,8 +3043,10 @@ export class WalletPermissionsManager implements WalletInterface {
2881
3043
  public async waitForAuthentication(
2882
3044
  ...args: Parameters<WalletInterface['waitForAuthentication']>
2883
3045
  ): ReturnType<WalletInterface['waitForAuthentication']> {
2884
- const [_, originator] = args
3046
+ let [_, originator] = args
2885
3047
  if (this.config.seekGroupedPermission && originator) {
3048
+ const { normalized: normalizedOriginator } = this.prepareOriginator(originator)
3049
+ originator = normalizedOriginator
2886
3050
  // 1. Fetch manifest.json from the originator
2887
3051
  let groupPermissions: GroupedPermissions | undefined
2888
3052
  try {
@@ -2970,7 +3134,7 @@ export class WalletPermissionsManager implements WalletInterface {
2970
3134
  try {
2971
3135
  await new Promise<boolean>(async (resolve, reject) => {
2972
3136
  this.activeRequests.set(key, {
2973
- request: { originator, permissions: permissionsToRequest },
3137
+ request: { originator: originator as string, permissions: permissionsToRequest },
2974
3138
  pending: [{ resolve, reject }]
2975
3139
  })
2976
3140
 
@@ -3021,7 +3185,7 @@ export class WalletPermissionsManager implements WalletInterface {
3021
3185
 
3022
3186
  /** Returns true if the specified origin is the admin originator. */
3023
3187
  private isAdminOriginator(originator: string): boolean {
3024
- return originator === this.adminOriginator
3188
+ return this.normalizeOriginator(originator) === this.adminOriginator
3025
3189
  }
3026
3190
 
3027
3191
  /**
@@ -3091,20 +3255,100 @@ export class WalletPermissionsManager implements WalletInterface {
3091
3255
  this.permissionCache.set(key, { expiry, cachedAt: Date.now() })
3092
3256
  }
3093
3257
 
3258
+ /** Records that a non-spending permission was just granted so we can skip re-prompting briefly. */
3259
+ private markRecentGrant(request: PermissionRequest): void {
3260
+ if (request.type === 'spending') return
3261
+ const key = this.buildRequestKey(request)
3262
+ if (!key) return
3263
+ this.recentGrants.set(key, Date.now() + WalletPermissionsManager.RECENT_GRANT_COVER_MS)
3264
+ }
3265
+
3266
+ /** Returns true if we are inside the short "cover window" immediately after granting permission. */
3267
+ private isRecentlyGranted(key: string): boolean {
3268
+ const expiry = this.recentGrants.get(key)
3269
+ if (!expiry) return false
3270
+ if (Date.now() > expiry) {
3271
+ this.recentGrants.delete(key)
3272
+ return false
3273
+ }
3274
+ return true
3275
+ }
3276
+
3277
+ /** Normalizes and canonicalizes originator domains (e.g., lowercase + drop default ports). */
3278
+ private normalizeOriginator(originator?: string): string {
3279
+ if (!originator) return ''
3280
+ const trimmed = originator.trim()
3281
+ if (!trimmed) {
3282
+ return ''
3283
+ }
3284
+
3285
+ try {
3286
+ const hasScheme = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(trimmed)
3287
+ const candidate = hasScheme ? trimmed : `https://${trimmed}`
3288
+ const url = new URL(candidate)
3289
+ if (!url.hostname) {
3290
+ return trimmed.toLowerCase()
3291
+ }
3292
+ const hostname = url.hostname.toLowerCase()
3293
+ const needsBrackets = hostname.includes(':')
3294
+ const baseHost = needsBrackets ? `[${hostname}]` : hostname
3295
+ const port = url.port
3296
+ const defaultPort = WalletPermissionsManager.DEFAULT_PORTS[url.protocol]
3297
+ if (port && defaultPort && port === defaultPort) {
3298
+ return baseHost
3299
+ }
3300
+ return port ? `${baseHost}:${port}` : baseHost
3301
+ } catch {
3302
+ // Fall back to a conservative lowercase trim if URL parsing fails.
3303
+ return trimmed.toLowerCase()
3304
+ }
3305
+ }
3306
+
3307
+ /**
3308
+ * Produces a normalized originator value along with the set of legacy
3309
+ * representations that should be considered when searching for existing
3310
+ * permission tokens (for backwards compatibility).
3311
+ */
3312
+ private prepareOriginator(originator?: string): { normalized: string; lookupValues: string[] } {
3313
+ const trimmed = originator?.trim()
3314
+ if (!trimmed) {
3315
+ throw new Error('Originator is required for permission checks.')
3316
+ }
3317
+ const normalized = this.normalizeOriginator(trimmed) || trimmed.toLowerCase()
3318
+ const lookupValues = Array.from(new Set([trimmed, normalized])).filter(Boolean)
3319
+ return { normalized, lookupValues }
3320
+ }
3321
+
3322
+ /**
3323
+ * Builds a unique list of originator variants that should be searched when
3324
+ * looking up on-chain tokens (e.g., legacy raw + normalized forms).
3325
+ */
3326
+ private buildOriginatorLookupValues(...origins: Array<string | undefined>): string[] {
3327
+ const variants = new Set<string>()
3328
+ for (const origin of origins) {
3329
+ const trimmed = origin?.trim()
3330
+ if (trimmed) {
3331
+ variants.add(trimmed)
3332
+ }
3333
+ }
3334
+ return Array.from(variants)
3335
+ }
3336
+
3094
3337
  /**
3095
3338
  * Builds a "map key" string so that identical requests (e.g. "protocol:domain:true:protoName:counterparty")
3096
3339
  * do not produce multiple user prompts.
3097
3340
  */
3098
3341
  private buildRequestKey(r: PermissionRequest): string {
3342
+ const normalizedOriginator = this.normalizeOriginator(r.originator)
3099
3343
  switch (r.type) {
3100
3344
  case 'protocol':
3101
- return `proto:${r.originator}:${!!r.privileged}:${r.protocolID?.join(',')}:${r.counterparty}`
3345
+ return `proto:${normalizedOriginator}:${!!r.privileged}:${r.protocolID?.join(',')}:${r.counterparty}`
3102
3346
  case 'basket':
3103
- return `basket:${r.originator}:${r.basket}`
3347
+ return `basket:${normalizedOriginator}:${r.basket}`
3104
3348
  case 'certificate':
3105
- return `cert:${r.originator}:${!!r.privileged}:${r.certificate?.verifier}:${r.certificate?.certType}:${r.certificate?.fields.join('|')}`
3349
+ return `cert:${normalizedOriginator}:${!!r.privileged}:${r.certificate?.verifier}:${r.certificate?.certType}:${r.certificate?.fields.join('|')}`
3106
3350
  case 'spending':
3107
- return `spend:${r.originator}:${r.spending?.satoshis}`
3351
+ return `spend:${normalizedOriginator}:${r.spending?.satoshis}`
3108
3352
  }
3109
3353
  }
3110
3354
  }