Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions docs/packages/sdk/bsv-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id: bsv-sdk
title: '@bsv/sdk'
kind: package
domain: sdk
version: '2.8.2'
version: '2.8.3'
npm: '@bsv/sdk'
last_updated: '2026-09-23'
last_verified: '2026-09-23'
Expand All @@ -15,6 +15,25 @@ repo: 'https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk'

# @bsv/sdk

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,
matching JSON validation; count, length and individual output bounds stay intact.
Apps affected by these client defects can update the SDK without changing calls.
Wallets retain the existing BRC100 contract; an ecosystem-wide application
migration is not required. The candidate is not published until the protected
release workflow completes.

The action-history compatibility regression affects signed `listActions` values
in the 2.8.x binary processor and client. Updating a wallet repairs responses for
older compatible clients; an application already bundling the affected 2.8.x
binary client also needs the SDK patch. No call or persisted-data change is
needed. Synthetic cross-version checks cover SDK 2.4.0, 2.5.0, 2.7.1, 2.8.1 and
2.8.2 at eight signed/unsigned boundaries: all 40 candidate cases pass, all 32
previously valid cases retain exact response bytes, and the eight known negative
2.8.1/2.8.2 failures are recorded separately. These checks are bounded fixture
evidence, not a claim that every ecosystem application has been tested.

Version 2.8.1 repairs portable decryption of authenticated empty
AES-GCM plaintext. Browser/mobile and native Node envelopes now interoperate
for empty encrypted fields. Encryption bytes and the full authentication tag
Expand Down Expand Up @@ -395,14 +414,13 @@ is removed. Only a successful locally initiated handshake can select the
implicit destination for a later `Peer` call; inbound messages cannot retarget
it.

### Wallet discovery deadlines in the 2.8.2 candidate
### Wallet discovery deadlines since 2.8.2

Automatic React Native and XDM probes retain their short discovery deadlines
and remove listeners when unavailable. A successful probe creates a separate
operational connection so authentication, permission approval and valid slow
responses do not inherit the one-second/200-millisecond discovery deadline.
Explicitly configured substrate `responseTimeout` values remain enforced, and
response validation and origin checks are unchanged. No API or wire migration
is needed. Applications must update their bundled SDK; updating only a wallet
cannot replace a web application's SDK. Publication remains a separate
protected-workflow action.
is needed. Applications affected by the timeout defect can update their bundled
SDK; a wallet release alone cannot replace code served by a web application.
6 changes: 3 additions & 3 deletions docs/reference/package-api-migrations.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/reference/stack-facts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.2` | 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.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/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) |
Expand Down
6 changes: 3 additions & 3 deletions governance/package-release-notes.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,9 @@
{
"name": "@bsv/sdk",
"publishedVersion": "2.8.2",
"releaseType": "none",
"summary": "Separates automatic React Native and XDM wallet discovery deadlines from subsequent wallet operations. Successful probes now create an operational substrate without the probe-only timeout, so user approval and valid slow responses can complete. Unresponsive discovery remains bounded with listener cleanup, explicitly configured operation timeouts remain enforced, and all response validation and origin checks are unchanged. SDK 2.8.2 was published by protected release 35912232040 from reviewed main cc1256c4d81b0f64de89a0df7a5af6efbe3b3983; registry bytes and provenance match the verified candidate.",
"migration": "No API or wire migration. Upgrade applications that use WalletClient auto-discovery to 2.8.2; earlier auto-discovered React Native/XDM connections can time out later calls at the one-second/200-millisecond discovery deadline. Explicit substrate responseTimeout values still apply. Wallet implementation upgrades alone do not update the SDK bundled by a web application."
"releaseType": "patch",
"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. Source 2.8.3 remains a candidate until the protected release workflow publishes it."
},
{
"name": "@bsv/simple",
Expand Down
2 changes: 1 addition & 1 deletion governance/repository-health/baselines.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.2",
"@bsv/sdk": "2.8.3",
"@bsv/verifast": "0.3.6",
"@bsv/btms": "1.2.3",
"@bsv/btms-permission-module": "1.2.1",
Expand Down
15 changes: 15 additions & 0 deletions packages/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,21 @@ All notable changes to this project will be documented in this file. The format

## [Unreleased]

### 2.8.3 candidate — BRC100 discovery and action-history compatibility

- Pass the configured caller originator through automatic discovery probes so
HTTP WalletWire keeps its existing caller binding without rejecting discovery.
- Bind the default HTTP JSON fetch receiver for native browser implementations;
preserve supplied HTTP clients, discovery ordering, deadlines and validation.
- Restore signed `listActions` net amounts with the existing int64 wire encoding,
matching established SDK behavior and JSON validation. Reject out-of-range,
unsafe, noncanonical and truncated values without relaxing counts, lengths or
individual output values.
- No BRC100 API, wire or account-data migration. Applications affected by these
client defects can update the SDK without changing their calls; wallet releases
do not require every application to upgrade in coordination.
- Publication remains a separate protected release workflow.

### 2.8.2 candidate — wallet discovery timeout lifecycle

- Keep React Native and XDM discovery bounded without carrying the short probe
Expand Down
17 changes: 12 additions & 5 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,21 @@ SDK 2.8.1 fixes portable AES-GCM decryption of authenticated empty plaintext.
Encryption bytes and full 16-byte tag verification are unchanged; invalid tags,
keys and IVs remain rejected.

The 2.8.2 candidate separates wallet discovery timeouts from normal operations.
SDK 2.8.2 separates wallet discovery timeouts from normal operations.
Automatic React Native and XDM discovery remains bounded, while subsequent
calls can wait for user approval without inheriting the one-second/200-millisecond
probe deadline. Explicit substrate `responseTimeout` values remain enforced.
Applications using `WalletClient` auto-discovery must upgrade their bundled SDK;
updating the wallet alone does not update a web application's SDK. No API, wire
or account-data migration is required. This source candidate is not published
until the protected npm release workflow completes.

The 2.8.3 candidate preserves the configured BRC100 originator in automatic
HTTP WalletWire discovery and binds the browser's default JSON `fetch` receiver.
It also restores negative `listActions` net amounts using their historical signed
int64 wire bytes, matching existing JSON validation. Counts, lengths and individual
output values remain unsigned and bounded.
Applications affected by these client defects can update their bundled SDK
without changing calls. Wallet upgrades continue to support the existing BRC100
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.

## Table of Contents

Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bsv/sdk",
"version": "2.8.2",
"version": "2.8.3",
"sideEffects": false,
"engines": {
"node": ">=22"
Expand Down
24 changes: 10 additions & 14 deletions packages/sdk/src/wallet/WalletClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,26 +125,22 @@ export default class WalletClient implements WalletInterface {

const attemptSubstrate = async (
factory: () => WalletInterface,
timeout?: number,
timeout: number,
connectedFactory?: () => WalletInterface
): Promise<{ success: boolean; sub?: WalletInterface }> => {
try {
const sub = factory()
let result
let timeoutHandle: ReturnType<typeof setTimeout> | undefined
if (typeof timeout === 'number') {
try {
result = await Promise.race([
sub.getVersion({}),
new Promise<never>((_resolve, reject) => {
timeoutHandle = setTimeout(() => reject(new Error('Timed out.')), timeout)
})
])
} finally {
if (timeoutHandle !== undefined) clearTimeout(timeoutHandle)
}
} else {
result = await sub.getVersion({})
try {
result = await Promise.race([
sub.getVersion({}, this.originator),
new Promise<never>((_resolve, reject) => {
timeoutHandle = setTimeout(() => reject(new Error('Timed out.')), timeout)
})
])
} finally {
clearTimeout(timeoutHandle)
}
validateWalletResult('getVersion', result)
// Probe deadlines bound discovery, not later calls that may await user approval.
Expand Down
18 changes: 18 additions & 0 deletions packages/sdk/src/wallet/__tests/WalletClient.discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,24 @@ describe('WalletClient discovery timeout lifecycle', () => {
expect(jest.getTimerCount()).toBe(0)
})

it('falls back cleanly when a CWI probe throws synchronously', async () => {
const { listeners } = walletHost('react-native')
Object.defineProperty(window, 'CWI', {
value: {
getVersion: () => {
throw new Error('Provider closed during discovery')
}
}
})
const client = new WalletClient('auto', 'app.example')
const connected = client.connectToSubstrate()
await jest.advanceTimersByTimeAsync(10)
await connected
expect(client.substrate).toBeInstanceOf(ReactNativeWebView)
expect(listeners.size).toBe(0)
expect(jest.getTimerCount()).toBe(0)
})

it.each<Transport>(['react-native', 'xdm'])(
'does not impose the %s discovery deadline on subsequent user approval',
async transport => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createServer } from 'node:http'
import type { AddressInfo } from 'node:net'
import WalletClient from '../WalletClient'
import WalletWireTransceiver from '../substrates/WalletWireTransceiver'
import HTTPWalletJSON from '../substrates/HTTPWalletJSON'

describe('WalletClient automatic HTTP discovery', () => {
const originalFetch = globalThis.fetch

afterEach(() => jest.restoreAllMocks())

it.each([
['wire', 'http://localhost:3301'],
['json', 'http://localhost:3321'],
['secure-json', 'https://localhost:2121']
])(
'preserves an explicit caller through %s discovery and later calls',
async (kind, endpoint) => {
const requests: Array<{ path: string; origin: string | undefined }> = []
const server = createServer((request, response) => {
requests.push({ path: request.url!, origin: request.headers.origin })
request.resume()
response.setHeader(
'Content-Type',
kind === 'wire' ? 'application/octet-stream' : 'application/json'
)
response.end(
kind === 'wire'
? Buffer.concat([Buffer.from([0]), Buffer.from('1.0.0.0')])
: JSON.stringify({ version: '1.0.0.0' })
)
})
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
const address = server.address() as AddressInfo
// Use real HTTP without occupying a developer's installed wallet ports.
jest.spyOn(globalThis, 'fetch').mockImplementation(async function (
this: unknown,
input,
init
) {
// Browser fetch enforces its receiver even though Node's fetch does not.
if (this !== globalThis) throw new TypeError('Illegal invocation')
const url = new URL(String(input))
if (url.origin !== endpoint) throw new Error('This wallet transport is unavailable')
return await originalFetch(`http://127.0.0.1:${address.port}${url.pathname}`, init)
})
try {
const client = new WalletClient('auto', 'app.example')
await expect(client.getVersion()).resolves.toEqual({ version: '1.0.0.0' })
expect(client.substrate).toBeInstanceOf(
kind === 'wire' ? WalletWireTransceiver : HTTPWalletJSON
)
await expect(client.getVersion()).resolves.toEqual({ version: '1.0.0.0' })
expect(requests).toEqual(
Array.from({ length: 3 }, () => ({
path: '/getVersion',
origin: kind === 'wire' ? 'app.example' : 'http://app.example'
}))
)
} finally {
server.closeAllConnections()
await new Promise<void>((resolve, reject) =>
server.close(error => (error ? reject(error) : resolve()))
)
}
}
)
})
2 changes: 1 addition & 1 deletion packages/sdk/src/wallet/substrates/HTTPWalletJSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export default class HTTPWalletJSON implements WalletInterface {
constructor(
originator: OriginatorDomainNameStringUnder250Bytes | undefined,
baseUrl: string = 'http://localhost:3321',
httpClient = fetch
httpClient = globalThis.fetch.bind(globalThis)
) {
this.baseUrl = normalizeWalletHttpBaseUrl(baseUrl)
this.originator = validateOriginator(originator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -902,7 +902,7 @@ export default class WalletWireProcessor implements WalletWire {

// satoshis
resultWriter.writeVarIntNum(
this.#requireInteger(action.satoshis, 'listActions satoshis', 0, 21e14)
this.#requireInteger(action.satoshis, 'listActions satoshis', -21e14, 21e14)
)

// status
Expand Down
25 changes: 19 additions & 6 deletions packages/sdk/src/wallet/substrates/WalletWireTransceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,14 +398,27 @@ export default class WalletWireTransceiver implements WalletInterface {
return status
}

#readActionSatoshis(reader: ReaderUint8Array): number {
// Action history is a signed net amount. Legacy wallet wire encodes negative
// values as 0xff followed by signed little-endian int64. This exception is
// field-specific: counts, lengths and individual output values stay unsigned.
let value: number
if (
reader.remaining() >= 9 &&
reader.bin[reader.pos] === 0xff &&
reader.bin[reader.pos + 8] >= 0x80
) {
reader.skip(1)
value = reader.readInt64LEBn().toNumber()
} else {
value = reader.readVarIntNumStrict(false)
}
return this.#requireInteger(value, 'listActions satoshis', -21e14, 21e14)
}

#parseAction(reader: ReaderUint8Array): ListActionsResult['actions'][number] {
const txid = toHex(reader.read(32))
const satoshis = this.#requireInteger(
reader.readVarIntNumStrict(false),
'listActions satoshis',
0,
21e14
)
const satoshis = this.#readActionSatoshis(reader)
const status = this.#parseActionStatus(reader.readInt8())
const isOutgoing = this.#readBooleanFlag(reader, 'listActions isOutgoing')
const description = toUTF8Strict(reader.read(reader.readVarIntNumStrict(false)))
Expand Down
Loading
Loading