@bopen-io/color-permission-module 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -59,19 +59,34 @@ the manager's `getVersion` string.
59
59
  indices and the approved facts name the requested outputs). The module selects
60
60
  admitted token inputs and adds token change in `createAction`, and supplies
61
61
  the source transactions of the inputs it selects in `inputBEEF`, merged with
62
- any `inputBEEF` the caller gave: it reads the asset basket with entire
63
- transactions a page of 10 outputs at a time, stops at the page that returns
64
- the last selected transaction, and keeps only each selected transaction with
65
- its proof or unproven ancestors. A selected output whose transaction the
66
- wallet does not return in full fails the call, naming the outpoint. The
67
- module keeps its record of each input it signs for (outpoint, script,
68
- satoshis, asset and decrypted custody) from `createAction` until that
69
- action's `signAction`, and signs from those records without listing the
70
- wallet's outputs; a wallet may hide an action's reserved inputs from its
71
- listings (wallet-toolbox's `auto` action-batch mode does). The records are
72
- dropped when `signAction` signs or refuses, and on `dispose()`; a record of an
73
- action aborted before `signAction` stays until the output is selected again
74
- or `dispose()`. In
62
+ any `inputBEEF` the caller gave. Every output the module files (token
63
+ change, owned outputs, receipts, deploys and handoff splits) carries the tag
64
+ `p color <hex>`: the lowercase hex of the SHA-256 of the UTF-8 bytes of its
65
+ custody's derivation suffix. The tag is 72 characters
66
+ whatever the suffix, and storage keeps nothing of the suffix itself. The module reads the
67
+ transactions of the inputs it selects with one `listOutputs` per asset
68
+ basket, by their tags (`tagQueryMode: 'any'`, `limit` the number of
69
+ selected inputs), checks that the wallet returned exactly the selected
70
+ outputs, and keeps only each selected transaction with its proof or
71
+ unproven ancestors. An output without its suffix tag is not selected, and an app-named
72
+ input without it is refused, so outputs filed without the tag are not spent
73
+ through the module. Suffix tags are
74
+ left out of the `p color assets` view. A selected output whose transaction
75
+ the wallet does not return in full fails the call, naming the outpoint. The
76
+ module stages its record of each input it signs for (outpoint, script,
77
+ satoshis, asset and decrypted custody) when `createAction` funds the action,
78
+ and when the wallet's `createAction` comes back it binds the records of the
79
+ inputs of the returned unsigned transaction to that action's reference.
80
+ `signAction` signs only the inputs bound to its reference, without listing
81
+ the wallet's outputs; a wallet may hide an action's reserved inputs from its
82
+ listings (wallet-toolbox's `auto` action-batch mode does). Which outputs are
83
+ available is storage's to decide; the module also skips outputs staged or
84
+ being selected by its own calls. A reference's records are dropped when
85
+ `signAction` signs or refuses, and on `dispose()`. Two cases leave records
86
+ until `dispose()`: an action aborted before `signAction` leaves its bound
87
+ records unused, and a wallet `createAction` that fails after the module
88
+ funded it leaves its inputs staged, so the module does not select them again
89
+ until `dispose()`. In
75
90
  `signAction` it checks the funded transaction against the plan, asks the user
76
91
  to approve it (`color_send`: every asset leg, satoshi totals and fee, the
77
92
  signatures it releases), signs the token inputs with any `sighashTypes`
@@ -133,10 +148,9 @@ The module fails explicitly, rather than falling back, on:
133
148
 
134
149
  Explicit inputs are supported: this wallet's `p color` outputs are signed by the
135
150
  module, and foreign token inputs count only once their asset's overlay admits
136
- them. The caller supplies the source transactions of the inputs it names in
137
- `inputBEEF`, as wallet-toolbox requires for explicit inputs; the module adds
138
- them only for the inputs it selects. An input missing from `inputBEEF` is
139
- resolved by the wallet and checked in `signAction`. `inputPositions` and token `changePositions` are honored; change
151
+ them. The module supplies `inputBEEF` for the inputs it selects; the source
152
+ transactions of an input the app names are the app's to supply in `inputBEEF`,
153
+ as wallet-toolbox requires for explicit inputs. `inputPositions` and token `changePositions` are honored; change
140
154
  positions follow the requested outputs.
141
155
 
142
156
  The module does not validate BRC-176 lineage: proof transport is not part of
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { AtomicBEEF, BEEF, BasketInsertion, Beef, CreateActionArgs, CreateActionInput, CreateActionOutput, CreateSignatureArgs, InternalizeActionArgs, ListOutputsArgs, ListOutputsResult, LockingScript, LookupAnswer, STEAK, SignActionArgs, SignActionResult, Transaction, WalletCounterparty, WalletInterface, WalletOutput, WalletProtocol } from "@bsv/sdk";
1
+ import { AtomicBEEF, BEEF, BasketInsertion, Beef, CreateActionArgs, CreateActionInput, CreateActionOutput, CreateActionResult, CreateSignatureArgs, InternalizeActionArgs, ListOutputsArgs, ListOutputsResult, LockingScript, LookupAnswer, STEAK, SignActionArgs, SignActionResult, Transaction, WalletCounterparty, WalletInterface, WalletOutput, WalletProtocol } from "@bsv/sdk";
2
2
  import { PermissionsModule, PermissionsModuleAction, PermissionsModuleNext, PermissionsModuleRequest } from "@bsv/wallet-toolbox-client";
3
3
  //#region src/strictJson.d.ts
4
4
  type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject;
@@ -188,6 +188,8 @@ interface Candidate extends SourceOutput {
188
188
  token: TokenOutput;
189
189
  amount: bigint;
190
190
  customInstructions: string;
191
+ /** The tags the wallet listed for the output. */
192
+ tags: readonly string[];
191
193
  }
192
194
  interface ManagedInput extends SourceOutput {
193
195
  assetId: string;
@@ -349,15 +351,14 @@ export declare class ColorPermissionModule implements PermissionsModule {
349
351
  readonly capabilities: Readonly<Record<string, string>>;
350
352
  /** Outputs a send in progress has selected, so a concurrent send does not select them too. */
351
353
  protected readonly selected: Set<string>;
352
- /**
353
- * The managed inputs of each funded action, by outpoint, kept from createAction until its
354
- * signAction so the module signs from its own records instead of listing the wallet's outputs.
355
- */
356
- protected readonly held: Map<string, ManagedInput>;
354
+ /** The managed inputs of funded actions whose createAction has not come back, by outpoint. */
355
+ protected readonly staged: Map<string, ManagedInput>;
356
+ /** The managed inputs the module signs for, by the action reference they were bound to. */
357
+ protected readonly bound: Map<string, ManagedInput[]>;
357
358
  protected readonly supportedAssets: Map<string, Promise<void>>;
358
359
  protected identityKey?: Promise<string>;
359
360
  constructor(options: ColorPermissionModuleOptions);
360
- /** Clears in-memory selections, kept input records and asset checks. */
361
+ /** Clears in-memory selections, staged and bound input records and asset checks. */
361
362
  dispose(): void;
362
363
  /** Protocol- and basket-routed calls: key and signature operations and the read views. */
363
364
  handleRequest(req: PermissionsModuleRequest, next: PermissionsModuleNext): Promise<unknown>;
@@ -376,8 +377,11 @@ export declare class ColorPermissionModule implements PermissionsModule {
376
377
  * before it can be spent.
377
378
  */
378
379
  protected fileDeploy(result: SignActionResult, action: PermissionsModuleAction, originator: string): Promise<void>;
379
- /** Verifies every `p color` receipt; the caller must already have submitted the transaction. */
380
- protected internalizeAction(args: InternalizeActionArgs, originator: string): Promise<void>;
380
+ /**
381
+ * Verifies every `p color` receipt and tags it with its derivation suffix; the caller must
382
+ * already have submitted the transaction.
383
+ */
384
+ protected internalizeAction(args: InternalizeActionArgs, originator: string): Promise<InternalizeActionArgs>;
381
385
  /**
382
386
  * Checks a received transaction: every input unlocks its source and every asset it involves is
383
387
  * supported. Token amounts are not re-evaluated: the received output must be admitted and
@@ -387,9 +391,16 @@ export declare class ColorPermissionModule implements PermissionsModule {
387
391
  /** Checks each distinct asset in order, so the first unsupported one is the one reported. */
388
392
  protected assertSupportedAssets(assetIds: Iterable<string>): Promise<void>;
389
393
  /** One receipt: its custody, its output's asset, owner and basket, and overlay admission. */
390
- protected verifyReceipt(tx: Transaction, outputIndex: number, remittance: BasketInsertion, originator: string): Promise<void>;
394
+ protected verifyReceipt(tx: Transaction, outputIndex: number, remittance: BasketInsertion, originator: string): Promise<Custody>;
391
395
  /** Checks a planned token action and adds its selected token inputs and per-asset change. */
392
396
  protected createAction(args: CreateActionArgs, originator: string): Promise<CreateActionArgs>;
397
+ /** Stages a funded action's managed inputs until its createAction comes back. */
398
+ protected stage(inputs: readonly ManagedInput[]): void;
399
+ /**
400
+ * Moves the staged inputs the returned transaction spends to its action reference. A result
401
+ * without a signable transaction binds nothing; its named inputs are only unstaged.
402
+ */
403
+ protected bindAction(result: CreateActionResult): void;
393
404
  protected reserve(reserved: Set<string>, inputs: ReadonlyArray<{
394
405
  outpoint: string;
395
406
  }>): void;
@@ -475,10 +486,10 @@ export declare class ColorPermissionModule implements PermissionsModule {
475
486
  */
476
487
  protected inputBeef(callerBEEF: BEEF | undefined, inputs: readonly ManagedInput[], originator: string): Promise<BEEF | undefined>;
477
488
  /**
478
- * Reads one basket with entire transactions, a page at a time, until every input's source
479
- * transaction has been returned, so no more BEEF is read than the selection needs.
489
+ * The selected inputs of one basket with their entire transactions, in one listing by their
490
+ * suffix tags. The listing must return exactly the selected outputs.
480
491
  */
481
- protected sourceTransactions(basket: string, inputs: readonly ManagedInput[], originator: string): Promise<Beef>;
492
+ protected sourceTransactions(basket: string, selected: readonly ManagedInput[], originator: string): Promise<Beef>;
482
493
  /** One basket's token outputs of the asset, read from their locking scripts, excluding selected ones. */
483
494
  protected candidates(assetId: string, role: TokenRole, originator: string): Promise<Candidate[]>;
484
495
  /** A chosen candidate with its custody, or undefined when its custody record is unreadable. */
@@ -506,10 +517,8 @@ export declare class ColorPermissionModule implements PermissionsModule {
506
517
  * `p color` outputs.
507
518
  */
508
519
  protected signAction(args: SignActionArgs, action: PermissionsModuleAction | undefined, originator: string): Promise<SignActionArgs>;
509
- /** The kept records of the module's inputs among the action's inputs. */
510
- protected heldInputs(createArgs: CreateActionArgs): ManagedInput[];
511
520
  /** Rechecks, approves and signs the module's inputs of a funded action. */
512
- protected signHeld(args: SignActionArgs, funded: {
521
+ protected signBound(args: SignActionArgs, funded: {
513
522
  plan: ColorPlan;
514
523
  createArgs: CreateActionArgs;
515
524
  tx: Transaction;
@@ -554,7 +563,7 @@ export declare class ColorPermissionModule implements PermissionsModule {
554
563
  /** The queried outputs the overlay reports admitted and unspent, matched to their source bytes. */
555
564
  protected spendable(assetId: string, outputs: readonly SourceOutput[]): Promise<Set<string>>;
556
565
  /** All spendable outputs of one basket, with locking scripts and custom instructions. */
557
- protected listBasket(basket: string, originator: string, filter: Pick<ListOutputsArgs, 'tags' | 'tagQueryMode'>): Promise<WalletOutput[]>;
566
+ protected listBasket(basket: string, originator: string, filter: Pick<ListOutputsArgs, 'tags' | 'tagQueryMode' | 'includeTags'>): Promise<WalletOutput[]>;
558
567
  protected sourceOutput(output: WalletOutput): SourceOutput;
559
568
  protected preimage(tx: Transaction, inputIndex: number, source: SourceOutput, scope: number): number[];
560
569
  /** Unlocking scripts for this module's inputs of `template`, keyed by input index. */
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Beef, LockingScript, PublicKey, Signature, Spend, Transaction, TransactionSignature, UnlockingScript } from "@bsv/sdk";
2
- import { hash160, hash256 } from "@bsv/sdk/primitives/Hash";
2
+ import { hash160, hash256, sha256 } from "@bsv/sdk/primitives/Hash";
3
3
  import Random from "@bsv/sdk/primitives/Random";
4
4
  import { toArray, toBase64, toHex } from "@bsv/sdk/primitives/utils";
5
5
  import { Bsv21Token, OneColor, StrictCborLink, isTokenShaped, tryDecodeStrictCbor } from "@bsv/templates";
@@ -140,6 +140,19 @@ function parseBalanceSelector(basket) {
140
140
  }
141
141
  /** The shared deployment discovery topic. */
142
142
  const DISCOVERY_TOPIC = "tm_color";
143
+ const SUFFIX_TAG = /^p color [0-9a-f]{64}$/;
144
+ /**
145
+ * The tag filed on a managed output: `p color ` and the lowercase hex of the SHA-256 of the UTF-8
146
+ * bytes of its custody's derivation suffix. It has a fixed length whatever the suffix, and storage
147
+ * keeps nothing of the suffix itself.
148
+ */
149
+ function derivationSuffixTag(derivationSuffix) {
150
+ return `${COLOR_BASKET_NAMESPACE} ${toHex(sha256(toArray(derivationSuffix, "utf8")))}`;
151
+ }
152
+ /** Whether a tag is a derivation suffix tag. */
153
+ function isDerivationSuffixTag(tag) {
154
+ return SUFFIX_TAG.test(tag);
155
+ }
143
156
  function colorTopic(assetId) {
144
157
  return `tm_color_${assetId}`;
145
158
  }
@@ -698,8 +711,6 @@ const DEFAULT_SIGHASH = TransactionSignature.SIGHASH_ALL | TransactionSignature.
698
711
  const MAX_ENVELOPE_BYTES = 16777216;
699
712
  const MAX_RECEIPT_INSTRUCTION_BYTES = 1e3;
700
713
  const LIST_PAGE_LIMIT = 1e4;
701
- /** Outputs per page when reading selected inputs' source transactions, which carry BEEF. */
702
- const INPUT_BEEF_PAGE_LIMIT = 10;
703
714
  const SUBMIT_ATTEMPTS = 3;
704
715
  const NOSEND_EXPIRY_LABEL_PREFIX = "p nosend expiry ";
705
716
  const STATE_TAG_PREFIX = "color_state_";
@@ -743,6 +754,22 @@ function mergeSource(into, from, outpoint) {
743
754
  if (!holdsTransaction(from, txid)) throw new Error(`BRC-207: the wallet returned no source transaction for token input ${outpoint}`);
744
755
  into.mergeBeef(from.toBinaryAtomic(txid));
745
756
  }
757
+ /** The tag every output the module files carries: the module finds an output's transaction by it. */
758
+ function suffixTagOf(custody) {
759
+ return derivationSuffixTag(custody.derivationSuffix);
760
+ }
761
+ /** `tags` with the custody's suffix tag added. */
762
+ function withSuffixTag(tags, custody) {
763
+ return [.../* @__PURE__ */ new Set([...tags ?? [], suffixTagOf(custody)])];
764
+ }
765
+ /** Whether a listed output carries the suffix tag of its custody. */
766
+ function carriesSuffixTag(tags, custody) {
767
+ return tags?.includes(suffixTagOf(custody)) === true;
768
+ }
769
+ /** The BEEF a listOutputs result returns, empty when it returns none. */
770
+ function listingBeef(listing) {
771
+ return listing.BEEF === void 0 ? new Beef() : Beef.fromBinary(listing.BEEF);
772
+ }
746
773
  function tokenInputs(inputs) {
747
774
  return inputs.map((input) => ({
748
775
  outpoint: input.outpoint,
@@ -1177,13 +1204,13 @@ function tryReadTokenOutput(lockingScript, outpoint) {
1177
1204
  return;
1178
1205
  }
1179
1206
  }
1180
- /** A record with its stored tags and the computed state tag in place of any stored one. */
1207
+ /** A record with its stored tags, less suffix tags, and the computed state tag in place of any stored one. */
1181
1208
  function inventoryView(record) {
1182
1209
  const state = inventoryState(record);
1183
1210
  return {
1184
1211
  record,
1185
1212
  state,
1186
- tags: [...(record.output.tags ?? []).filter((tag) => !tag.startsWith(STATE_TAG_PREFIX)), `${STATE_TAG_PREFIX}${state}`]
1213
+ tags: [...(record.output.tags ?? []).filter((tag) => !tag.startsWith(STATE_TAG_PREFIX) && !isDerivationSuffixTag(tag)), `${STATE_TAG_PREFIX}${state}`]
1187
1214
  };
1188
1215
  }
1189
1216
  /** A BEEF holding only the transactions of the returned page. */
@@ -1215,11 +1242,10 @@ var ColorPermissionModule = class {
1215
1242
  capabilities = { "brc207-color": "5" };
1216
1243
  /** Outputs a send in progress has selected, so a concurrent send does not select them too. */
1217
1244
  selected = /* @__PURE__ */ new Set();
1218
- /**
1219
- * The managed inputs of each funded action, by outpoint, kept from createAction until its
1220
- * signAction so the module signs from its own records instead of listing the wallet's outputs.
1221
- */
1222
- held = /* @__PURE__ */ new Map();
1245
+ /** The managed inputs of funded actions whose createAction has not come back, by outpoint. */
1246
+ staged = /* @__PURE__ */ new Map();
1247
+ /** The managed inputs the module signs for, by the action reference they were bound to. */
1248
+ bound = /* @__PURE__ */ new Map();
1223
1249
  supportedAssets = /* @__PURE__ */ new Map();
1224
1250
  identityKey;
1225
1251
  constructor(options) {
@@ -1230,10 +1256,11 @@ var ColorPermissionModule = class {
1230
1256
  this.provider = options.provider;
1231
1257
  this.requestTokenAccess = options.requestTokenAccess;
1232
1258
  }
1233
- /** Clears in-memory selections, kept input records and asset checks. */
1259
+ /** Clears in-memory selections, staged and bound input records and asset checks. */
1234
1260
  dispose() {
1235
1261
  this.selected.clear();
1236
- this.held.clear();
1262
+ this.staged.clear();
1263
+ this.bound.clear();
1237
1264
  this.supportedAssets.clear();
1238
1265
  this.identityKey = void 0;
1239
1266
  }
@@ -1266,11 +1293,7 @@ var ColorPermissionModule = class {
1266
1293
  switch (req.method) {
1267
1294
  case "createAction": return { args: await this.createAction(req.args, req.originator) };
1268
1295
  case "signAction": return { args: await this.signAction(req.args, req.action, req.originator) };
1269
- case "internalizeAction": {
1270
- const args = req.args;
1271
- await this.internalizeAction(args, req.originator);
1272
- return { args };
1273
- }
1296
+ case "internalizeAction": return { args: await this.internalizeAction(req.args, req.originator) };
1274
1297
  case "listActions": {
1275
1298
  const args = req.args;
1276
1299
  await this.authorizeRead(req.originator, {
@@ -1283,6 +1306,7 @@ var ColorPermissionModule = class {
1283
1306
  }
1284
1307
  }
1285
1308
  async onResponse(res, context) {
1309
+ if (context.method === "createAction") this.bindAction(res);
1286
1310
  if (context.method === "signAction" && context.action !== void 0) {
1287
1311
  await this.fileDeploy(res, context.action, context.originator);
1288
1312
  if (!context.action.noSend) await this.submitSigned(res, context.action.transaction);
@@ -1302,8 +1326,8 @@ var ColorPermissionModule = class {
1302
1326
  txid,
1303
1327
  vout: 0
1304
1328
  });
1305
- const owned = ColorPlan.fromLabels(action.createArgs?.labels)?.ownedOutputs.some((remittance) => remittance.outputIndex === 0 && remittance.assetId === "ISSUE");
1306
- if (deploy?.deploy !== true || owned !== true) return;
1329
+ const remittance = ColorPlan.fromLabels(action.createArgs?.labels)?.ownedOutputs.find((owned) => owned.outputIndex === 0 && owned.assetId === "ISSUE");
1330
+ if (deploy?.deploy !== true || remittance === void 0) return;
1307
1331
  const customInstructions = action.createArgs?.outputs?.[0]?.customInstructions;
1308
1332
  await this.wallet.internalizeAction({
1309
1333
  tx: result.tx,
@@ -1313,12 +1337,16 @@ var ColorPermissionModule = class {
1313
1337
  protocol: "basket insertion",
1314
1338
  insertionRemittance: {
1315
1339
  basket: assetBasket(deploy.assetId, deploy.role),
1316
- ...customInstructions === void 0 ? {} : { customInstructions }
1340
+ ...customInstructions === void 0 ? {} : { customInstructions },
1341
+ tags: [suffixTagOf(remittance)]
1317
1342
  }
1318
1343
  }]
1319
1344
  }, originator);
1320
1345
  }
1321
- /** Verifies every `p color` receipt; the caller must already have submitted the transaction. */
1346
+ /**
1347
+ * Verifies every `p color` receipt and tags it with its derivation suffix; the caller must
1348
+ * already have submitted the transaction.
1349
+ */
1322
1350
  async internalizeAction(args, originator) {
1323
1351
  if ((args.labels ?? []).some((label) => label.startsWith(`p color `))) throw new Error("BRC-207: p color labels have no meaning in internalizeAction");
1324
1352
  const tx = parsed("the received transaction", () => Transaction.fromAtomicBEEF(args.tx));
@@ -1329,11 +1357,23 @@ var ColorPermissionModule = class {
1329
1357
  remittance
1330
1358
  }] : [];
1331
1359
  });
1332
- if (receipts.length === 0) return;
1360
+ if (receipts.length === 0) return args;
1333
1361
  await this.assertCompletedTransaction(tx);
1362
+ const tags = /* @__PURE__ */ new Map();
1334
1363
  await eachInOrder(receipts, async ({ outputIndex, remittance }) => {
1335
- await this.verifyReceipt(tx, outputIndex, remittance, originator);
1364
+ const custody = await this.verifyReceipt(tx, outputIndex, remittance, originator);
1365
+ tags.set(outputIndex, withSuffixTag(remittance.tags, custody));
1336
1366
  });
1367
+ return {
1368
+ ...args,
1369
+ outputs: args.outputs.map((output) => output.insertionRemittance === void 0 || !tags.has(output.outputIndex) ? output : {
1370
+ ...output,
1371
+ insertionRemittance: {
1372
+ ...output.insertionRemittance,
1373
+ tags: tags.get(output.outputIndex)
1374
+ }
1375
+ })
1376
+ };
1337
1377
  }
1338
1378
  /**
1339
1379
  * Checks a received transaction: every input unlocks its source and every asset it involves is
@@ -1374,6 +1414,7 @@ var ColorPermissionModule = class {
1374
1414
  lockingScript: source.lockingScript,
1375
1415
  satoshis: source.satoshis
1376
1416
  }])).has(outpoint)) throw new Error("BRC-207: the received output is not admitted and unspent in its asset overlay");
1417
+ return custody;
1377
1418
  }
1378
1419
  /** Checks a planned token action and adds its selected token inputs and per-asset change. */
1379
1420
  async createAction(args, originator) {
@@ -1391,17 +1432,47 @@ var ColorPermissionModule = class {
1391
1432
  const { inputs, changes } = await this.fund(reserved, plan, planned, explicit, handoff, originator);
1392
1433
  sighashScopes(plan, new Set([...explicit.managed, ...inputs].map((input) => input.outpoint)));
1393
1434
  const inputBEEF = await this.inputBeef(args.inputBEEF, inputs, originator);
1394
- for (const input of [...explicit.managed, ...inputs]) this.held.set(input.outpoint, input);
1395
- return {
1435
+ const funded = {
1396
1436
  ...args,
1397
1437
  inputs: orderInputs(explicit.inputs, tokenInputs(inputs), plan.inputPositions),
1398
1438
  outputs: placeChange(outputs, changes, plan.changePositions),
1399
1439
  ...inputBEEF === void 0 ? {} : { inputBEEF }
1400
1440
  };
1441
+ this.stage([...explicit.managed, ...inputs]);
1442
+ return funded;
1401
1443
  } finally {
1402
1444
  for (const outpoint of reserved) this.selected.delete(outpoint);
1403
1445
  }
1404
1446
  }
1447
+ /** Stages a funded action's managed inputs until its createAction comes back. */
1448
+ stage(inputs) {
1449
+ for (const input of inputs) this.staged.set(input.outpoint, input);
1450
+ }
1451
+ /**
1452
+ * Moves the staged inputs the returned transaction spends to its action reference. A result
1453
+ * without a signable transaction binds nothing; its named inputs are only unstaged.
1454
+ */
1455
+ bindAction(result) {
1456
+ const signable = result.signableTransaction;
1457
+ const atomic = signable?.tx ?? result.tx;
1458
+ if (atomic === void 0) return;
1459
+ let tx;
1460
+ try {
1461
+ tx = Transaction.fromAtomicBEEF(atomic);
1462
+ } catch {
1463
+ return;
1464
+ }
1465
+ for (const input of tx.inputs) {
1466
+ const outpoint = inputOutpoint(input);
1467
+ const staged = this.staged.get(outpoint);
1468
+ if (staged === void 0) continue;
1469
+ this.staged.delete(outpoint);
1470
+ if (signable === void 0) continue;
1471
+ const records = this.bound.get(signable.reference) ?? [];
1472
+ records.push(staged);
1473
+ this.bound.set(signable.reference, records);
1474
+ }
1475
+ }
1405
1476
  reserve(reserved, inputs) {
1406
1477
  for (const input of inputs) {
1407
1478
  this.selected.add(input.outpoint);
@@ -1433,7 +1504,8 @@ var ColorPermissionModule = class {
1433
1504
  basket: sdk.specOpBasketPrefixOutputs,
1434
1505
  tags: [COLOR_BASKET_NAMESPACE],
1435
1506
  include: "locking scripts",
1436
- includeCustomInstructions: true
1507
+ includeCustomInstructions: true,
1508
+ includeTags: true
1437
1509
  }, originator);
1438
1510
  const own = new Map(listed.outputs.filter((output) => outpoints.has(output.outpoint)).map((output) => [output.outpoint, output]));
1439
1511
  const beef = args.inputBEEF === void 0 ? void 0 : parsed("inputBEEF", () => Beef.fromBinary(args.inputBEEF));
@@ -1446,9 +1518,10 @@ var ColorPermissionModule = class {
1446
1518
  }
1447
1519
  /** Counts one of this wallet's `p color` outputs once it is managed, admitted and unspent. */
1448
1520
  async ownInput(explicit, output, originator) {
1449
- if (!output.spendable) throw new Error("BRC-207: an explicit p color input is reserved by another action");
1521
+ if (!output.spendable || this.staged.has(output.outpoint) || this.selected.has(output.outpoint)) throw new Error("BRC-207: an explicit p color input is reserved by another action");
1450
1522
  const managed = await this.managedInput(output, originator);
1451
1523
  if (managed === void 0) throw new Error("BRC-207: an explicit p color input is not a managed token output");
1524
+ if (!carriesSuffixTag(output.tags, managed.custody)) throw new Error(`BRC-207: token output ${managed.outpoint} was filed without its suffix tag and is not spendable through the module`);
1452
1525
  await this.assertSupportedAsset(managed.assetId);
1453
1526
  if (!(await this.spendable(managed.assetId, [managed])).has(managed.outpoint) && !(await this.resubmitHeld(managed.assetId, managed.role, [managed], originator)).has(managed.outpoint)) throw new Error("BRC-207: an explicit p color input is not admitted and unspent");
1454
1527
  explicit.managed.push(managed);
@@ -1568,7 +1641,8 @@ var ColorPermissionModule = class {
1568
1641
  return {
1569
1642
  ...output,
1570
1643
  basket,
1571
- customInstructions: custodyInstructions(custodyOf(remittance))
1644
+ customInstructions: custodyInstructions(custodyOf(remittance)),
1645
+ tags: withSuffixTag(output.tags, remittance)
1572
1646
  };
1573
1647
  }
1574
1648
  /**
@@ -1730,40 +1804,43 @@ var ColorPermissionModule = class {
1730
1804
  if (inputs.length === 0) return callerBEEF;
1731
1805
  const beef = callerBEEF === void 0 ? new Beef() : parsed("inputBEEF", () => Beef.fromBinary(callerBEEF));
1732
1806
  const baskets = /* @__PURE__ */ new Map();
1733
- for (const input of inputs) if (input.sourceBEEF === void 0) {
1807
+ for (const input of inputs) {
1808
+ if (input.sourceBEEF !== void 0) {
1809
+ mergeSource(beef, Beef.fromBinary(input.sourceBEEF), input.outpoint);
1810
+ continue;
1811
+ }
1734
1812
  const basket = assetBasket(input.assetId, input.role);
1735
1813
  baskets.set(basket, [...baskets.get(basket) ?? [], input]);
1736
- } else mergeSource(beef, Beef.fromBinary(input.sourceBEEF), input.outpoint);
1737
- await eachInOrder(baskets, async ([basket, held]) => {
1738
- const sources = await this.sourceTransactions(basket, held, originator);
1739
- for (const input of held) mergeSource(beef, sources, input.outpoint);
1814
+ }
1815
+ await eachInOrder(baskets, async ([basket, selected]) => {
1816
+ const sources = await this.sourceTransactions(basket, selected, originator);
1817
+ for (const input of selected) mergeSource(beef, sources, input.outpoint);
1740
1818
  });
1741
1819
  return beef.toBinary();
1742
1820
  }
1743
1821
  /**
1744
- * Reads one basket with entire transactions, a page at a time, until every input's source
1745
- * transaction has been returned, so no more BEEF is read than the selection needs.
1822
+ * The selected inputs of one basket with their entire transactions, in one listing by their
1823
+ * suffix tags. The listing must return exactly the selected outputs.
1746
1824
  */
1747
- async sourceTransactions(basket, inputs, originator) {
1748
- const txids = new Set(inputs.map((input) => parseOutpoint(input.outpoint).txid));
1749
- const beef = new Beef();
1750
- for (let offset = 0; [...txids].some((txid) => !holdsTransaction(beef, txid));) {
1751
- const page = await this.wallet.listOutputs({
1752
- basket,
1753
- include: "entire transactions",
1754
- limit: INPUT_BEEF_PAGE_LIMIT,
1755
- offset
1756
- }, originator);
1757
- if (page.BEEF !== void 0) beef.mergeBeef(page.BEEF);
1758
- offset += page.outputs.length;
1759
- if (page.outputs.length < INPUT_BEEF_PAGE_LIMIT) break;
1760
- }
1761
- return beef;
1825
+ async sourceTransactions(basket, selected, originator) {
1826
+ const listing = await this.wallet.listOutputs({
1827
+ basket,
1828
+ tags: selected.map((input) => suffixTagOf(input.custody)),
1829
+ tagQueryMode: "any",
1830
+ include: "entire transactions",
1831
+ limit: selected.length
1832
+ }, originator);
1833
+ const wanted = new Set(selected.map((input) => input.outpoint));
1834
+ const returned = new Set(listing.outputs.map((output) => output.outpoint));
1835
+ const missing = [...wanted].filter((outpoint) => !returned.has(outpoint));
1836
+ const extra = [...returned].filter((outpoint) => !wanted.has(outpoint));
1837
+ if (missing.length > 0 || extra.length > 0 || listing.outputs.length !== wanted.size) throw new Error(`BRC-207: the wallet returned other outputs than the selected token inputs: missing ${missing.join(", ") || "none"}; extra ${extra.join(", ") || "none"}`);
1838
+ return listingBeef(listing);
1762
1839
  }
1763
1840
  /** One basket's token outputs of the asset, read from their locking scripts, excluding selected ones. */
1764
1841
  async candidates(assetId, role, originator) {
1765
- return (await this.listBasket(assetBasket(assetId, role), originator, {})).flatMap((output) => {
1766
- if (this.selected.has(output.outpoint) || output.customInstructions === void 0) return [];
1842
+ return (await this.listBasket(assetBasket(assetId, role), originator, { includeTags: true })).flatMap((output) => {
1843
+ if (output.spendable === false || this.selected.has(output.outpoint) || this.staged.has(output.outpoint) || output.customInstructions === void 0 || !(output.tags ?? []).some(isDerivationSuffixTag)) return [];
1767
1844
  const source = this.sourceOutput(output);
1768
1845
  const token = tryReadTokenOutput(source.lockingScript, parseOutpoint(output.outpoint));
1769
1846
  if (token?.assetId !== assetId || token.role !== role) return [];
@@ -1771,20 +1848,23 @@ var ColorPermissionModule = class {
1771
1848
  ...source,
1772
1849
  token,
1773
1850
  amount: token.amount,
1774
- customInstructions: output.customInstructions
1851
+ customInstructions: output.customInstructions,
1852
+ tags: output.tags ?? []
1775
1853
  }];
1776
1854
  });
1777
1855
  }
1778
1856
  /** A chosen candidate with its custody, or undefined when its custody record is unreadable. */
1779
1857
  async readableInput(candidate, originator) {
1780
1858
  try {
1781
- const { outpoint, lockingScript, satoshis, token, customInstructions } = candidate;
1782
- return await this.custodyInput({
1859
+ const { outpoint, lockingScript, satoshis, token, customInstructions, tags } = candidate;
1860
+ const input = await this.custodyInput({
1783
1861
  outpoint,
1784
1862
  lockingScript,
1785
1863
  satoshis,
1786
1864
  token
1787
1865
  }, customInstructions, originator);
1866
+ if (input === void 0 || !carriesSuffixTag(tags, input.custody)) return void 0;
1867
+ return input;
1788
1868
  } catch {
1789
1869
  return;
1790
1870
  }
@@ -1895,6 +1975,7 @@ var ColorPermissionModule = class {
1895
1975
  satoshis: TOKEN_CARRIER_SATOSHIS,
1896
1976
  basket: assetBasket(assetId, "value"),
1897
1977
  customInstructions: custodyInstructions(custody),
1978
+ tags: [suffixTagOf(custody)],
1898
1979
  outputDescription: "BRC-207 token change"
1899
1980
  }
1900
1981
  };
@@ -1912,27 +1993,20 @@ var ColorPermissionModule = class {
1912
1993
  const tx = action.transaction;
1913
1994
  const tokens = transactionTokens(tx, true);
1914
1995
  assertTransactionOperations(plan, tokens);
1915
- const managed = this.heldInputs(createArgs);
1996
+ const managed = this.bound.get(action.reference) ?? [];
1916
1997
  try {
1917
- return await this.signHeld(args, {
1998
+ return await this.signBound(args, {
1918
1999
  plan,
1919
2000
  createArgs,
1920
2001
  tx,
1921
2002
  tokens
1922
2003
  }, managed, originator);
1923
2004
  } finally {
1924
- for (const input of managed) this.held.delete(input.outpoint);
2005
+ this.bound.delete(action.reference);
1925
2006
  }
1926
2007
  }
1927
- /** The kept records of the module's inputs among the action's inputs. */
1928
- heldInputs(createArgs) {
1929
- return (createArgs.inputs ?? []).flatMap((input) => {
1930
- const managed = this.held.get(input.outpoint);
1931
- return managed === void 0 ? [] : [managed];
1932
- });
1933
- }
1934
2008
  /** Rechecks, approves and signs the module's inputs of a funded action. */
1935
- async signHeld(args, funded, managed, originator) {
2009
+ async signBound(args, funded, managed, originator) {
1936
2010
  const { plan, createArgs, tx, tokens } = funded;
1937
2011
  const scopes = sighashScopes(plan, new Set(managed.map((input) => input.outpoint)));
1938
2012
  await eachInOrder(new Set(tokens.consumed.map((input) => input.assetId)), async (assetId) => {
@@ -2202,6 +2276,7 @@ var ColorPermissionModule = class {
2202
2276
  basket,
2203
2277
  tags: filter.tags,
2204
2278
  tagQueryMode: filter.tagQueryMode,
2279
+ includeTags: filter.includeTags,
2205
2280
  include: "locking scripts",
2206
2281
  includeCustomInstructions: true,
2207
2282
  limit: LIST_PAGE_LIMIT,