Skip to content

Commit 29d614b

Browse files
authored
Merge pull request #274 from ty-everett/codex/auto-change-policy
fix(wallet): enforce managed BRC-29 change policy
2 parents 68b796d + c8ab66a commit 29d614b

35 files changed

Lines changed: 804 additions & 138 deletions

packages/sdk/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,12 @@ All notable changes to this project will be documented in this file. The format
224224

225225
### Fixed
226226

227+
- Release prep for `2.1.7`: fix `Brc29RemittanceModule` settlement acceptance to always use the
228+
`wallet payment` internalization protocol. The deprecated
229+
`internalizeProtocol: 'basket insertion'` configuration now fails fast with
230+
recovery guidance instead of constructing an invalid remittance and
231+
misclassifying recipient funds as custom outputs.
232+
227233
### Security
228234

229235
---

packages/sdk/docs/reference/remittance.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,9 @@ description?: string
193193

194194
#### Property internalizeProtocol
195195

196-
How wallet internalizes the payment.
196+
Deprecated. BRC-29 settlements must be internalized as `wallet payment` so the
197+
recipient can verify and manage the derived output. The module rejects
198+
`basket insertion`; it is reserved for application-managed custom outputs.
197199

198200
```ts
199201
internalizeProtocol?: "wallet payment" | "basket insertion"

packages/sdk/docs/remittance-getting-started.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ await manager.init()
9898
await testSyncThreads(manager)
9999
```
100100

101+
`Brc29RemittanceModule` internalizes a received settlement as a `wallet payment`.
102+
That classification is required: the wallet verifies the BRC-29 derivation and
103+
records the output as spendable managed change in its default balance. Do not
104+
use `basket insertion` for BRC-29 settlement funds; basket insertion is reserved
105+
for application-managed custom outputs and cannot target the default basket.
106+
101107
## Event Hooks
102108

103109
Use `onEvent` or the per-event `events` callbacks to react to each step.

packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bsv/sdk",
3-
"version": "2.1.6",
3+
"version": "2.1.7",
44
"type": "module",
55
"description": "BSV Blockchain Software Development Kit",
66
"main": "dist/cjs/mod.js",

packages/sdk/src/remittance/__tests/BasicBRC29.additional.test.ts

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -570,22 +570,9 @@ describe('Brc29RemittanceModule – acceptSettlement validation', () => {
570570
)
571571
})
572572

573-
it('uses basket insertion internalizeProtocol when configured', async () => {
574-
const internalizeAction = jest.fn(async () => ({ accepted: true as const }))
575-
const wallet = makeWallet({ internalizeAction })
576-
const module = new Brc29RemittanceModule({ internalizeProtocol: 'basket insertion' })
577-
578-
const result = await module.acceptSettlement(
579-
{ threadId: 'tid', settlement: { ...validSettlement }, sender: 'sender-key' },
580-
makeContext(wallet)
581-
)
582-
expect(result.action).toBe('accept')
583-
expect(internalizeAction).toHaveBeenCalledWith(
584-
expect.objectContaining({
585-
outputs: [expect.objectContaining({ protocol: 'basket insertion' })]
586-
}),
587-
'example.com'
588-
)
573+
it('rejects the unsafe basket insertion internalizeProtocol', () => {
574+
expect(() => new Brc29RemittanceModule({ internalizeProtocol: 'basket insertion' }))
575+
.toThrow('BRC-29 settlements cannot be internalized as basket insertions')
589576
})
590577
})
591578

@@ -618,7 +605,7 @@ describe('Brc29RemittanceModule – constructor defaults', () => {
618605
outputDescription: 'out-desc',
619606
refundFeeSatoshis: 500,
620607
minRefundSatoshis: 200,
621-
internalizeProtocol: 'basket insertion',
608+
internalizeProtocol: 'wallet payment',
622609
nonceProvider: customNonce,
623610
lockingScriptProvider: customScript
624611
})
@@ -628,7 +615,7 @@ describe('Brc29RemittanceModule – constructor defaults', () => {
628615
expect((module as any).outputDescription).toBe('out-desc')
629616
expect((module as any).refundFeeSatoshis).toBe(500)
630617
expect((module as any).minRefundSatoshis).toBe(200)
631-
expect((module as any).internalizeProtocol).toBe('basket insertion')
618+
expect((module as any).internalizeProtocol).toBe('wallet payment')
632619
expect((module as any).nonceProvider).toBe(customNonce)
633620
expect((module as any).lockingScriptProvider).toBe(customScript)
634621
})

packages/sdk/src/remittance/modules/BasicBRC29.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,15 @@ export interface Brc29RemittanceModuleConfig {
112112
*/
113113
minRefundSatoshis?: number
114114

115-
/** How wallet internalizes the payment. */
115+
/**
116+
* @deprecated BRC-29 settlements must be internalized as `wallet payment`.
117+
* `basket insertion` never supplied the required insertion remittance and,
118+
* more importantly, would classify recipient funds as custom application
119+
* outputs that the wallet's automatic BRC-29 signer cannot spend.
120+
*
121+
* The property remains in the input type for source compatibility, but the
122+
* constructor rejects `basket insertion` with an actionable error.
123+
*/
116124
internalizeProtocol?: 'wallet payment' | 'basket insertion'
117125

118126
nonceProvider?: NonceProvider
@@ -138,7 +146,7 @@ export class Brc29RemittanceModule
138146
private readonly outputDescription: string
139147
private readonly refundFeeSatoshis: number
140148
private readonly minRefundSatoshis: number
141-
private readonly internalizeProtocol: 'wallet payment' | 'basket insertion'
149+
private readonly internalizeProtocol: 'wallet payment'
142150
private readonly nonceProvider: NonceProvider
143151
private readonly lockingScriptProvider: LockingScriptProvider
144152

@@ -150,7 +158,13 @@ export class Brc29RemittanceModule
150158
this.outputDescription = cfg.outputDescription ?? 'Payment for remittance invoice'
151159
this.refundFeeSatoshis = cfg.refundFeeSatoshis ?? 1000
152160
this.minRefundSatoshis = cfg.minRefundSatoshis ?? 1000
153-
this.internalizeProtocol = cfg.internalizeProtocol ?? 'wallet payment'
161+
if (cfg.internalizeProtocol === 'basket insertion') {
162+
throw new TypeError(
163+
'BRC-29 settlements cannot be internalized as basket insertions. ' +
164+
'Use wallet payment for spendable wallet balance, or implement a separate custom-output protocol with insertionRemittance.'
165+
)
166+
}
167+
this.internalizeProtocol = 'wallet payment'
154168
this.nonceProvider = cfg.nonceProvider ?? DefaultNonceProvider
155169
this.lockingScriptProvider = cfg.lockingScriptProvider ?? DefaultLockingScriptProvider
156170
}
@@ -253,11 +267,9 @@ export class Brc29RemittanceModule
253267
): Promise<{ action: 'accept'; receiptData?: Brc29ReceiptData } | { action: 'terminate'; termination: Termination }> {
254268
const { wallet, originator } = ctx
255269
const origin = originator as OriginatorDomainNameStringUnder250Bytes | undefined
256-
console.log('acceptSettlement', args)
257270
try {
258271
const settlement = ensureValidSettlement(args.settlement)
259272
const outputIndex = settlement.outputIndex ?? 0
260-
debugger
261273
const internalizeResult = await wallet.internalizeAction(
262274
{
263275
tx: settlement.transaction,

packages/wallet/wallet-toolbox/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ attention to changes that materially alter behavior or extend functionality.
66

77
## wallet-toolbox (unreleased)
88

9+
- Release prep for `2.4.1`: define one managed-change policy across Knex and IndexedDB allocation,
10+
counting, default balance reporting, `balanceAndUtxos`, and `noSendChange`.
11+
Only complete BRC-29 signer metadata is eligible; custom rows remain visible
12+
through raw default-basket listing for recovery. `internalizeAction` now
13+
rejects basket insertion into `default`, prevents managed change from being
14+
reclassified, promotes verified legacy BRC-29 rows through `wallet payment`,
15+
and permits incompatible custom rows to be swept to non-default baskets
16+
without changing wallet balance.
17+
918
- Release prep for `2.4.0`: security fix for `GHSA-36f9-7rg5-cpf8`. `buildSignableTransaction` now verifies that storage-returned outputs match the caller's requested `args.outputs` before signing (rejects substituted/altered/reclassified/missing outputs), and rejects any unrequested output that isn't client-derived change or a single commission output bounded by `MAX_STORAGE_COMMISSION_SATOSHIS`. `WalletPermissionsManager.createAction` independently verifies requested outputs are present in the signable transaction before authorizing, as defense-in-depth. Protects against a malicious or compromised `StorageClient` operator substituting or injecting outputs that would be signed and broadcast without the caller's knowledge.
1019

1120
- Release prep for `2.3.3`: add Arcade-first wallet service wiring, go-chaintracks

packages/wallet/wallet-toolbox/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ const result = await wallet.createAction({
7575

7676
[Full API documentation](https://bsv-blockchain.github.io/wallet-toolbox) is available on GitHub Pages.
7777

78+
See [Managed change, sweeping, and recovery](./docs/managed-change-policy.md)
79+
for the default-basket invariant, automatic funding policy, and supported
80+
`internalizeAction` repair paths.
81+
7882
The codebase has detailed JSDoc annotations throughout — these will surface inline in editors like VS Code.
7983

8084
## Development

packages/wallet/wallet-toolbox/client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bsv/wallet-toolbox-client",
3-
"version": "2.4.0",
3+
"version": "2.4.1",
44
"description": "Client only Wallet Storage",
55
"main": "./out/src/index.client.js",
66
"types": "./out/src/index.client.d.ts",

packages/wallet/wallet-toolbox/docs/client.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17944,12 +17944,12 @@ When the transaction already exists, the description is updated. The isOutgoing
1794417944

1794517945
"basket insertion" Merge Rules:
1794617946
1. The "default" basket may not be specified as the insertion basket.
17947-
2. A change output in the "default" basket may not be target of an insertion into a different basket.
17948-
3. These baskets do not affect the wallet's balance and are typed "custom".
17947+
2. Managed change may not be reclassified as a basket insertion.
17948+
3. Basket insertions do not affect wallet balance and are typed "custom".
1794917949

1795017950
"wallet payment" Merge Rules:
17951-
1. Targetting an existing change "default" basket output results in a no-op. No error. No alterations made.
17952-
2. Targetting a previously "custom" non-change output converts it into a change output. This alters the transaction's `amount`, and the wallet balance.
17951+
1. Targeting existing managed change in the default basket is idempotent.
17952+
2. Targeting a custom output promotes it to managed BRC-29 change only after the locking script is verified. This is the supported recovery path for a misclassified BRC-29 payment.
1795317953

1795417954
```ts
1795517955
export async function internalizeAction(wallet: Wallet, auth: AuthId, args: InternalizeActionArgs): Promise<StorageInternalizeActionResult>
@@ -17980,12 +17980,12 @@ When the transaction already exists, the description is updated. The isOutgoing
1798017980

1798117981
"basket insertion" Merge Rules:
1798217982
1. The "default" basket may not be specified as the insertion basket.
17983-
2. A change output in the "default" basket may not be target of an insertion into a different basket.
17984-
3. These baskets do not affect the wallet's balance and are typed "custom".
17983+
2. Managed change may not be reclassified as a basket insertion.
17984+
3. Basket insertions do not affect wallet balance and are typed "custom".
1798517985

1798617986
"wallet payment" Merge Rules:
17987-
1. Targetting an existing change "default" basket output results in a no-op. No error. No alterations made.
17988-
2. Targetting a previously "custom" non-change output converts it into a change output. This alters the transaction's `satoshis`, and the wallet balance.
17987+
1. Targeting existing managed change in the default basket is idempotent.
17988+
2. Targeting a custom output promotes it to managed BRC-29 change and increases wallet balance. This includes recovery of a legacy custom row incorrectly stored in the default basket.
1798917989

1799017990
```ts
1799117991
export async function internalizeAction(storage: StorageProvider, auth: AuthId, args: InternalizeActionArgs): Promise<StorageInternalizeActionResult>

0 commit comments

Comments
 (0)