@bsv/wallet-toolbox 1.6.24 → 1.6.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/wallet-toolbox",
3
- "version": "1.6.24",
3
+ "version": "1.6.26",
4
4
  "description": "BRC100 conforming wallet, wallet storage and wallet signer components",
5
5
  "main": "./out/src/index.js",
6
6
  "types": "./out/src/index.d.ts",
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "@bsv/auth-express-middleware": "^1.2.3",
35
35
  "@bsv/payment-express-middleware": "^1.2.3",
36
- "@bsv/sdk": "^1.7.6",
36
+ "@bsv/sdk": "^1.8.2",
37
37
  "express": "^4.21.2",
38
38
  "idb": "^8.0.2",
39
39
  "knex": "^3.1.0",
@@ -7,7 +7,9 @@ import {
7
7
  WalletProtocol,
8
8
  Base64String,
9
9
  PubKeyHex,
10
- SecurityLevels
10
+ SecurityLevels,
11
+ CreateActionInput,
12
+ Beef
11
13
  } from '@bsv/sdk'
12
14
  import { validateCreateActionArgs } from './sdk'
13
15
 
@@ -656,17 +658,39 @@ export class WalletPermissionsManager implements WalletInterface {
656
658
  )
657
659
  }
658
660
  for (const p of params.granted.protocolPermissions || []) {
659
- await this.createPermissionOnChain(
660
- {
661
- type: 'protocol',
662
- originator,
663
- privileged: false, // No privileged protocols allowed in groups for added security.
664
- protocolID: p.protocolID,
665
- counterparty: p.counterparty || 'self',
666
- reason: p.description
667
- },
668
- expiry
661
+ const token = await this.findProtocolToken(
662
+ originator,
663
+ false, // No privileged protocols allowed in groups for added security.
664
+ p.protocolID,
665
+ p.counterparty || 'self',
666
+ true
669
667
  )
668
+ 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
+ )
681
+ } 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
+ )
693
+ }
670
694
  }
671
695
  for (const b of params.granted.basketAccess || []) {
672
696
  await this.createPermissionOnChain(
@@ -1335,6 +1359,89 @@ export class WalletPermissionsManager implements WalletInterface {
1335
1359
  return undefined
1336
1360
  }
1337
1361
 
1362
+ /** Finds ALL DPACP permission tokens matching origin/domain, privileged, protocol, cpty. Never filters by expiry. */
1363
+ private async findAllProtocolTokens(
1364
+ originator: string,
1365
+ privileged: boolean,
1366
+ protocolID: WalletProtocol,
1367
+ counterparty: string
1368
+ ): Promise<PermissionToken[]> {
1369
+ 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
+ }
1379
+
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
+ )
1389
+
1390
+ const matches: PermissionToken[] = []
1391
+
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
+ }
1427
+
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
+ })
1441
+ }
1442
+
1443
+ return matches
1444
+ }
1338
1445
  /** Looks for a DBAP token matching (originator, basket). */
1339
1446
  private async findBasketToken(
1340
1447
  originator: string,
@@ -1569,6 +1676,70 @@ export class WalletPermissionsManager implements WalletInterface {
1569
1676
  )
1570
1677
  }
1571
1678
 
1679
+ private async coalescePermissionTokens(
1680
+ oldTokens: PermissionToken[],
1681
+ newScript: LockingScript,
1682
+ opts?: {
1683
+ tags?: string[]
1684
+ basket?: string
1685
+ description?: string
1686
+ }
1687
+ ): Promise<string> {
1688
+ if (!oldTokens?.length) throw new Error('No permission tokens to coalesce')
1689
+ if (oldTokens.length < 2) throw new Error('Need at least 2 tokens to coalesce')
1690
+
1691
+ // 1) Create a signable action with N inputs and a single renewed output
1692
+ const { signableTransaction } = await this.createAction(
1693
+ {
1694
+ description: opts?.description ?? `Coalesce ${oldTokens.length} permission tokens`,
1695
+ inputs: oldTokens.map((t, i) => ({
1696
+ outpoint: `${t.txid}.${t.outputIndex}`,
1697
+ unlockingScriptLength: 74,
1698
+ inputDescription: `Consume permission token #${i + 1}`
1699
+ })),
1700
+ outputs: [
1701
+ {
1702
+ lockingScript: newScript.toHex(),
1703
+ satoshis: 1,
1704
+ outputDescription: 'Renewed permission token',
1705
+ ...(opts?.basket ? { basket: opts.basket } : {}),
1706
+ ...(opts?.tags ? { tags: opts.tags } : {})
1707
+ }
1708
+ ],
1709
+ options: {
1710
+ acceptDelayedBroadcast: false,
1711
+ randomizeOutputs: false,
1712
+ signAndProcess: false
1713
+ }
1714
+ },
1715
+ this.adminOriginator
1716
+ )
1717
+
1718
+ if (!signableTransaction?.reference || !signableTransaction.tx) {
1719
+ throw new Error('Failed to create signable transaction')
1720
+ }
1721
+
1722
+ // 2) Sign each input
1723
+ const partialTx = Transaction.fromAtomicBEEF(signableTransaction.tx)
1724
+ const pushdrop = new PushDrop(this.underlying)
1725
+ const unlocker = pushdrop.unlock(WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL, '1', 'self')
1726
+
1727
+ const spends: Record<number, { unlockingScript: string }> = {}
1728
+ for (let i = 0; i < oldTokens.length; i++) {
1729
+ // The signable transaction already contains the necessary prevout context
1730
+ const unlockingScript = await unlocker.sign(partialTx, i)
1731
+ spends[i] = { unlockingScript: unlockingScript.toHex() }
1732
+ }
1733
+
1734
+ // 3) Finalize the action
1735
+ const { txid } = await this.underlying.signAction({
1736
+ reference: signableTransaction.reference,
1737
+ spends
1738
+ })
1739
+
1740
+ if (!txid) throw new Error('Failed to finalize coalescing transaction')
1741
+ return txid
1742
+ }
1572
1743
  /**
1573
1744
  * Renews a permission token by spending the old token as input and creating a new token output.
1574
1745
  * This invalidates the old token and replaces it with a new one.
@@ -1596,57 +1767,74 @@ export class WalletPermissionsManager implements WalletInterface {
1596
1767
  true,
1597
1768
  true
1598
1769
  )
1599
-
1600
1770
  const tags = this.buildTagsForRequest(r)
1771
+ // Check if there are multiple old tokens for the same parameters (shouldn't usually happen)
1772
+ const oldTokens = await this.findAllProtocolTokens(
1773
+ oldToken.originator,
1774
+ oldToken.privileged!,
1775
+ [oldToken.securityLevel!, oldToken.protocol!],
1776
+ oldToken.counterparty!
1777
+ )
1601
1778
 
1602
- // 3) For BRC-100, we do a "createAction" with a partial input referencing oldToken
1603
- // plus a single new output. We'll hydrate the template, then signAction for the wallet to finalize.
1604
- const oldOutpoint = `${oldToken.txid}.${oldToken.outputIndex}`
1605
- const { signableTransaction } = await this.createAction(
1606
- {
1607
- description: `Renew ${r.type} permission`,
1608
- inputBEEF: oldToken.tx,
1609
- inputs: [
1610
- {
1611
- outpoint: oldOutpoint,
1612
- unlockingScriptLength: 73, // length of signature
1613
- inputDescription: `Consume old ${r.type} token`
1779
+ // If so, coalesce them into a single token first, to avoid bloat
1780
+ if (oldTokens.length > 1) {
1781
+ const txid = await this.coalescePermissionTokens(oldTokens, newScript, {
1782
+ tags,
1783
+ basket: BASKET_MAP[r.type],
1784
+ description: `Coalesce ${r.type} permission tokens`
1785
+ })
1786
+ console.log('Coalesced permission tokens:', txid)
1787
+ } else {
1788
+ // Otherwise, just proceed with the single-token renewal
1789
+ // 3) For BRC-100, we do a "createAction" with a partial input referencing oldToken
1790
+ // plus a single new output. We'll hydrate the template, then signAction for the wallet to finalize.
1791
+ const oldOutpoint = `${oldToken.txid}.${oldToken.outputIndex}`
1792
+ const { signableTransaction } = await this.createAction(
1793
+ {
1794
+ description: `Renew ${r.type} permission`,
1795
+ inputBEEF: oldToken.tx,
1796
+ inputs: [
1797
+ {
1798
+ outpoint: oldOutpoint,
1799
+ unlockingScriptLength: 73, // length of signature
1800
+ inputDescription: `Consume old ${r.type} token`
1801
+ }
1802
+ ],
1803
+ outputs: [
1804
+ {
1805
+ lockingScript: newScript.toHex(),
1806
+ satoshis: 1,
1807
+ outputDescription: `Renewed ${r.type} permission token`,
1808
+ basket: BASKET_MAP[r.type],
1809
+ tags
1810
+ }
1811
+ ],
1812
+ options: {
1813
+ acceptDelayedBroadcast: false
1614
1814
  }
1615
- ],
1616
- outputs: [
1617
- {
1618
- lockingScript: newScript.toHex(),
1619
- satoshis: 1,
1620
- outputDescription: `Renewed ${r.type} permission token`,
1621
- basket: BASKET_MAP[r.type],
1622
- tags
1815
+ },
1816
+ this.adminOriginator
1817
+ )
1818
+ const tx = Transaction.fromBEEF(signableTransaction!.tx)
1819
+ const unlocker = new PushDrop(this.underlying).unlock(
1820
+ WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
1821
+ '1',
1822
+ 'self',
1823
+ 'all',
1824
+ false,
1825
+ 1,
1826
+ LockingScript.fromHex(oldToken.outputScript)
1827
+ )
1828
+ const unlockingScript = await unlocker.sign(tx, 0)
1829
+ await this.underlying.signAction({
1830
+ reference: signableTransaction!.reference,
1831
+ spends: {
1832
+ 0: {
1833
+ unlockingScript: unlockingScript.toHex()
1623
1834
  }
1624
- ],
1625
- options: {
1626
- acceptDelayedBroadcast: false
1627
- }
1628
- },
1629
- this.adminOriginator
1630
- )
1631
- const tx = Transaction.fromBEEF(signableTransaction!.tx)
1632
- const unlocker = new PushDrop(this.underlying).unlock(
1633
- WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
1634
- '1',
1635
- 'self',
1636
- 'all',
1637
- false,
1638
- 1,
1639
- LockingScript.fromHex(oldToken.outputScript)
1640
- )
1641
- const unlockingScript = await unlocker.sign(tx, 0)
1642
- await this.underlying.signAction({
1643
- reference: signableTransaction!.reference,
1644
- spends: {
1645
- 0: {
1646
- unlockingScript: unlockingScript.toHex()
1647
1835
  }
1648
- }
1649
- })
1836
+ })
1837
+ }
1650
1838
  }
1651
1839
 
1652
1840
  /**