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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,21 @@ the manager's `getVersion` string.
57
57
  `p color plan` labels, built with `ColorPlan.toLabels`, and
58
58
  `options.randomizeOutputs: false` (every plan requires it: the plan's output
59
59
  indices and the approved facts name the requested outputs). The module selects
60
- admitted token inputs and adds token change in `createAction`. In
60
+ admitted token inputs and adds token change in `createAction`, and supplies
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
61
75
  `signAction` it checks the funded transaction against the plan, asks the user
62
76
  to approve it (`color_send`: every asset leg, satoshi totals and fee, the
63
77
  signatures it releases), signs the token inputs with any `sighashTypes`
@@ -119,8 +133,10 @@ The module fails explicitly, rather than falling back, on:
119
133
 
120
134
  Explicit inputs are supported: this wallet's `p color` outputs are signed by the
121
135
  module, and foreign token inputs count only once their asset's overlay admits
122
- them. An input missing from `inputBEEF` is resolved by the wallet and checked
123
- in `signAction`. `inputPositions` and token `changePositions` are honored; change
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
124
140
  positions follow the requested outputs.
125
141
 
126
142
  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, 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, 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;
@@ -194,6 +194,8 @@ interface ManagedInput extends SourceOutput {
194
194
  role: TokenRole;
195
195
  amount: bigint;
196
196
  custody: Custody;
197
+ /** The source transaction as BEEF, when the module made it (a handoff split). */
198
+ sourceBEEF?: BEEF;
197
199
  }
198
200
  type InventoryState = 'available' | 'reserved';
199
201
  /** One asset's token units and authority outputs on one side of a transaction. */
@@ -347,10 +349,15 @@ export declare class ColorPermissionModule implements PermissionsModule {
347
349
  readonly capabilities: Readonly<Record<string, string>>;
348
350
  /** Outputs a send in progress has selected, so a concurrent send does not select them too. */
349
351
  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>;
350
357
  protected readonly supportedAssets: Map<string, Promise<void>>;
351
358
  protected identityKey?: Promise<string>;
352
359
  constructor(options: ColorPermissionModuleOptions);
353
- /** Clears in-memory selections and asset checks. */
360
+ /** Clears in-memory selections, kept input records and asset checks. */
354
361
  dispose(): void;
355
362
  /** Protocol- and basket-routed calls: key and signature operations and the read views. */
356
363
  handleRequest(req: PermissionsModuleRequest, next: PermissionsModuleNext): Promise<unknown>;
@@ -461,6 +468,17 @@ export declare class ColorPermissionModule implements PermissionsModule {
461
468
  * does not report, as BTMS does when it selects, and returns the outputs it then admits.
462
469
  */
463
470
  protected resubmitHeld(assetId: string, role: TokenRole, outputs: readonly SourceOutput[], originator: string): Promise<Set<string>>;
471
+ /**
472
+ * `callerBEEF` with the source transactions of the module's selected inputs merged in, each with
473
+ * its proof or unproven ancestors and nothing else. Inputs the caller named keep the BEEF the
474
+ * caller gave; the module adds none for them.
475
+ */
476
+ protected inputBeef(callerBEEF: BEEF | undefined, inputs: readonly ManagedInput[], originator: string): Promise<BEEF | undefined>;
477
+ /**
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.
480
+ */
481
+ protected sourceTransactions(basket: string, inputs: readonly ManagedInput[], originator: string): Promise<Beef>;
464
482
  /** One basket's token outputs of the asset, read from their locking scripts, excluding selected ones. */
465
483
  protected candidates(assetId: string, role: TokenRole, originator: string): Promise<Candidate[]>;
466
484
  /** A chosen candidate with its custody, or undefined when its custody record is unreadable. */
@@ -488,8 +506,15 @@ export declare class ColorPermissionModule implements PermissionsModule {
488
506
  * `p color` outputs.
489
507
  */
490
508
  protected signAction(args: SignActionArgs, action: PermissionsModuleAction | undefined, originator: string): Promise<SignActionArgs>;
491
- /** The transaction's inputs that spend this wallet's `p color` outputs, with their stored custody. */
492
- protected actionInputs(tx: Transaction, originator: string): Promise<ManagedInput[]>;
509
+ /** The kept records of the module's inputs among the action's inputs. */
510
+ protected heldInputs(createArgs: CreateActionArgs): ManagedInput[];
511
+ /** Rechecks, approves and signs the module's inputs of a funded action. */
512
+ protected signHeld(args: SignActionArgs, funded: {
513
+ plan: ColorPlan;
514
+ createArgs: CreateActionArgs;
515
+ tx: Transaction;
516
+ tokens: TransactionTokens;
517
+ }, managed: ManagedInput[], originator: string): Promise<SignActionArgs>;
493
518
  /**
494
519
  * Submits a signed token transaction to the topic of every asset among its inputs and outputs,
495
520
  * and a deploy to the discovery topic.
package/dist/index.mjs CHANGED
@@ -698,6 +698,8 @@ const DEFAULT_SIGHASH = TransactionSignature.SIGHASH_ALL | TransactionSignature.
698
698
  const MAX_ENVELOPE_BYTES = 16777216;
699
699
  const MAX_RECEIPT_INSTRUCTION_BYTES = 1e3;
700
700
  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;
701
703
  const SUBMIT_ATTEMPTS = 3;
702
704
  const NOSEND_EXPIRY_LABEL_PREFIX = "p nosend expiry ";
703
705
  const STATE_TAG_PREFIX = "color_state_";
@@ -727,6 +729,20 @@ function custodyOf(remittance) {
727
729
  senderIdentityKey
728
730
  };
729
731
  }
732
+ /** Whether `beef` holds the transaction `txid` in full, not by txid alone. */
733
+ function holdsTransaction(beef, txid) {
734
+ const found = beef.findTxid(txid);
735
+ return found !== void 0 && !found.isTxidOnly;
736
+ }
737
+ /**
738
+ * Merges the source transaction of `outpoint` from `from` into `into` with only its own proof or
739
+ * unproven ancestors, so other transactions in `from` are left out.
740
+ */
741
+ function mergeSource(into, from, outpoint) {
742
+ const { txid } = parseOutpoint(outpoint);
743
+ if (!holdsTransaction(from, txid)) throw new Error(`BRC-207: the wallet returned no source transaction for token input ${outpoint}`);
744
+ into.mergeBeef(from.toBinaryAtomic(txid));
745
+ }
730
746
  function tokenInputs(inputs) {
731
747
  return inputs.map((input) => ({
732
748
  outpoint: input.outpoint,
@@ -1199,6 +1215,11 @@ var ColorPermissionModule = class {
1199
1215
  capabilities = { "brc207-color": "5" };
1200
1216
  /** Outputs a send in progress has selected, so a concurrent send does not select them too. */
1201
1217
  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();
1202
1223
  supportedAssets = /* @__PURE__ */ new Map();
1203
1224
  identityKey;
1204
1225
  constructor(options) {
@@ -1209,9 +1230,10 @@ var ColorPermissionModule = class {
1209
1230
  this.provider = options.provider;
1210
1231
  this.requestTokenAccess = options.requestTokenAccess;
1211
1232
  }
1212
- /** Clears in-memory selections and asset checks. */
1233
+ /** Clears in-memory selections, kept input records and asset checks. */
1213
1234
  dispose() {
1214
1235
  this.selected.clear();
1236
+ this.held.clear();
1215
1237
  this.supportedAssets.clear();
1216
1238
  this.identityKey = void 0;
1217
1239
  }
@@ -1368,10 +1390,13 @@ var ColorPermissionModule = class {
1368
1390
  this.reserve(reserved, explicit.managed);
1369
1391
  const { inputs, changes } = await this.fund(reserved, plan, planned, explicit, handoff, originator);
1370
1392
  sighashScopes(plan, new Set([...explicit.managed, ...inputs].map((input) => input.outpoint)));
1393
+ const inputBEEF = await this.inputBeef(args.inputBEEF, inputs, originator);
1394
+ for (const input of [...explicit.managed, ...inputs]) this.held.set(input.outpoint, input);
1371
1395
  return {
1372
1396
  ...args,
1373
1397
  inputs: orderInputs(explicit.inputs, tokenInputs(inputs), plan.inputPositions),
1374
- outputs: placeChange(outputs, changes, plan.changePositions)
1398
+ outputs: placeChange(outputs, changes, plan.changePositions),
1399
+ ...inputBEEF === void 0 ? {} : { inputBEEF }
1375
1400
  };
1376
1401
  } finally {
1377
1402
  for (const outpoint of reserved) this.selected.delete(outpoint);
@@ -1696,6 +1721,45 @@ var ColorPermissionModule = class {
1696
1721
  });
1697
1722
  return await this.spendable(assetId, outputs);
1698
1723
  }
1724
+ /**
1725
+ * `callerBEEF` with the source transactions of the module's selected inputs merged in, each with
1726
+ * its proof or unproven ancestors and nothing else. Inputs the caller named keep the BEEF the
1727
+ * caller gave; the module adds none for them.
1728
+ */
1729
+ async inputBeef(callerBEEF, inputs, originator) {
1730
+ if (inputs.length === 0) return callerBEEF;
1731
+ const beef = callerBEEF === void 0 ? new Beef() : parsed("inputBEEF", () => Beef.fromBinary(callerBEEF));
1732
+ const baskets = /* @__PURE__ */ new Map();
1733
+ for (const input of inputs) if (input.sourceBEEF === void 0) {
1734
+ const basket = assetBasket(input.assetId, input.role);
1735
+ 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);
1740
+ });
1741
+ return beef.toBinary();
1742
+ }
1743
+ /**
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.
1746
+ */
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;
1762
+ }
1699
1763
  /** One basket's token outputs of the asset, read from their locking scripts, excluding selected ones. */
1700
1764
  async candidates(assetId, role, originator) {
1701
1765
  return (await this.listBasket(assetBasket(assetId, role), originator, {})).flatMap((output) => {
@@ -1782,6 +1846,7 @@ var ColorPermissionModule = class {
1782
1846
  sighashTypes: []
1783
1847
  }),
1784
1848
  inputs: tokenInputs(selected),
1849
+ inputBEEF: await this.inputBeef(void 0, selected, originator),
1785
1850
  outputs,
1786
1851
  options: {
1787
1852
  randomizeOutputs: false,
@@ -1810,7 +1875,8 @@ var ColorPermissionModule = class {
1810
1875
  assetId,
1811
1876
  role: "value",
1812
1877
  amount,
1813
- custody: exact.custody
1878
+ custody: exact.custody,
1879
+ sourceBEEF: signed.tx
1814
1880
  };
1815
1881
  }
1816
1882
  /** A fresh wallet-owned value output in the asset's basket, keyed to this wallet. */
@@ -1846,7 +1912,28 @@ var ColorPermissionModule = class {
1846
1912
  const tx = action.transaction;
1847
1913
  const tokens = transactionTokens(tx, true);
1848
1914
  assertTransactionOperations(plan, tokens);
1849
- const managed = await this.actionInputs(tx, originator);
1915
+ const managed = this.heldInputs(createArgs);
1916
+ try {
1917
+ return await this.signHeld(args, {
1918
+ plan,
1919
+ createArgs,
1920
+ tx,
1921
+ tokens
1922
+ }, managed, originator);
1923
+ } finally {
1924
+ for (const input of managed) this.held.delete(input.outpoint);
1925
+ }
1926
+ }
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
+ /** Rechecks, approves and signs the module's inputs of a funded action. */
1935
+ async signHeld(args, funded, managed, originator) {
1936
+ const { plan, createArgs, tx, tokens } = funded;
1850
1937
  const scopes = sighashScopes(plan, new Set(managed.map((input) => input.outpoint)));
1851
1938
  await eachInOrder(new Set(tokens.consumed.map((input) => input.assetId)), async (assetId) => {
1852
1939
  const assetInputs = tokens.consumed.filter((input) => input.assetId === assetId);
@@ -1862,17 +1949,6 @@ var ColorPermissionModule = class {
1862
1949
  }
1863
1950
  };
1864
1951
  }
1865
- /** The transaction's inputs that spend this wallet's `p color` outputs, with their stored custody. */
1866
- async actionInputs(tx, originator) {
1867
- const outpoints = new Set(tx.inputs.map((input) => inputOutpoint(input)));
1868
- const listed = await this.wallet.listOutputs({
1869
- basket: sdk.specOpBasketPrefixOutputs,
1870
- tags: [COLOR_BASKET_NAMESPACE],
1871
- include: "locking scripts",
1872
- includeCustomInstructions: true
1873
- }, originator);
1874
- return (await Promise.all(listed.outputs.filter((output) => outpoints.has(output.outpoint)).map(async (output) => await this.managedInput(output, originator)))).filter((input) => input !== void 0);
1875
- }
1876
1952
  /**
1877
1953
  * Submits a signed token transaction to the topic of every asset among its inputs and outputs,
1878
1954
  * and a deploy to the discovery topic.