From cfd573e77e4d1cf35561dabdb77f293c493cf632 Mon Sep 17 00:00:00 2001 From: Ty J Everett Date: Wed, 23 Sep 2026 22:43:40 -0700 Subject: [PATCH] fix(sdk): preserve empty stored action-history metadata --- conformance/META.json | 4 +- conformance/PARITY_MATRIX.json | 12 +- conformance/runner/reports/report.json | 34 +++- conformance/runner/reports/results.xml | 12 +- conformance/runner/ts/dispatchers/wallet.ts | 19 +- .../vectors/wallet/brc100/listactions.json | 102 ++++++++++- docs/packages/sdk/bsv-sdk.md | 16 +- docs/reference/package-api-migrations.md | 6 +- docs/reference/stack-facts.md | 10 +- governance/package-release-notes.json | 6 +- governance/repository-health/baselines.json | 8 +- packages/sdk/CHANGELOG.md | 10 ++ packages/sdk/README.md | 12 ++ packages/sdk/package.json | 2 +- packages/sdk/src/wallet/Wallet.interfaces.ts | 4 + .../sdk/src/wallet/WalletResultValidation.ts | 14 +- .../ListActionsHistory.compatibility.test.ts | 167 ++++++++++++++++++ specs/sdk/brc-100-wallet.json | 6 +- 18 files changed, 403 insertions(+), 41 deletions(-) create mode 100644 packages/sdk/src/wallet/__tests/ListActionsHistory.compatibility.test.ts diff --git a/conformance/META.json b/conformance/META.json index 4e39d1b2b..06dbbfcb9 100644 --- a/conformance/META.json +++ b/conformance/META.json @@ -73,8 +73,8 @@ }, "stats": { "total_files": 77, - "total_vectors": 6699, - "last_updated": "2026-09-23" + "total_vectors": 6700, + "last_updated": "2026-09-24" }, "regression_index": { "beef-v2-txid-panic": "go-sdk#306", diff --git a/conformance/PARITY_MATRIX.json b/conformance/PARITY_MATRIX.json index 874955ba1..fd9a6648b 100644 --- a/conformance/PARITY_MATRIX.json +++ b/conformance/PARITY_MATRIX.json @@ -1,16 +1,16 @@ { "schema_version": "1.0", - "generated_at": "2026-09-23", + "generated_at": "2026-09-24", "source": "ts-stack conformance corpus", "description": "Machine-readable parity status for cross-language SDK implementations (Go, Rust, Python). Use this to track and drive conformance.", "summary": { "total_files": 77, - "total_vectors": 6699, + "total_vectors": 6700, "fully_required_files": 58, "files_with_intended": 17, "files_with_mixed_status": 15, "vectors_by_status": { - "required": 6495, + "required": 6496, "intended": 204, "skipped": 7 }, @@ -20,7 +20,7 @@ "historical_regression": 36, "partial_ts_behavioral_difference": 5116, "intended_parity_work": 24, - "wallet_stateful_harness_required": 190 + "wallet_stateful_harness_required": 191 } }, "files": [ @@ -793,10 +793,10 @@ { "path": "wallet/brc100/listactions.json", "id": "wallet.brc100.listactions", - "total_vectors": 16, + "total_vectors": 17, "file_level_parity": "required", "effective_status": "mixed", - "required_count": 15, + "required_count": 16, "intended_count": 1, "skipped_count": 0, "reason_category": "wallet_stateful_harness_required", diff --git a/conformance/runner/reports/report.json b/conformance/runner/reports/report.json index d06ec37cc..1e4686d38 100644 --- a/conformance/runner/reports/report.json +++ b/conformance/runner/reports/report.json @@ -1,6 +1,6 @@ { - "timestamp": "2026-09-22T04:21:47.930Z", - "totalVectors": 6694, + "timestamp": "2026-09-24T05:41:55.330Z", + "totalVectors": 6700, "totalFiles": 77, "parseErrors": 0, "suites": [ @@ -86,6 +86,31 @@ "name": "auth.brc31-handshake.16", "pass": true, "error": null + }, + { + "name": "auth.brc31-handshake.17", + "pass": true, + "error": null + }, + { + "name": "auth.brc31-handshake.18", + "pass": true, + "error": null + }, + { + "name": "auth.brc31-handshake.19", + "pass": true, + "error": null + }, + { + "name": "auth.brc31-handshake.20", + "pass": true, + "error": null + }, + { + "name": "auth.brc31-handshake.21", + "pass": true, + "error": null } ] }, @@ -31491,6 +31516,11 @@ "name": "wallet.brc100.listactions.16", "pass": true, "error": null + }, + { + "name": "wallet.brc100.listactions.17", + "pass": true, + "error": null } ] }, diff --git a/conformance/runner/reports/results.xml b/conformance/runner/reports/results.xml index 691c3e727..158b9a12d 100644 --- a/conformance/runner/reports/results.xml +++ b/conformance/runner/reports/results.xml @@ -1,6 +1,6 @@ - - + + @@ -17,6 +17,11 @@ + + + + + @@ -6343,7 +6348,7 @@ - + @@ -6360,6 +6365,7 @@ + diff --git a/conformance/runner/ts/dispatchers/wallet.ts b/conformance/runner/ts/dispatchers/wallet.ts index b252a9f9e..8b2747793 100644 --- a/conformance/runner/ts/dispatchers/wallet.ts +++ b/conformance/runner/ts/dispatchers/wallet.ts @@ -65,7 +65,12 @@ import { LookupQuestion, LookupResolver, PrivateKey, - ProtoWallet + ProtoWallet, + WalletClient, + WalletWireProcessor, + WalletWireTransceiver, + type ListActionsResult, + type ListActionsArgs } from '@bsv/sdk' // Wallet-toolbox imports via moduleNameMapper aliases (see jest.config.mjs). @@ -695,6 +700,18 @@ async function dispatchListActions( input: Record, expected: Record ): Promise { + if (input.walletResult !== undefined) { + const result = input.walletResult as ListActionsResult + const provider = await setupTestWallet() + provider.listActions = async () => result + const binary = new WalletWireTransceiver(new WalletWireProcessor(provider)) + for (const substrate of [provider, binary]) { + const client = new WalletClient(substrate, 'conformance.example') + await expect(client.listActions(input.args as ListActionsArgs)).resolves.toEqual(expected) + } + return + } + // Vector 14 expects 1 action (pre-populated state) — demoted. // All other vectors expect empty results from a fresh wallet. const rootHex = diff --git a/conformance/vectors/wallet/brc100/listactions.json b/conformance/vectors/wallet/brc100/listactions.json index 145fa26e4..f3df012c1 100644 --- a/conformance/vectors/wallet/brc100/listactions.json +++ b/conformance/vectors/wallet/brc100/listactions.json @@ -5,7 +5,7 @@ "brc": [ "BRC-100" ], - "version": "2.0.1", + "version": "2.0.2", "reference_impl": "@bsv/sdk@2.0.14 + wallet-toolbox", "parity_class": "required", "vectors": [ @@ -367,6 +367,106 @@ "listActions", "originator" ] + }, + { + "id": "wallet.brc100.listactions.17", + "description": "Established wallet history preserves empty stored descriptions and unassigned baskets through validated JSON and binary boundaries", + "input": { + "args": { + "labels": [ + "history" + ], + "includeLabels": true, + "includeInputs": true, + "includeInputSourceLockingScripts": true, + "includeInputUnlockingScripts": true, + "includeOutputs": true, + "includeOutputLockingScripts": true + }, + "walletResult": { + "totalActions": 1, + "actions": [ + { + "txid": "abababababababababababababababababababababababababababababababab", + "satoshis": -1, + "status": "completed", + "isOutgoing": true, + "description": "", + "labels": [ + "history" + ], + "version": 1, + "lockTime": 0, + "inputs": [ + { + "sourceOutpoint": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd.0", + "sourceSatoshis": 10, + "sourceLockingScript": "51", + "unlockingScript": "51", + "inputDescription": "", + "sequenceNumber": 4294967295 + } + ], + "outputs": [ + { + "satoshis": 9, + "lockingScript": "51", + "spendable": true, + "tags": [], + "outputIndex": 0, + "outputDescription": "", + "basket": "" + } + ] + } + ] + } + }, + "expected": { + "totalActions": 1, + "actions": [ + { + "txid": "abababababababababababababababababababababababababababababababab", + "satoshis": -1, + "status": "completed", + "isOutgoing": true, + "description": "", + "labels": [ + "history" + ], + "version": 1, + "lockTime": 0, + "inputs": [ + { + "sourceOutpoint": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd.0", + "sourceSatoshis": 10, + "sourceLockingScript": "51", + "unlockingScript": "51", + "inputDescription": "", + "sequenceNumber": 4294967295 + } + ], + "outputs": [ + { + "satoshis": 9, + "lockingScript": "51", + "spendable": true, + "tags": [], + "outputIndex": 0, + "outputDescription": "", + "basket": "" + } + ] + } + ] + }, + "tags": [ + "brc-100", + "listActions", + "history-compatibility", + "wallet-result", + "non-empty" + ] } ] } diff --git a/docs/packages/sdk/bsv-sdk.md b/docs/packages/sdk/bsv-sdk.md index 062f6cbb7..4a1e4e77f 100644 --- a/docs/packages/sdk/bsv-sdk.md +++ b/docs/packages/sdk/bsv-sdk.md @@ -3,10 +3,10 @@ id: bsv-sdk title: '@bsv/sdk' kind: package domain: sdk -version: '2.8.3' +version: '2.8.4' npm: '@bsv/sdk' -last_updated: '2026-09-23' -last_verified: '2026-09-23' +last_updated: '2026-09-24' +last_verified: '2026-09-24' review_cadence_days: 30 status: stable tags: ['sdk', 'crypto', 'transactions'] @@ -15,6 +15,16 @@ repo: 'https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk' # @bsv/sdk +The 2.8.4 candidate also restores `listActions` responses containing empty +stored descriptions or unassigned basket names, including ordinary generated +change. The original strings are preserved; wallets need not fabricate display +metadata or rewrite history. Nonempty description limits, UTF-8 byte ceilings, +required scripts and labels, signed net values, nonnegative outputs and all +new-action request validation remain unchanged. Direct, HTTP JSON and binary +clients share the same regression coverage and a portable conformance vector. +No API, wire or account-data migration is required; only clients already bundling +the affected hardened SDK need a dependency update for this defect. + The 2.8.3 candidate repairs HTTP wallet discovery with an explicit BRC100 originator and binds the default JSON transport fetch receiver for browsers. It restores signed `listActions` net amounts with the historical wire encoding, diff --git a/docs/reference/package-api-migrations.md b/docs/reference/package-api-migrations.md index e75fa44a9..864d99465 100644 --- a/docs/reference/package-api-migrations.md +++ b/docs/reference/package-api-migrations.md @@ -48,7 +48,7 @@ and clean-consumer tests remain the executable type authority. | `@bsv/overlay-topics` | `1.8.4` | `1.8.4` | none | [API and usage](../packages/overlays/overlay-topics.md) | Topic and lookup identifiers and valid canonical wire encodings remain unchanged. Audit historical rows for malformed amounts, noncanonical identifiers, incomplete ownership or admin evidence, and ambiguous outpoint linkage before replay or rebuild. Custom state and screening providers must return exact booleans, compare identity keys case-insensitively where documented, conserve exact safe-integer value, and honor bounded query and result contracts. | | `@bsv/paymail` | `2.4.9` | `2.4.10` | patch | [API and usage](../packages/messaging/paymail.md) | None. ESM consumers are unaffected. | | `@bsv/payment-express-middleware` | `2.1.7` | `2.1.7` | none | [API and usage](../packages/middleware/payment-express-middleware.md) | No wire or public API migration is required; legacy x-bsv-payment JSON behavior remains supported, and Express 4 and 5 applications use their own peer-provided Express installation. Wallet adapters must return accepted and optional isMerge as own data properties; inherited/accessor-backed verdicts now fail closed. Overinclusive Atomic BEEF receipts are normalized to the declared subject closure. Production replicas must share one durable atomic replay store, and operators must reconcile a replay-store failure after wallet acceptance before asking a payer to spend again. Distributors must retain THIRD_PARTY_NOTICES.md and LICENSES/ with the package. | -| `@bsv/sdk` | `2.8.3` | `2.8.3` | none | [API and usage](../packages/sdk/bsv-sdk.md) | No application API, wire, account-data or wallet protocol migration is required. Applications affected by explicit-originator HTTP discovery or the browser JSON fallback can update their bundled SDK to 2.8.3 without changing calls. Wallets and affected 2.8.x binary clients should include this patch to restore signed action history. Existing older clients retain their original calls and wire bytes. This repairs SDK client and wire compatibility defects; wallet upgrades must continue supporting existing applications and do not require an ecosystem-wide SDK migration. Version 2.8.3 was published by the protected SDK-only release workflow with verified registry integrity and provenance. | +| `@bsv/sdk` | `2.8.3` | `2.8.4` | patch | [API and usage](../packages/sdk/bsv-sdk.md) | No API, wire, wallet-data or ecosystem-wide application migration is required. Applications affected by history rejection in the hardened 2.8.x SDK can update to 2.8.4 without changing their BRC100 calls. Existing wallets may continue returning their original stored metadata, including empty strings; do not fabricate descriptions or rewrite historical records. Source 2.8.4 remains a candidate until the protected SDK-only npm workflow publishes and verifies it. | | `@bsv/simple` | `0.6.0` | `0.6.0` | none | [API and usage](../packages/helpers/simple.md) | Replace createServerWalletHandler() deployments with createServerWalletHandler({ authorize: async ({ action, headers }) => authenticatedSessionCanUseAction(headers, action) }). The callback must return literal true for each status, create, request, receive, balance, outputs, or reset action; omission now returns HTTP 403 for every action. Roll out the authentication layer and callback with the package, update anonymous probes or automation, and apply the same policy to every replica. Do not emulate the old public behavior with an unconditional authorize: () => true callback. Valid recipient derivations and authenticated Message Box peers remain supported; malformed, wrong-owner, or transaction-mutated flows now fail closed. New DID, CredentialSchema, and Certifier records use canonical 32-byte types. Current SDK wallet methods reject historical short types, so do not put migration aliases in wallet list, acquire, prove, or relinquish calls. Export affected records through the storage version that created them, authenticate them offline against the exact locally configured identifier, and reissue/import canonical replacements; no legacy certificate is rewritten automatically. Distributors must retain THIRD_PARTY_NOTICES.md and LICENSES/. The internal comparator consolidation requires no migration and is included in the existing unpublished 0.6.0 candidate. | | `@bsv/templates` | `1.10.2` | `1.10.3` | patch | [API and usage](../packages/helpers/templates.md) | None. CommonJS consumers that patched 1.10.2 locally can drop the patch after upgrading to 1.10.3; ESM consumers are unaffected. | | `@bsv/teranode-listener` | `1.1.6` | `1.1.6` | none | [API and usage](../packages/network/teranode-listener.md) | No API migration is required for valid consumers: raw callbacks remain the default and decoding is opt-in with decodeMessages: true. Configuration arrays and callbacks are snapshotted at construction, boolean controls must be literal booleans, and malformed or duplicate topics, addresses, keys, and unsupported properties now fail closed. usePrivateDHT: false now actually omits the DHT service. The published mainnet PNET value is transport compatibility data, not a publisher credential; decoded sender and payload fields remain untrusted and security-critical claims require independent validation. Distributors must retain THIRD_PARTY_NOTICES.md and LICENSES/ with the package. | @@ -359,8 +359,8 @@ CLI entry points: `{"lch":"./dist/cli.js"}`. - Package documentation: [docs/packages/sdk/bsv-sdk.md](../packages/sdk/bsv-sdk.md) - Source: [packages/sdk](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk) -- Release note: Preserves the configured BRC100 caller originator during automatic wallet discovery, so the HTTP WalletWire probe satisfies the existing transport caller binding. Binds the default HTTP JSON fetch receiver for browsers while retaining custom HTTP clients. Keeps transport preference, bounded discovery, operation timeout behavior, validation and origin checks unchanged. Restores signed listActions net amounts using their historical int64 wire encoding, matching existing JSON validation, while retaining canonical unsigned counts/lengths and nonnegative individual output values. -- Migration: No application API, wire, account-data or wallet protocol migration is required. Applications affected by explicit-originator HTTP discovery or the browser JSON fallback can update their bundled SDK to 2.8.3 without changing calls. Wallets and affected 2.8.x binary clients should include this patch to restore signed action history. Existing older clients retain their original calls and wire bytes. This repairs SDK client and wire compatibility defects; wallet upgrades must continue supporting existing applications and do not require an ecosystem-wide SDK migration. Version 2.8.3 was published by the protected SDK-only release workflow with verified registry integrity and provenance. +- Release note: Restores BRC100 listActions compatibility with established wallet histories whose generated change, imported actions or unassigned outputs have empty display descriptions or basket names. Keeps nonempty description bounds, UTF-8 byte ceilings, own-data validation, requested scripts and labels, signed action amounts, nonnegative individual values, and creation-request validation unchanged. +- Migration: No API, wire, wallet-data or ecosystem-wide application migration is required. Applications affected by history rejection in the hardened 2.8.x SDK can update to 2.8.4 without changing their BRC100 calls. Existing wallets may continue returning their original stored metadata, including empty strings; do not fabricate descriptions or rewrite historical records. Source 2.8.4 remains a candidate until the protected SDK-only npm workflow publishes and verifies it. | Public subpath | Runtime target(s) | Declaration target(s) | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | diff --git a/docs/reference/stack-facts.md b/docs/reference/stack-facts.md index 035968df6..ddd009c9e 100644 --- a/docs/reference/stack-facts.md +++ b/docs/reference/stack-facts.md @@ -62,7 +62,7 @@ authorized release action. | overlays | `@bsv/overlay-discovery-services` | `2.2.5` | node-library | node-cjs, node-esm | node | `>=22` | [packages/overlays/overlay-discovery-services](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/overlay-discovery-services) | | overlays | `@bsv/overlay-express` | `2.7.2` | node-library | node-cjs, node-esm | node | `>=22` | [packages/overlays/overlay-express](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/overlay-express) | | overlays | `@bsv/overlay-topics` | `1.8.4` | node-library | node-esm | node | `>=22` | [packages/overlays/topics](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/topics) | -| sdk | `@bsv/sdk` | `2.8.3` | browser-library | browser-bundler, browser-esm, node-cjs, node-esm, umd-global | browser, node, umd | `>=22` | [packages/sdk](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk) | +| sdk | `@bsv/sdk` | `2.8.4` | browser-library | browser-bundler, browser-esm, node-cjs, node-esm, umd-global | browser, node, umd | `>=22` | [packages/sdk](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk) | | sdk | `@bsv/verifast` | `0.3.6` | wasm-library | browser-bundler, browser-esm, node-cjs, node-esm, umd-global, wasm-worker | browser, node, umd, wasm, worker | `>=22` | [packages/verifast](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/verifast) | | wallet | `@bsv/btms` | `1.2.3` | node-library | node-cjs, node-esm | node | `>=22` | [packages/wallet/btms](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/btms) | | wallet | `@bsv/btms-permission-module` | `1.2.1` | node-library | node-esm | node | `>=22` | [packages/wallet/btms-permission-module](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/btms-permission-module) | @@ -107,13 +107,13 @@ recorded container release route; they are not published by the public-package j | Metric | Current value | | --- | --- | | Vector files | 77 | -| Vectors | 6699 | -| Structurally passed | 6488 | +| Vectors | 6700 | +| Structurally passed | 6489 | | Governed skips | 211 | -| Required parity vectors | 6495 | +| Required parity vectors | 6496 | | Intended parity vectors | 204 | | Explicitly skipped vector entries | 7 | -| Corpus metadata revision | 2026-09-23 | +| Corpus metadata revision | 2026-09-24 | Structural runner pass/skip results and parity classifications answer different questions: the former is the current runner outcome, while the latter records cross-language diff --git a/governance/package-release-notes.json b/governance/package-release-notes.json index 3aaeb3f63..361e8b423 100644 --- a/governance/package-release-notes.json +++ b/governance/package-release-notes.json @@ -167,9 +167,9 @@ { "name": "@bsv/sdk", "publishedVersion": "2.8.3", - "releaseType": "none", - "summary": "Preserves the configured BRC100 caller originator during automatic wallet discovery, so the HTTP WalletWire probe satisfies the existing transport caller binding. Binds the default HTTP JSON fetch receiver for browsers while retaining custom HTTP clients. Keeps transport preference, bounded discovery, operation timeout behavior, validation and origin checks unchanged. Restores signed listActions net amounts using their historical int64 wire encoding, matching existing JSON validation, while retaining canonical unsigned counts/lengths and nonnegative individual output values.", - "migration": "No application API, wire, account-data or wallet protocol migration is required. Applications affected by explicit-originator HTTP discovery or the browser JSON fallback can update their bundled SDK to 2.8.3 without changing calls. Wallets and affected 2.8.x binary clients should include this patch to restore signed action history. Existing older clients retain their original calls and wire bytes. This repairs SDK client and wire compatibility defects; wallet upgrades must continue supporting existing applications and do not require an ecosystem-wide SDK migration. Version 2.8.3 was published by the protected SDK-only release workflow with verified registry integrity and provenance." + "releaseType": "patch", + "summary": "Restores BRC100 listActions compatibility with established wallet histories whose generated change, imported actions or unassigned outputs have empty display descriptions or basket names. Keeps nonempty description bounds, UTF-8 byte ceilings, own-data validation, requested scripts and labels, signed action amounts, nonnegative individual values, and creation-request validation unchanged.", + "migration": "No API, wire, wallet-data or ecosystem-wide application migration is required. Applications affected by history rejection in the hardened 2.8.x SDK can update to 2.8.4 without changing their BRC100 calls. Existing wallets may continue returning their original stored metadata, including empty strings; do not fabricate descriptions or rewrite historical records. Source 2.8.4 remains a candidate until the protected SDK-only npm workflow publishes and verifies it." }, { "name": "@bsv/simple", diff --git a/governance/repository-health/baselines.json b/governance/repository-health/baselines.json index d5c86c571..5b8e7c150 100644 --- a/governance/repository-health/baselines.json +++ b/governance/repository-health/baselines.json @@ -14,11 +14,11 @@ "run": "https://github.com/BSV-blockchain/ts-stack/actions/runs/30144812565" }, "conformance": { - "passed": 6488, + "passed": 6489, "skipped": 211, - "total": 6699, + "total": 6700, "vectorFiles": 77, - "run": "Local: pnpm --filter @bsv/conformance-runner-ts test (2026-09-23); 6488 vector cases passed plus 2 metadata/wire tests, 211 unchanged governed skips; adds 5 required BRC-104 response byte vectors" + "run": "Local: pnpm --filter @bsv/conformance-runner-ts test (2026-09-24); 6489 vector cases plus 2 metadata/wire tests, 211 unchanged governed skips; adds required empty historical BRC100 display metadata interoperability vector" }, "testExceptions": { "explicitSkipDeclarations": 60, @@ -322,7 +322,7 @@ "@bsv/overlay-discovery-services": "2.2.5", "@bsv/overlay-express": "2.7.2", "@bsv/overlay-topics": "1.8.4", - "@bsv/sdk": "2.8.3", + "@bsv/sdk": "2.8.4", "@bsv/verifast": "0.3.6", "@bsv/btms": "1.2.3", "@bsv/btms-permission-module": "1.2.1", diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 87d39bfa6..cf2173d30 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -214,6 +214,16 @@ All notable changes to this project will be documented in this file. The format ## [Unreleased] +### 2.8.4 candidate — established action-history display metadata + +- Accept empty historical action, input and output descriptions, and empty + unassigned basket names, without changing stored values or BRC100 wire bytes. +- Keep nonempty description bounds, UTF-8 ceilings, request validation, + requested scripts and labels, and transaction/value checks unchanged. +- Add direct, HTTP JSON, binary and shared conformance regression coverage. +- No application API or account-data migration is required. Publication remains + a separate protected workflow action. + ### 2.8.3 candidate — BRC100 discovery and action-history compatibility - Pass the configured caller originator through automatic discovery probes so diff --git a/packages/sdk/README.md b/packages/sdk/README.md index e6f19988b..13e72755b 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -65,6 +65,18 @@ contract; an ecosystem-wide application migration is not required. No API, wire or account-data migration is required. Source 2.8.3 is not published until the protected npm release workflow completes. +### Action history compatibility + +The 2.8.4 candidate accepts empty stored `listActions` descriptions and empty +basket names, as returned by established wallets for generated change and +unassigned outputs. It preserves those values rather than inventing metadata. +Nonempty descriptions retain their 5–2,000 UTF-8 byte bounds, and basket names +retain their 300-byte ceiling. Required scripts, label binding, transaction +identifiers, monetary values and new-action request validation are unchanged. +Affected SDK clients can update without changing their BRC100 calls; wallet +upgrades do not require an ecosystem-wide application migration. No wire or +account-data migration is required. Publication uses the protected npm workflow. + ## Table of Contents 1. [Objective](#objective) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index e8fb50931..335e9cf50 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@bsv/sdk", - "version": "2.8.3", + "version": "2.8.4", "sideEffects": false, "engines": { "node": ">=22" diff --git a/packages/sdk/src/wallet/Wallet.interfaces.ts b/packages/sdk/src/wallet/Wallet.interfaces.ts index 74e2e3313..627391580 100644 --- a/packages/sdk/src/wallet/Wallet.interfaces.ts +++ b/packages/sdk/src/wallet/Wallet.interfaces.ts @@ -501,6 +501,7 @@ export interface WalletActionInput { sourceSatoshis: SatoshiValue sourceLockingScript?: HexString unlockingScript?: HexString + /** Stored history may use an empty string when no display description was recorded. */ inputDescription: DescriptionString5to50Bytes sequenceNumber: PositiveIntegerOrZero } @@ -515,8 +516,10 @@ export interface WalletActionOutput { tags: OutputTagStringUnder300Bytes[] outputIndex: PositiveIntegerOrZero + /** Stored history may use an empty string when no display description was recorded. */ outputDescription: DescriptionString5to50Bytes + /** Empty when the historical output was not assigned to a basket. */ basket: BasketStringUnder300Bytes } @@ -539,6 +542,7 @@ export interface WalletAction { satoshis: SatoshiValue status: ActionStatus isOutgoing: boolean + /** Stored history may use an empty string when no display description was recorded. */ description: DescriptionString5to50Bytes labels?: LabelStringUnder300Bytes[] version: PositiveIntegerOrZero diff --git a/packages/sdk/src/wallet/WalletResultValidation.ts b/packages/sdk/src/wallet/WalletResultValidation.ts index b45cbbf43..ba5193153 100644 --- a/packages/sdk/src/wallet/WalletResultValidation.ts +++ b/packages/sdk/src/wallet/WalletResultValidation.ts @@ -1748,6 +1748,12 @@ function validateListResult( return entries } +// Stored history may lack display metadata, particularly wallet-generated change. +// Keep creation-request requirements and nonempty result bounds unchanged. +function historyDescription(value: unknown, call: string, field: string): void { + string(value, call, field, value === '' ? 0 : 5, 2000) +} + function validateListActions(result: UnknownRecord, call: string, request?: unknown): void { const actions = validateListResult(result, call, 'totalActions', 'actions', request) const requestArgs = requestRecord(request) @@ -1770,7 +1776,7 @@ function validateListActions(result: UnknownRecord, call: string, request?: unkn if (typeof action.isOutgoing !== 'boolean') { invalid(call, `${field}.isOutgoing`, 'a boolean') } - string(action.description, call, `${field}.description`, 5, 2000) + historyDescription(action.description, call, `${field}.description`) stringArray(action.labels, call, `${field}.labels`, 1, 300) if (requestArgs?.includeLabels === true && action.labels === undefined) { invalid(call, `${field}.labels`, 'the explicitly requested labels') @@ -1838,7 +1844,7 @@ function validateListActions(result: UnknownRecord, call: string, request?: unkn 'the explicitly requested unlocking script' ) } - string(input.inputDescription, call, `${inputField}.inputDescription`, 5, 2000) + historyDescription(input.inputDescription, call, `${inputField}.inputDescription`) uint(input.sequenceNumber, call, `${inputField}.sequenceNumber`) } } @@ -1874,8 +1880,8 @@ function validateListActions(result: UnknownRecord, call: string, request?: unkn } stringArray(output.tags, call, `${outputField}.tags`, 1, 300) uint(output.outputIndex, call, `${outputField}.outputIndex`) - string(output.outputDescription, call, `${outputField}.outputDescription`, 5, 2000) - string(output.basket, call, `${outputField}.basket`, 1, 300) + historyDescription(output.outputDescription, call, `${outputField}.outputDescription`) + string(output.basket, call, `${outputField}.basket`, 0, 300) } } } diff --git a/packages/sdk/src/wallet/__tests/ListActionsHistory.compatibility.test.ts b/packages/sdk/src/wallet/__tests/ListActionsHistory.compatibility.test.ts new file mode 100644 index 000000000..eacb0efbe --- /dev/null +++ b/packages/sdk/src/wallet/__tests/ListActionsHistory.compatibility.test.ts @@ -0,0 +1,167 @@ +import WalletClient from '../WalletClient.js' +import HTTPWalletJSON from '../substrates/HTTPWalletJSON.js' +import WalletWireProcessor from '../substrates/WalletWireProcessor.js' +import WalletWireTransceiver from '../substrates/WalletWireTransceiver.js' +import { validateWalletResult } from '../WalletResultValidation.js' +import { validateWalletArgs } from '../WalletArgumentValidation.js' +import type { ListActionsArgs, ListActionsResult, WalletInterface } from '../Wallet.interfaces.js' + +const args: ListActionsArgs = { + labels: ['history'], + includeLabels: true, + includeInputs: true, + includeInputSourceLockingScripts: true, + includeInputUnlockingScripts: true, + includeOutputs: true, + includeOutputLockingScripts: true +} + +function history(): ListActionsResult { + // Knex, IndexedDB and native SQLite histories use empty strings when stored + // display metadata is absent, including ordinary wallet-generated change. + return { + totalActions: 1, + actions: [ + { + txid: 'ab'.repeat(32), + satoshis: -1, + status: 'completed', + isOutgoing: true, + description: '', + labels: ['history'], + version: 1, + lockTime: 0, + inputs: [ + { + sourceOutpoint: `${'cd'.repeat(32)}.0`, + sourceSatoshis: 10, + sourceLockingScript: '51', + unlockingScript: '51', + inputDescription: '', + sequenceNumber: 0xffffffff + } + ], + outputs: [ + { + satoshis: 9, + lockingScript: '51', + spendable: true, + tags: [], + outputIndex: 0, + outputDescription: '', + basket: '' + } + ] + } + ] + } +} + +function descriptions(result: ListActionsResult): Array<[Record, string]> { + const action = result.actions[0] + return [ + [action as unknown as Record, 'description'], + [action.inputs![0] as unknown as Record, 'inputDescription'], + [action.outputs![0] as unknown as Record, 'outputDescription'] + ] +} + +describe('established BRC100 action-history metadata', () => { + it('preserves empty descriptions and unassigned baskets through direct, JSON and binary clients', async () => { + const result = history() + const backend = { + listActions: jest.fn().mockResolvedValue(result) + } as unknown as WalletInterface + const json = new HTTPWalletJSON( + 'app.example', + 'http://localhost:3321', + jest.fn().mockResolvedValue({ ok: true, status: 200, json: async () => result }) + ) + const binary = new WalletWireTransceiver(new WalletWireProcessor(backend)) + for (const substrate of [backend, json, binary]) { + await expect(new WalletClient(substrate, 'app.example').listActions(args)).resolves.toEqual( + result + ) + } + }) + + it.each(['', 'valid', 'é'.repeat(1000)])('preserves supported description bytes (%#)', value => { + const result = history() + for (const [record, field] of descriptions(result)) record[field] = value + expect(validateWalletResult('listActions', result, args)).toEqual(result) + }) + + it.each([undefined, null, 0, {}, 'four', 'x'.repeat(2001), 'é'.repeat(1001)])( + 'rejects missing, malformed, short nonempty and oversized descriptions (%#)', + value => { + for (let index = 0; index < 3; index++) { + const result = history() + const [record, field] = descriptions(result)[index] + record[field] = value + expect(() => validateWalletResult('listActions', result, args)).toThrow(field) + } + } + ) + + it('preserves the basket byte ceiling and requires an actual string', () => { + const result = history() + const output = result.actions[0].outputs![0] + for (const value of ['', 'default', 'é'.repeat(150)]) { + output.basket = value + expect(validateWalletResult('listActions', result, args)).toEqual(result) + } + for (const value of [undefined, null, 0, 'x'.repeat(301), 'é'.repeat(151)]) { + Object.assign(output, { basket: value }) + expect(() => validateWalletResult('listActions', result, args)).toThrow('basket') + } + }) + + it('still rejects missing requested scripts and invalid transaction values with empty metadata', () => { + for (const change of [ + (r: ListActionsResult) => { + r.actions[0].txid = 'invalid' + }, + (r: ListActionsResult) => { + r.actions[0].satoshis = Number.MAX_SAFE_INTEGER + }, + (r: ListActionsResult) => { + r.actions[0].outputs![0].satoshis = -1 + }, + (r: ListActionsResult) => { + delete r.actions[0].outputs![0].lockingScript + }, + (r: ListActionsResult) => { + delete r.actions[0].inputs![0].sourceLockingScript + }, + (r: ListActionsResult) => { + delete r.actions[0].inputs![0].unlockingScript + }, + (r: ListActionsResult) => { + r.actions[0].labels = ['unrelated'] + } + ]) { + const result = history() + change(result) + expect(() => validateWalletResult('listActions', result, args)).toThrow( + 'Invalid listActions result' + ) + } + }) + + it('keeps new action request descriptions and requested basket names nonempty', () => { + expect(() => validateWalletArgs('createAction', { description: '' })).toThrow() + expect(() => + validateWalletArgs('createAction', { + description: 'Valid action', + outputs: [{ lockingScript: '51', satoshis: 1, outputDescription: '' }] + }) + ).toThrow() + expect(() => + validateWalletArgs('createAction', { + description: 'Valid action', + inputs: [{ outpoint: `${'ab'.repeat(32)}.0`, inputDescription: '', unlockingScript: '51' }] + }) + ).toThrow() + expect(() => validateWalletArgs('listOutputs', { basket: '' })).toThrow() + }) +}) diff --git a/specs/sdk/brc-100-wallet.json b/specs/sdk/brc-100-wallet.json index e1ddbd7a1..c207aecdb 100644 --- a/specs/sdk/brc-100-wallet.json +++ b/specs/sdk/brc-100-wallet.json @@ -207,7 +207,7 @@ "sourceSatoshis": { "$ref": "#/$defs/SatoshiValue" }, "sourceLockingScript":{ "$ref": "#/$defs/HexString" }, "unlockingScript": { "$ref": "#/$defs/HexString" }, - "inputDescription": { "$ref": "#/$defs/DescriptionString5to50Bytes" }, + "inputDescription": { "anyOf": [{ "const": "" }, { "$ref": "#/$defs/DescriptionString5to50Bytes" }], "description": "Stored history can omit display metadata using an empty string; new action request requirements are unchanged." }, "sequenceNumber": { "$ref": "#/$defs/PositiveIntegerOrZero" } } }, @@ -222,7 +222,7 @@ "customInstructions":{ "type": "string" }, "tags": { "type": "array", "items": { "$ref": "#/$defs/OutputTagStringUnder300Bytes" } }, "outputIndex": { "$ref": "#/$defs/PositiveIntegerOrZero" }, - "outputDescription": { "$ref": "#/$defs/DescriptionString5to50Bytes" }, + "outputDescription": { "anyOf": [{ "const": "" }, { "$ref": "#/$defs/DescriptionString5to50Bytes" }], "description": "Stored history can omit display metadata using an empty string; new action request requirements are unchanged." }, "basket": { "$ref": "#/$defs/BasketStringUnder300Bytes" } } }, @@ -249,7 +249,7 @@ "satoshis": { "$ref": "#/$defs/SatoshiValue" }, "status": { "$ref": "#/$defs/ActionStatus" }, "isOutgoing": { "type": "boolean" }, - "description": { "$ref": "#/$defs/DescriptionString5to50Bytes" }, + "description": { "anyOf": [{ "const": "" }, { "$ref": "#/$defs/DescriptionString5to50Bytes" }], "description": "Stored history can omit display metadata using an empty string; new action request requirements are unchanged." }, "labels": { "type": "array", "items": { "$ref": "#/$defs/LabelStringUnder300Bytes" } }, "version": { "$ref": "#/$defs/PositiveIntegerOrZero" }, "lockTime": { "$ref": "#/$defs/PositiveIntegerOrZero" },