diff --git a/.github/workflows/bitcode-gate-quality.yml b/.github/workflows/bitcode-gate-quality.yml
index 14eae2dae..7085eabd8 100644
--- a/.github/workflows/bitcode-gate-quality.yml
+++ b/.github/workflows/bitcode-gate-quality.yml
@@ -99,6 +99,7 @@ jobs:
node scripts/check-v31-gate2-auxillaries-package-route-contracts.mjs --skip-branch-check
node scripts/check-v31-gate3-profile-account-state.mjs --skip-branch-check
node scripts/check-v31-gate4-connects-provider-readiness.mjs --skip-branch-check
+ node scripts/check-v31-gate5-wallet-btd-pane-readiness.mjs --skip-branch-check
else
echo "Unexpected BITCODE_SPEC.txt pointer: $POINTER" >&2
exit 1
@@ -139,6 +140,7 @@ jobs:
run: |
pnpm --dir uapi exec jest --runTestsByPath \
tests/userDataRoute.test.ts \
+ tests/auxillariesWalletPane.test.tsx \
tests/api/auxillariesGithubConnectionRoute.test.ts \
tests/api/vcsRoutes.test.ts \
tests/auxillariesExternalsPane.test.tsx \
diff --git a/BITCODE_SPEC_V31.md b/BITCODE_SPEC_V31.md
index 4c283e2c1..3c6085000 100644
--- a/BITCODE_SPEC_V31.md
+++ b/BITCODE_SPEC_V31.md
@@ -252,6 +252,9 @@ Gate 5 Wallet/BTD precision:
- Wallet panes consume no-custody wallet capability and signer posture from V30 BTD/wallet primitives.
- BTD panes expose range/read-right/treasury/settlement readiness summaries without revealing protected AssetPack source.
- Treasury and organization BTD posture remains distinct from Exchange market state.
+- `@bitcode/btd` owns the source-safe `BtdWalletBtdSupportProjection` used by Auxillaries to summarize wallet capability, signer readiness, network readiness, BTD read-right counts, range cell counts, account treasury posture, settlement blockers, and proof roots.
+- `AuxillariesWalletBtdPaneState` is the API-facing support object that carries that projection into `/api/auxillaries/data`; UI panes may render it but may not recalculate no-custody, read-right, or settlement law locally.
+- The Wallet pane may show account BTC fee posture, BTD range/read-right counts, roots, and blockers, but it must keep protected source visibility false before paid unlock and must identify that the support pane is not an Exchange market state surface.
Gate 6 policy precision:
@@ -418,6 +421,10 @@ The pane support model must contain:
- organization treasury posture kept distinct from Exchange market state;
- explicit proof that no server component custodies private keys or displays wallet secrets.
+Gate 5 binds this model through `BtdWalletBtdSupportProjection` in `@bitcode/btd` and `AuxillariesWalletBtdPaneState` in the package-owned Auxillaries route contract.
+The projection carries `walletCapabilityRoot` and `btdSupportRoot`, names signer capabilities such as message signing, PSBT signing, and rights transfer readiness, counts owner-read/licensed-read/pending/denied/unknown AssetPack read-right posture, sums range cells, and always marks `protectedSourceVisible: false` for pre-settlement support views.
+Its treasury summary is account-scoped, no-custody, and explicitly `not_exchange_market_state`.
+
No server component may custody the Reader private key.
Server-side routes may prepare receipts, validate proofs, serialize quote/posture evidence, and persist registry rows only when explicitly requested.
They must not claim signature, broadcast, finality, settlement unlock, or rights transfer without matching receipt and readback evidence.
diff --git a/BITCODE_SPEC_V31_DELTA.md b/BITCODE_SPEC_V31_DELTA.md
index d7e5c28d8..16ec7333e 100644
--- a/BITCODE_SPEC_V31_DELTA.md
+++ b/BITCODE_SPEC_V31_DELTA.md
@@ -144,6 +144,13 @@ Closure acceptance:
- BTD pane state consumes range/read-right/treasury/settlement readiness without protected source;
- treasury and organization BTD support remain distinct from Exchange market state.
+Gate 5 implementation centers:
+
+- `packages/btd/src/auxillaries-support.ts` owns `BtdWalletBtdSupportProjection`, deriving no-custody wallet capability, signer posture, network readiness, BTD range/read-right counts, account treasury posture, settlement blockers, and source-safe roots;
+- `packages/api/src/routes/auxillaries-contract.ts` maps that BTD package projection into `AuxillariesWalletBtdPaneState` so `/api/auxillaries/data` returns one package-owned Wallet/BTD support object;
+- `uapi/app/auxillaries/components/AuxillariesWalletPane.tsx` renders signer, network, range, read-right, settlement, treasury-boundary, and proof-root readouts from `walletBtdPaneState` rather than locally rederiving BTD law;
+- focused BTD, API, route, and pane tests prove no-custody posture, range/read-right summary, protected-source non-disclosure, and explicit non-Exchange treasury classification.
+
### Gate 6: Organization Team Role Policy Authority
Gate 6 deepens organization and policy authority.
diff --git a/BITCODE_SPEC_V31_NOTES.md b/BITCODE_SPEC_V31_NOTES.md
index f2ec8fd30..2db67ee8c 100644
--- a/BITCODE_SPEC_V31_NOTES.md
+++ b/BITCODE_SPEC_V31_NOTES.md
@@ -91,6 +91,14 @@ Route hydration must include template preference and notification readback where
## Gate 4 closure note
Gate 4 is closed only when Connects provider state is a source-safe readiness contract rather than a raw OAuth/VCS status blob.
+That source-safe provider connection posture remains closed after later V31 gates advance the roadmap.
Provider readiness must name provider id/name, installation state, credential posture, token presence class, scopes class, last readback status, blocker, repair action, and proof root without exposing raw provider credentials.
VCS and GitHub Auxillaries routes may use credentials internally to validate or repair a connection, but responses, UI metadata, telemetry, and recovery proof hooks must carry only classified posture and before/after readiness roots.
The Externals pane can display compact provider proof detail, but it must consume `connectionReadiness` and `recoveryRuns` from the package-owned Auxillaries contract instead of locally deciding provider law.
+
+## Gate 5 closure note
+
+Gate 5 is closed only when Wallet/BTD support is package-owned and source-safe.
+`@bitcode/btd` must derive no-custody wallet capability, signer posture, network readiness, BTD range/read-right counts, account treasury posture, settlement blockers, and support roots.
+`packages/api` may map that projection into `AuxillariesWalletBtdPaneState`, but UI and routes must not rederive BTD read-access, settlement, rights-transfer, or no-custody law locally.
+The Wallet pane must consume `walletBtdPaneState` from `/api/auxillaries/data`, render signer/network/range/read-right/settlement/treasury readiness, keep protected source invisible before paid unlock, and state that account treasury support is not Exchange market state.
diff --git a/BITCODE_SPEC_V31_PARITY_MATRIX.md b/BITCODE_SPEC_V31_PARITY_MATRIX.md
index 911f089f1..5357a144a 100644
--- a/BITCODE_SPEC_V31_PARITY_MATRIX.md
+++ b/BITCODE_SPEC_V31_PARITY_MATRIX.md
@@ -121,9 +121,9 @@ No `_legacy/` source is active source truth.
| Requirement | Source evidence | Current V31 judgment |
| --- | --- | --- |
-| Wallet panes consume no-custody signer posture | BTD wallet primitives and pane tests | pending |
-| BTD panes consume range/read-right/treasury summaries | BTD access and treasury tests | pending |
-| Settlement-readiness state remains distinct from Exchange market state | package and UI tests | pending |
+| Wallet panes consume no-custody signer posture | `packages/btd/src/auxillaries-support.ts`, `packages/api/src/routes/auxillaries-contract.ts`, and `uapi/tests/auxillariesWalletPane.test.tsx` project no-custody capability, signer readiness, signer actions, roots, and UI readout | drafted |
+| BTD panes consume range/read-right/treasury summaries | `BtdWalletBtdSupportProjection`, `AuxillariesWalletBtdPaneState`, `packages/btd/__tests__/btd.test.ts`, and `packages/api/src/routes/__tests__/auxillaries-contract.test.ts` cover range counts, read-right counts, BTC fee treasury, and protected-source invisibility | drafted |
+| Settlement-readiness state remains distinct from Exchange market state | Wallet pane and package tests assert account-scoped treasury posture, settlement blockers, and `not_exchange_market_state` classification | drafted |
## Gate 6 Parity
diff --git a/SPECIFICATIONS_ROADMAP.md b/SPECIFICATIONS_ROADMAP.md
index f3bbc4837..0f596e2a8 100644
--- a/SPECIFICATIONS_ROADMAP.md
+++ b/SPECIFICATIONS_ROADMAP.md
@@ -5,7 +5,7 @@
- Current active canonical pointer: `BITCODE_SPEC.txt` -> `V30`
- Current active canon: `BITCODE_SPEC_V30.md`
- Current draft target: `BITCODE_SPEC_V31.md`
-- Current working gate: V31 Gate 4 Connects provider readiness and recovery, which deepens source-safe provider connection posture with provider id/name, installation state, token presence class, scopes class, last readback status, blocker, repair action, readiness roots, and before/after recovery evidence over the V30 Protocol/BTD substrate.
+- Current working gate: V31 Gate 5 Wallet and BTD pane readiness, which connects Auxillaries Wallet/BTD UI to package-owned V30 wallet, signer, network, BTD range, read-right, treasury, no-custody, and settlement-readiness projections without protected source leakage or Exchange market inference.
- Purpose: concise running index of Bitcode/ENGI specification history, current work, and planned work.
This roadmap is not an active system specification.
diff --git a/package.json b/package.json
index 82bdca8e8..d111f3f5f 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
"check:v31-gate2": "node scripts/check-v31-gate2-auxillaries-package-route-contracts.mjs",
"check:v31-gate3": "node scripts/check-v31-gate3-profile-account-state.mjs",
"check:v31-gate4": "node scripts/check-v31-gate4-connects-provider-readiness.mjs",
+ "check:v31-gate5": "node scripts/check-v31-gate5-wallet-btd-pane-readiness.mjs",
"check:spec-quality": "node scripts/run-bitcode-spec-quality.mjs --mode basic",
"check:spec-quality:title": "node scripts/run-bitcode-spec-quality.mjs --mode strict-from-title",
"check:spec-quality:v24": "node scripts/run-bitcode-spec-quality.mjs --mode strict-version --version V24",
diff --git a/packages/api/README.md b/packages/api/README.md
index a0d4d0b26..d81ebc9ce 100644
--- a/packages/api/README.md
+++ b/packages/api/README.md
@@ -41,6 +41,15 @@ and a readiness root. Routes may validate credentials privately, but API
responses, UI metadata, telemetry, and recovery proof hooks must only serialize
classified readiness and `AuxillariesRecoveryRun` before/after roots.
+Wallet/BTD support state is package-owned through `@bitcode/btd` and mapped
+here as `AuxillariesWalletBtdPaneState`. Route payloads may include wallet
+binding facts, BTD holding counts, recent AssetPack ids/ranges, and BTC fee
+posture, but the no-custody signer posture, network readiness, range/read-right
+summary, account treasury boundary, settlement blockers, and support roots must
+come from the BTD projection. API responses must keep protected AssetPack source
+hidden, keep wallet private material out of JSON, and mark the Wallet/BTD
+treasury support state as separate from Exchange market state.
+
In V26 fourth-gate this package is where merged-world Bitcode becomes concrete:
- `/conversations` continuity
- `/executions` compatibility and pipeline-run APIs
diff --git a/packages/api/src/routes/__tests__/auxillaries-contract.test.ts b/packages/api/src/routes/__tests__/auxillaries-contract.test.ts
index 2b61a937c..3838ece47 100644
--- a/packages/api/src/routes/__tests__/auxillaries-contract.test.ts
+++ b/packages/api/src/routes/__tests__/auxillaries-contract.test.ts
@@ -45,6 +45,7 @@ describe('Auxillaries package route contracts', () => {
address: 'tb1pauxcontract',
verificationState: 'verified',
metadata: {
+ network: 'testnet',
private_key: 'wallet-secret',
connectedAt: '2026-05-21T00:00:00.000Z',
},
@@ -80,6 +81,8 @@ describe('Auxillaries package route contracts', () => {
assetPackId: 'asset-pack-1',
rangeStart: 1,
rangeEndExclusive: 3,
+ readRightState: 'owner_read',
+ sourceSafePreviewRoot: 'source-safe-preview-root',
},
],
modelPreferences: {
@@ -149,6 +152,39 @@ describe('Auxillaries package route contracts', () => {
expect(payload.walletBtdPaneState.signerPosture).toMatchObject({
ready: true,
state: 'verified',
+ canSignPsbt: true,
+ serverCustody: false,
+ });
+ expect(payload.walletBtdPaneState.walletCapability).toMatchObject({
+ network: 'testnet',
+ noCustody: true,
+ serverCustody: false,
+ capabilities: expect.arrayContaining(['message_sign', 'psbt_sign', 'rights_transfer']),
+ });
+ expect(payload.walletBtdPaneState.networkReadiness).toMatchObject({
+ state: 'ready',
+ network: 'testnet',
+ blocker: null,
+ });
+ expect(payload.walletBtdPaneState.btdReadRightSummary).toMatchObject({
+ aggregateBtd: 128,
+ assetPackCount: 1,
+ rangeCount: 1,
+ totalRangeCells: 2,
+ ownerReadCount: 1,
+ protectedSourceVisible: false,
+ sourceSafePreviewRoots: ['source-safe-preview-root'],
+ });
+ expect(payload.walletBtdPaneState.treasurySummary).toMatchObject({
+ feeAsset: 'BTC',
+ noCustody: true,
+ treasuryScope: 'account',
+ organizationTreasurySeparated: true,
+ exchangeMarketState: 'not_exchange_market_state',
+ });
+ expect(payload.walletBtdPaneState).toMatchObject({
+ settlementReadiness: 'ready',
+ settlementBlockers: [],
});
expect(payload.organizationAuthority.policyDecision).toBe('allowed');
expect(payload.auxillariesContract.contractVersion).toBe(AUXILLARIES_CONTRACT_VERSION);
@@ -163,6 +199,7 @@ describe('Auxillaries package route contracts', () => {
expect(JSON.stringify(payload)).not.toContain('wallet-secret');
expect(JSON.stringify(payload)).not.toContain('repository source');
expect(JSON.stringify(payload)).not.toContain('private prompt body');
+ expect(JSON.stringify(payload.walletBtdPaneState)).not.toContain('asset pack source');
expect(JSON.stringify(payload.connectionReadiness[0].metadata)).not.toContain('provider-token');
expect(assertAuxillariesJsonSafe(payload)).toBeUndefined();
});
diff --git a/packages/api/src/routes/auxillaries-contract.ts b/packages/api/src/routes/auxillaries-contract.ts
index 287a83326..0257a3dde 100644
--- a/packages/api/src/routes/auxillaries-contract.ts
+++ b/packages/api/src/routes/auxillaries-contract.ts
@@ -1,4 +1,5 @@
import { createHash } from 'crypto';
+import { buildBtdWalletBtdSupportProjection } from '@bitcode/btd';
export const AUXILLARY_FLOW_STEPS = ['wallet', 'externals', 'profile', 'interfaces'] as const;
export const AUXILLARIES_CONTRACT_VERSION = 'v31-draft-auxillaries-contracts' as const;
@@ -85,6 +86,9 @@ export interface AuxillaryBtdAssetPackSummary {
label?: string;
rangeStart?: number;
rangeEndExclusive?: number;
+ readRightState?: 'owner_read' | 'licensed_read' | 'pending_settlement' | 'denied' | 'unknown';
+ accessPolicyHash?: string | null;
+ sourceSafePreviewRoot?: string | null;
acquiredAt?: string | null;
}
@@ -234,24 +238,50 @@ export interface AuxillariesWalletBtdPaneState {
provider: string | null;
address: string | null;
verificationState: string | null;
+ network: string | null;
noCustody: true;
+ serverCustody: false;
+ capabilities: Array<'message_sign' | 'psbt_sign' | 'rights_transfer'>;
+ walletCapabilityRoot: string;
};
signerPosture: {
ready: boolean;
state: 'verified' | 'manual' | 'pending' | 'missing' | 'invalid';
requiredAction: 'none' | 'connect_wallet' | 'verify_wallet_signature' | 'repair_wallet_binding';
+ canSignPsbt: boolean;
+ canSignRightsTransfer: boolean;
+ serverCustody: false;
+ };
+ networkReadiness: {
+ state: AuxillariesReadinessState;
+ network: string | null;
+ requiredAction: 'none' | 'connect_wallet' | 'verify_network' | 'repair_wallet_binding';
+ blocker: string | null;
};
btdReadRightSummary: {
aggregateBtd: number;
assetPackCount: number;
recentAssetPackIds: string[];
+ rangeCount: number;
+ totalRangeCells: number;
+ ownerReadCount: number;
+ licensedReadCount: number;
+ pendingSettlementCount: number;
+ deniedCount: number;
+ unknownCount: number;
+ protectedSourceVisible: false;
+ sourceSafePreviewRoots: string[];
};
treasurySummary: {
btcFeeBalance: number | null;
feeAsset: 'BTC';
noCustody: true;
+ treasuryScope: 'account';
+ organizationTreasurySeparated: true;
+ exchangeMarketState: 'not_exchange_market_state';
};
settlementReadiness: AuxillariesReadinessState;
+ settlementBlockers: string[];
sourceSafetyClass: AuxillariesSourceSafetyClass;
btdSupportRoot: string;
}
@@ -1118,61 +1148,40 @@ export function buildAuxillariesWalletBtdPaneState(input: {
const verificationState = readString(walletStatus?.verificationState) ?? binding?.status ?? null;
const connected = readBoolean(walletStatus?.connected) ?? Boolean(address);
const valid = readBoolean(walletStatus?.valid) ?? verificationState === 'verified';
- const signerState: AuxillariesWalletBtdPaneState['signerPosture']['state'] = !address
- ? 'missing'
- : valid || verificationState === 'verified'
- ? 'verified'
- : verificationState === 'pending'
- ? 'pending'
- : verificationState === 'manual'
- ? 'manual'
- : connected
- ? 'invalid'
- : 'missing';
- const recentAssetPacks = Array.isArray(input.recentBtdAssetPacks) ? input.recentBtdAssetPacks : [];
- const aggregateBtd = typeof input.btdBalance === 'number' && Number.isFinite(input.btdBalance)
- ? input.btdBalance
- : 0;
- const withoutRoot = {
- kind: 'AuxillariesWalletBtdPaneState' as const,
- walletCapability: {
- hasBinding: Boolean(address),
- provider,
+ const walletMetadata = asRecord(walletStatus?.metadata);
+ const projection = buildBtdWalletBtdSupportProjection({
+ wallet: {
address,
+ provider,
verificationState,
- noCustody: true as const,
- },
- signerPosture: {
- ready: signerState === 'verified',
- state: signerState,
- requiredAction:
- signerState === 'verified'
- ? 'none' as const
- : signerState === 'missing'
- ? 'connect_wallet' as const
- : signerState === 'manual' || signerState === 'pending'
- ? 'verify_wallet_signature' as const
- : 'repair_wallet_binding' as const,
+ connected,
+ valid,
+ network:
+ readString(walletStatus?.network) ??
+ readString(walletMetadata?.network) ??
+ null,
},
- btdReadRightSummary: {
- aggregateBtd,
- assetPackCount: recentAssetPacks.length,
- recentAssetPackIds: recentAssetPacks.map((assetPack) => assetPack.assetPackId).filter(Boolean),
- },
- treasurySummary: {
- btcFeeBalance: typeof input.btcFeeBalance === 'number' && Number.isFinite(input.btcFeeBalance)
- ? input.btcFeeBalance
- : null,
- feeAsset: 'BTC' as const,
- noCustody: true as const,
- },
- settlementReadiness: signerState === 'verified' ? 'ready' as const : 'degraded' as const,
+ aggregateBtd: input.btdBalance,
+ btcFeeBalance: input.btcFeeBalance,
+ recentAssetPacks: Array.isArray(input.recentBtdAssetPacks)
+ ? input.recentBtdAssetPacks
+ : [],
+ });
+ const withoutRoot: Omit = {
+ kind: 'AuxillariesWalletBtdPaneState' as const,
+ walletCapability: projection.walletCapability,
+ signerPosture: projection.signerPosture,
+ networkReadiness: projection.networkReadiness,
+ btdReadRightSummary: projection.btdReadRightSummary,
+ treasurySummary: projection.treasurySummary,
+ settlementReadiness: projection.settlementReadiness,
+ settlementBlockers: projection.settlementBlockers,
sourceSafetyClass: 'source_safe' as AuxillariesSourceSafetyClass,
};
return {
...withoutRoot,
- btdSupportRoot: stableProofRoot('auxillaries-wallet-btd-pane-state', withoutRoot),
+ btdSupportRoot: projection.btdSupportRoot,
};
}
diff --git a/packages/api/src/routes/auxillaries.ts b/packages/api/src/routes/auxillaries.ts
index 0b25037a2..1b7ae5bfa 100644
--- a/packages/api/src/routes/auxillaries.ts
+++ b/packages/api/src/routes/auxillaries.ts
@@ -159,11 +159,36 @@ async function listRecentBtdAssetPacksForProfile(
const rangeStart = readNumberField(row, 'range_start', 'rangeStart');
const rangeEndExclusive = readNumberField(row, 'range_end_exclusive', 'rangeEndExclusive');
const acquiredAt = readStringField(row, 'issued_at', 'acquiredAt');
+ const readRightState = readStringField(
+ row,
+ 'read_right_state',
+ 'readRightState',
+ 'settlement_state',
+ 'settlementState',
+ );
+ const accessPolicyHash = readStringField(row, 'access_policy_hash', 'accessPolicyHash');
+ const sourceSafePreviewRoot = readStringField(
+ row,
+ 'source_safe_preview_root',
+ 'preview_root',
+ 'sourceSafePreviewRoot',
+ );
if (label) summary.label = label;
if (typeof rangeStart === 'number') summary.rangeStart = rangeStart;
if (typeof rangeEndExclusive === 'number') summary.rangeEndExclusive = rangeEndExclusive;
if (acquiredAt) summary.acquiredAt = acquiredAt;
+ if (
+ readRightState === 'owner_read' ||
+ readRightState === 'licensed_read' ||
+ readRightState === 'pending_settlement' ||
+ readRightState === 'denied' ||
+ readRightState === 'unknown'
+ ) {
+ summary.readRightState = readRightState;
+ }
+ if (accessPolicyHash) summary.accessPolicyHash = accessPolicyHash;
+ if (sourceSafePreviewRoot) summary.sourceSafePreviewRoot = sourceSafePreviewRoot;
return summary;
})
diff --git a/packages/btd/README.md b/packages/btd/README.md
index cd01ae011..17c733867 100644
--- a/packages/btd/README.md
+++ b/packages/btd/README.md
@@ -40,6 +40,11 @@ This package owns:
- BTC fee operation posture, including quote lifecycle, signer recovery,
no-server-custody PSBT handoff, Taproot/script posture, broadcast/finality
observation, replacement/reorg repair, and testnet/mainnet network policy
+- Auxillaries Wallet/BTD support projection through
+ `BtdWalletBtdSupportProjection`, which derives no-custody wallet capability,
+ signer posture, network readiness, source-safe BTD range/read-right counts,
+ account treasury posture, settlement blockers, and roots without exposing
+ protected source or wallet private material
- Terminal operational health reads that compose deployment lanes, telemetry,
upgrade posture, provider readiness, settlement-network posture, synthetic
testnet minting, journal rows, ledger anchors, and reconciliation state
@@ -71,6 +76,7 @@ import {
buildBridgeReadinessResearchPosture,
buildBtdProtocolTelemetryEnvelope,
buildBtdInterfaceIntegrationRegressionProof,
+ buildBtdWalletBtdSupportProjection,
reconcileLedgerDatabaseProjection,
sourceToSharesProofToSettlementConservationCheck,
toBtdJsonSafe,
@@ -93,6 +99,8 @@ support panes can surface range, read-right, treasury, signer, settlement, and
no-custody posture from this package through source-safe summaries, but they
must not rederive `$BTD` mint, read-access, rights-transfer, BTC fee, bridge,
or source-disclosure law locally.
+The Auxillaries support projection is account-treasury support, not Exchange
+market state, and always keeps protected source invisible before paid unlock.
Terminal should consume the operational-health subpath when it needs the
client-safe read model without importing storage-backed package entry points:
diff --git a/packages/btd/__tests__/btd.test.ts b/packages/btd/__tests__/btd.test.ts
index 760044b8b..d69e2d8a2 100644
--- a/packages/btd/__tests__/btd.test.ts
+++ b/packages/btd/__tests__/btd.test.ts
@@ -13,6 +13,7 @@ import {
assertBtdAccessPolicyTemplateCoverage,
assertBtdMintableSupplyLimit,
buildAssetPackSettlementUnlock,
+ buildBtdWalletBtdSupportProjection,
buildBtdReadAccessProjectionFromRegistryRows,
calculateLlmBtcFeeEstimate,
calculateMeasuredBtdFromTokens,
@@ -140,6 +141,95 @@ describe('AssetPack settlement unlock', () => {
});
});
+describe('Auxillaries Wallet/BTD support projection', () => {
+ it('derives no-custody signer, range, read-right, treasury, and settlement posture', () => {
+ const projection = buildBtdWalletBtdSupportProjection({
+ wallet: {
+ address: 'tb1pwallet',
+ provider: 'leather',
+ verificationState: 'verified',
+ connected: true,
+ valid: true,
+ network: 'testnet',
+ },
+ aggregateBtd: 12,
+ btcFeeBalance: 0.025,
+ recentAssetPacks: [
+ {
+ assetPackId: 'asset-pack-owner',
+ rangeStart: 100,
+ rangeEndExclusive: 112,
+ readRightState: 'owner_read',
+ sourceSafePreviewRoot: 'preview-root-owner',
+ },
+ {
+ assetPackId: 'asset-pack-license',
+ rangeStart: 200,
+ rangeEndExclusive: 205,
+ readRightState: 'licensed_read',
+ },
+ ],
+ });
+
+ expect(projection.walletCapability).toMatchObject({
+ hasBinding: true,
+ noCustody: true,
+ serverCustody: false,
+ capabilities: ['message_sign', 'psbt_sign', 'rights_transfer'],
+ });
+ expect(projection.signerPosture).toMatchObject({
+ ready: true,
+ canSignPsbt: true,
+ canSignRightsTransfer: true,
+ serverCustody: false,
+ });
+ expect(projection.networkReadiness).toMatchObject({
+ state: 'ready',
+ network: 'testnet',
+ blocker: null,
+ });
+ expect(projection.btdReadRightSummary).toMatchObject({
+ aggregateBtd: 12,
+ assetPackCount: 2,
+ rangeCount: 2,
+ totalRangeCells: 17,
+ ownerReadCount: 1,
+ licensedReadCount: 1,
+ protectedSourceVisible: false,
+ sourceSafePreviewRoots: ['preview-root-owner'],
+ });
+ expect(projection.treasurySummary).toMatchObject({
+ feeAsset: 'BTC',
+ noCustody: true,
+ treasuryScope: 'account',
+ organizationTreasurySeparated: true,
+ exchangeMarketState: 'not_exchange_market_state',
+ });
+ expect(projection.settlementReadiness).toBe('ready');
+ expect(projection.settlementBlockers).toEqual([]);
+ expect(projection.btdSupportRoot).toMatch(/^[0-9a-f]{64}$/);
+ });
+
+ it('blocks settlement when wallet identity is missing', () => {
+ const projection = buildBtdWalletBtdSupportProjection({
+ wallet: null,
+ aggregateBtd: 0,
+ recentAssetPacks: [],
+ });
+
+ expect(projection.walletCapability.hasBinding).toBe(false);
+ expect(projection.signerPosture.requiredAction).toBe('connect_wallet');
+ expect(projection.networkReadiness).toMatchObject({
+ state: 'blocked',
+ blocker: 'wallet.network_missing',
+ });
+ expect(projection.settlementReadiness).toBe('blocked');
+ expect(projection.settlementBlockers).toEqual(
+ expect.arrayContaining(['wallet.connect_wallet', 'wallet.network_missing']),
+ );
+ });
+});
+
describe('calculateMeasuredBtdFromTokens', () => {
it('measures content amount without implying spend', () => {
expect(calculateMeasuredBtdFromTokens({ inputTokens: 1200, outputTokens: 50 })).toBe(2);
diff --git a/packages/btd/src/auxillaries-support.ts b/packages/btd/src/auxillaries-support.ts
new file mode 100644
index 000000000..094c9de2e
--- /dev/null
+++ b/packages/btd/src/auxillaries-support.ts
@@ -0,0 +1,334 @@
+import { createHash } from 'crypto';
+
+import { BITCODE_FEE_ASSET } from './constants';
+
+export type BtdAuxillariesSignerState = 'verified' | 'manual' | 'pending' | 'missing' | 'invalid';
+export type BtdAuxillariesSignerRequiredAction =
+ | 'none'
+ | 'connect_wallet'
+ | 'verify_wallet_signature'
+ | 'repair_wallet_binding';
+export type BtdAuxillariesReadinessState = 'ready' | 'degraded' | 'blocked' | 'unknown';
+export type BtdAuxillariesNetworkRequiredAction =
+ | 'none'
+ | 'connect_wallet'
+ | 'verify_network'
+ | 'repair_wallet_binding';
+export type BtdAuxillariesReadRightState =
+ | 'owner_read'
+ | 'licensed_read'
+ | 'pending_settlement'
+ | 'denied'
+ | 'unknown';
+
+export interface BtdAuxillariesWalletInput {
+ address?: string | null;
+ provider?: string | null;
+ verificationState?: string | null;
+ connected?: boolean | null;
+ valid?: boolean | null;
+ network?: string | null;
+}
+
+export interface BtdAuxillariesAssetPackInput {
+ assetPackId: string;
+ rangeStart?: number | null;
+ rangeEndExclusive?: number | null;
+ readRightState?: BtdAuxillariesReadRightState | string | null;
+ accessPolicyHash?: string | null;
+ sourceSafePreviewRoot?: string | null;
+ acquiredAt?: string | null;
+}
+
+export interface BtdWalletBtdSupportProjection {
+ kind: 'btd.wallet_btd_support_projection';
+ walletCapability: {
+ hasBinding: boolean;
+ provider: string | null;
+ address: string | null;
+ verificationState: string | null;
+ network: string | null;
+ noCustody: true;
+ serverCustody: false;
+ capabilities: Array<'message_sign' | 'psbt_sign' | 'rights_transfer'>;
+ walletCapabilityRoot: string;
+ };
+ signerPosture: {
+ ready: boolean;
+ state: BtdAuxillariesSignerState;
+ requiredAction: BtdAuxillariesSignerRequiredAction;
+ canSignPsbt: boolean;
+ canSignRightsTransfer: boolean;
+ serverCustody: false;
+ };
+ networkReadiness: {
+ state: BtdAuxillariesReadinessState;
+ network: string | null;
+ requiredAction: BtdAuxillariesNetworkRequiredAction;
+ blocker: string | null;
+ };
+ btdReadRightSummary: {
+ aggregateBtd: number;
+ assetPackCount: number;
+ recentAssetPackIds: string[];
+ rangeCount: number;
+ totalRangeCells: number;
+ ownerReadCount: number;
+ licensedReadCount: number;
+ pendingSettlementCount: number;
+ deniedCount: number;
+ unknownCount: number;
+ protectedSourceVisible: false;
+ sourceSafePreviewRoots: string[];
+ };
+ treasurySummary: {
+ btcFeeBalance: number | null;
+ feeAsset: typeof BITCODE_FEE_ASSET;
+ noCustody: true;
+ treasuryScope: 'account';
+ organizationTreasurySeparated: true;
+ exchangeMarketState: 'not_exchange_market_state';
+ };
+ settlementReadiness: BtdAuxillariesReadinessState;
+ settlementBlockers: string[];
+ sourceSafetyClass: 'source_safe';
+ btdSupportRoot: string;
+}
+
+export function buildBtdWalletBtdSupportProjection(input: {
+ wallet?: BtdAuxillariesWalletInput | null;
+ aggregateBtd?: number | null;
+ btcFeeBalance?: number | null;
+ recentAssetPacks?: BtdAuxillariesAssetPackInput[] | null;
+}): BtdWalletBtdSupportProjection {
+ const wallet = input.wallet ?? null;
+ const address = readString(wallet?.address);
+ const provider = readString(wallet?.provider);
+ const verificationState = readString(wallet?.verificationState);
+ const network = readString(wallet?.network);
+ const connected = typeof wallet?.connected === 'boolean' ? wallet.connected : Boolean(address);
+ const valid = typeof wallet?.valid === 'boolean' ? wallet.valid : verificationState === 'verified';
+ const signerState = deriveSignerState({ address, connected, valid, verificationState });
+ const signerReady = signerState === 'verified';
+ const signerRequiredAction: BtdAuxillariesSignerRequiredAction = signerReady
+ ? 'none'
+ : signerState === 'missing'
+ ? 'connect_wallet'
+ : signerState === 'manual' || signerState === 'pending'
+ ? 'verify_wallet_signature'
+ : 'repair_wallet_binding';
+ const capabilities = [
+ address ? 'message_sign' : null,
+ signerReady ? 'psbt_sign' : null,
+ signerReady ? 'rights_transfer' : null,
+ ].filter((capability): capability is 'message_sign' | 'psbt_sign' | 'rights_transfer' => Boolean(capability));
+ const walletCapabilityWithoutRoot = {
+ hasBinding: Boolean(address),
+ provider,
+ address,
+ verificationState,
+ network,
+ noCustody: true as const,
+ serverCustody: false as const,
+ capabilities,
+ };
+ const networkReadiness = deriveNetworkReadiness({ address, network, signerState });
+ const assetPacks = (input.recentAssetPacks ?? []).map(normalizeAssetPack).filter(Boolean) as NormalizedBtdAssetPack[];
+ const readRightCounts = assetPacks.reduce(
+ (counts, assetPack) => {
+ counts[assetPack.readRightState] += 1;
+ return counts;
+ },
+ {
+ owner_read: 0,
+ licensed_read: 0,
+ pending_settlement: 0,
+ denied: 0,
+ unknown: 0,
+ } satisfies Record,
+ );
+ const settlementBlockers = [
+ signerReady ? null : `wallet.${signerRequiredAction}`,
+ networkReadiness.state === 'blocked' ? networkReadiness.blocker : null,
+ ].filter((entry): entry is string => Boolean(entry));
+ const settlementReadiness: BtdAuxillariesReadinessState = settlementBlockers.length
+ ? address
+ ? 'degraded'
+ : 'blocked'
+ : 'ready';
+ const withoutRoot = {
+ kind: 'btd.wallet_btd_support_projection' as const,
+ walletCapability: {
+ ...walletCapabilityWithoutRoot,
+ walletCapabilityRoot: stableRoot('btd-wallet-capability', walletCapabilityWithoutRoot),
+ },
+ signerPosture: {
+ ready: signerReady,
+ state: signerState,
+ requiredAction: signerRequiredAction,
+ canSignPsbt: signerReady,
+ canSignRightsTransfer: signerReady,
+ serverCustody: false as const,
+ },
+ networkReadiness,
+ btdReadRightSummary: {
+ aggregateBtd: readFiniteNumber(input.aggregateBtd) ?? 0,
+ assetPackCount: assetPacks.length,
+ recentAssetPackIds: assetPacks.map((assetPack) => assetPack.assetPackId),
+ rangeCount: assetPacks.filter((assetPack) => assetPack.rangeCells > 0).length,
+ totalRangeCells: assetPacks.reduce((sum, assetPack) => sum + assetPack.rangeCells, 0),
+ ownerReadCount: readRightCounts.owner_read,
+ licensedReadCount: readRightCounts.licensed_read,
+ pendingSettlementCount: readRightCounts.pending_settlement,
+ deniedCount: readRightCounts.denied,
+ unknownCount: readRightCounts.unknown,
+ protectedSourceVisible: false as const,
+ sourceSafePreviewRoots: assetPacks
+ .map((assetPack) => assetPack.sourceSafePreviewRoot)
+ .filter((root): root is string => Boolean(root)),
+ },
+ treasurySummary: {
+ btcFeeBalance: readFiniteNumber(input.btcFeeBalance),
+ feeAsset: BITCODE_FEE_ASSET,
+ noCustody: true as const,
+ treasuryScope: 'account' as const,
+ organizationTreasurySeparated: true as const,
+ exchangeMarketState: 'not_exchange_market_state' as const,
+ },
+ settlementReadiness,
+ settlementBlockers,
+ sourceSafetyClass: 'source_safe' as const,
+ };
+
+ return {
+ ...withoutRoot,
+ btdSupportRoot: stableRoot('btd-wallet-btd-support-projection', withoutRoot),
+ };
+}
+
+interface NormalizedBtdAssetPack {
+ assetPackId: string;
+ rangeCells: number;
+ readRightState: BtdAuxillariesReadRightState;
+ sourceSafePreviewRoot: string | null;
+}
+
+function normalizeAssetPack(value: BtdAuxillariesAssetPackInput): NormalizedBtdAssetPack | null {
+ const assetPackId = readString(value.assetPackId);
+ if (!assetPackId) return null;
+ const rangeStart = readFiniteNumber(value.rangeStart);
+ const rangeEndExclusive = readFiniteNumber(value.rangeEndExclusive);
+ const rangeCells =
+ typeof rangeStart === 'number' &&
+ typeof rangeEndExclusive === 'number' &&
+ rangeEndExclusive > rangeStart
+ ? rangeEndExclusive - rangeStart
+ : 0;
+
+ return {
+ assetPackId,
+ rangeCells,
+ readRightState: normalizeReadRightState(value.readRightState),
+ sourceSafePreviewRoot: readString(value.sourceSafePreviewRoot),
+ };
+}
+
+function deriveSignerState(input: {
+ address: string | null;
+ connected: boolean;
+ valid: boolean;
+ verificationState: string | null;
+}): BtdAuxillariesSignerState {
+ if (!input.address) return 'missing';
+ if (input.valid || input.verificationState === 'verified') return 'verified';
+ if (input.verificationState === 'pending') return 'pending';
+ if (input.verificationState === 'manual') return 'manual';
+ if (input.connected) return 'invalid';
+ return 'missing';
+}
+
+function deriveNetworkReadiness(input: {
+ address: string | null;
+ network: string | null;
+ signerState: BtdAuxillariesSignerState;
+}): BtdWalletBtdSupportProjection['networkReadiness'] {
+ if (!input.address) {
+ return {
+ state: 'blocked',
+ network: null,
+ requiredAction: 'connect_wallet',
+ blocker: 'wallet.network_missing',
+ };
+ }
+
+ if (!input.network) {
+ return {
+ state: input.signerState === 'verified' ? 'degraded' : 'blocked',
+ network: null,
+ requiredAction: 'verify_network',
+ blocker: 'wallet.network_unverified',
+ };
+ }
+
+ if (input.signerState === 'invalid') {
+ return {
+ state: 'blocked',
+ network: input.network,
+ requiredAction: 'repair_wallet_binding',
+ blocker: 'wallet.binding_invalid',
+ };
+ }
+
+ return {
+ state: 'ready',
+ network: input.network,
+ requiredAction: 'none',
+ blocker: null,
+ };
+}
+
+function normalizeReadRightState(value: unknown): BtdAuxillariesReadRightState {
+ const normalized = readString(value);
+ if (
+ normalized === 'owner_read' ||
+ normalized === 'licensed_read' ||
+ normalized === 'pending_settlement' ||
+ normalized === 'denied' ||
+ normalized === 'unknown'
+ ) {
+ return normalized;
+ }
+
+ return 'unknown';
+}
+
+function readString(value: unknown): string | null {
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
+}
+
+function readFiniteNumber(value: unknown): number | null {
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
+ if (typeof value === 'string' && value.trim()) {
+ const parsed = Number(value);
+ if (Number.isFinite(parsed)) return parsed;
+ }
+ return null;
+}
+
+function stableRoot(label: string, value: unknown) {
+ return createHash('sha256')
+ .update(`${label}:`)
+ .update(stableSerialize(value))
+ .digest('hex');
+}
+
+function stableSerialize(value: unknown): string {
+ if (Array.isArray(value)) return `[${value.map(stableSerialize).join(',')}]`;
+ if (value && typeof value === 'object') {
+ return `{${Object.keys(value as Record)
+ .sort()
+ .map((key) => `${JSON.stringify(key)}:${stableSerialize((value as Record)[key])}`)
+ .join(',')}}`;
+ }
+ return JSON.stringify(value);
+}
diff --git a/packages/btd/src/index.ts b/packages/btd/src/index.ts
index 84c9620e6..f617fea1a 100644
--- a/packages/btd/src/index.ts
+++ b/packages/btd/src/index.ts
@@ -163,6 +163,7 @@ export * from './allocation';
export * from './ancestry';
export * from './api-boundaries';
export * from './authority';
+export * from './auxillaries-support';
export * from './constants';
export * from './deployment-lanes';
export * from './exchange';
diff --git a/scripts/check-v31-gate4-connects-provider-readiness.mjs b/scripts/check-v31-gate4-connects-provider-readiness.mjs
index 76c63c7d4..f2c45961e 100644
--- a/scripts/check-v31-gate4-connects-provider-readiness.mjs
+++ b/scripts/check-v31-gate4-connects-provider-readiness.mjs
@@ -74,8 +74,8 @@ function main() {
const branch = git(root, ['branch', '--show-current']);
assertCheck(
failures,
- branch === 'version/v31' || /^v31\/gate-4-[a-z0-9][a-z0-9-]*$/u.test(branch),
- `V31 Gate 4 work must occur on version/v31 or v31/gate-4-* branches. Observed ${branch || 'detached HEAD'}.`,
+ branch === 'version/v31' || /^v31\/gate-(?:4|5|6|7|8|9|10)-[a-z0-9][a-z0-9-]*$/u.test(branch),
+ `V31 Gate 4+ work must occur on version/v31 or v31/gate-4+ branches. Observed ${branch || 'detached HEAD'}.`,
);
}
@@ -219,7 +219,11 @@ function main() {
);
}
- assertCheck(failures, roadmap.includes('Current working gate: V31 Gate 4'), 'Roadmap must track V31 Gate 4 as current working gate.');
+ assertCheck(
+ failures,
+ /Current working gate: V31 Gate (?:4|5|6|7|8|9|10)\b/u.test(roadmap),
+ 'Roadmap must track V31 Gate 4 or a later V31 gate as current working gate.',
+ );
assertCheck(failures, delta.includes('Gate 4 implementation centers'), 'V31 DELTA must name Gate 4 implementation centers.');
assertCheck(failures, notes.includes('Gate 4 closure note'), 'V31 NOTES must include the Gate 4 closure note.');
assertCheck(failures, !/\|\s*Provider readiness is source-safe\s*\|[^|]*\|\s*pending\s*\|/u.test(parity), 'Gate 4 provider-readiness parity must not remain pending.');
diff --git a/scripts/check-v31-gate5-wallet-btd-pane-readiness.mjs b/scripts/check-v31-gate5-wallet-btd-pane-readiness.mjs
new file mode 100644
index 000000000..9a796c7c6
--- /dev/null
+++ b/scripts/check-v31-gate5-wallet-btd-pane-readiness.mjs
@@ -0,0 +1,247 @@
+#!/usr/bin/env node
+
+import { execFileSync } from 'node:child_process';
+import { existsSync, readFileSync } from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const defaultRepoRoot = path.resolve(__dirname, '..');
+
+function read(root, relativePath) {
+ return readFileSync(path.join(root, relativePath), 'utf8');
+}
+
+function fileExists(root, relativePath) {
+ return existsSync(path.join(root, relativePath));
+}
+
+function git(root, args) {
+ return execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim();
+}
+
+function assertCheck(failures, condition, message) {
+ if (!condition) failures.push(message);
+}
+
+function parseArgs(argv) {
+ const args = {
+ skipBranchCheck: false,
+ repoRoot: defaultRepoRoot,
+ };
+
+ for (let index = 0; index < argv.length; index += 1) {
+ const arg = argv[index];
+ if (arg === '--skip-branch-check') args.skipBranchCheck = true;
+ else if (arg === '--repo-root') args.repoRoot = path.resolve(argv[++index]);
+ else if (arg === '--help' || arg === '-h') args.help = true;
+ else throw new Error(`Unknown argument ${arg}`);
+ }
+
+ return args;
+}
+
+function printHelp() {
+ process.stdout.write(
+ [
+ 'Usage: node scripts/check-v31-gate5-wallet-btd-pane-readiness.mjs [--skip-branch-check] [--repo-root ]',
+ '',
+ 'Checks V31 Gate 5 Wallet/BTD pane readiness, no-custody projection, source-safe read-right summaries, tests, docs, and workflow coverage.',
+ ].join('\n'),
+ );
+ process.stdout.write('\n');
+}
+
+function main() {
+ const args = parseArgs(process.argv.slice(2));
+ if (args.help) {
+ printHelp();
+ return;
+ }
+
+ const root = args.repoRoot;
+ const failures = [];
+ const pointer = read(root, 'BITCODE_SPEC.txt').trim();
+
+ assertCheck(
+ failures,
+ pointer === 'V30',
+ `BITCODE_SPEC.txt must remain V30 during V31 gate work. Observed ${pointer || 'empty'}.`,
+ );
+
+ if (!args.skipBranchCheck) {
+ const branch = git(root, ['branch', '--show-current']);
+ assertCheck(
+ failures,
+ branch === 'version/v31' || /^v31\/gate-(?:5|6|7|8|9|10)-[a-z0-9][a-z0-9-]*$/u.test(branch),
+ `V31 Gate 5+ work must occur on version/v31 or v31/gate-5+ branches. Observed ${branch || 'detached HEAD'}.`,
+ );
+ }
+
+ for (const relativePath of [
+ 'packages/btd/src/auxillaries-support.ts',
+ 'packages/btd/__tests__/btd.test.ts',
+ 'packages/api/src/routes/auxillaries-contract.ts',
+ 'packages/api/src/routes/auxillaries.ts',
+ 'packages/api/src/routes/__tests__/auxillaries-contract.test.ts',
+ 'uapi/app/auxillaries/components/AuxillariesWalletPane.tsx',
+ 'uapi/hooks/useUserData.ts',
+ 'uapi/hooks/useUserData.js',
+ 'uapi/tests/auxillariesWalletPane.test.tsx',
+ 'uapi/tests/userDataRoute.test.ts',
+ 'BITCODE_SPEC_V31.md',
+ 'BITCODE_SPEC_V31_DELTA.md',
+ 'BITCODE_SPEC_V31_NOTES.md',
+ 'BITCODE_SPEC_V31_PARITY_MATRIX.md',
+ 'SPECIFICATIONS_ROADMAP.md',
+ ]) {
+ assertCheck(failures, fileExists(root, relativePath), `Missing V31 Gate 5 file: ${relativePath}`);
+ }
+
+ const btdSupport = read(root, 'packages/btd/src/auxillaries-support.ts');
+ const btdIndex = read(root, 'packages/btd/src/index.ts');
+ const btdTest = read(root, 'packages/btd/__tests__/btd.test.ts');
+ const contract = read(root, 'packages/api/src/routes/auxillaries-contract.ts');
+ const route = read(root, 'packages/api/src/routes/auxillaries.ts');
+ const contractTest = read(root, 'packages/api/src/routes/__tests__/auxillaries-contract.test.ts');
+ const walletPane = read(root, 'uapi/app/auxillaries/components/AuxillariesWalletPane.tsx');
+ const useUserData = read(root, 'uapi/hooks/useUserData.ts');
+ const useUserDataJs = read(root, 'uapi/hooks/useUserData.js');
+ const walletPaneTest = read(root, 'uapi/tests/auxillariesWalletPane.test.tsx');
+ const userDataRouteTest = read(root, 'uapi/tests/userDataRoute.test.ts');
+ const spec = read(root, 'BITCODE_SPEC_V31.md');
+ const delta = read(root, 'BITCODE_SPEC_V31_DELTA.md');
+ const notes = read(root, 'BITCODE_SPEC_V31_NOTES.md');
+ const parity = read(root, 'BITCODE_SPEC_V31_PARITY_MATRIX.md');
+ const roadmap = read(root, 'SPECIFICATIONS_ROADMAP.md');
+ const apiReadme = read(root, 'packages/api/README.md');
+ const auxReadme = read(root, 'uapi/app/auxillaries/README.md');
+ const btdReadme = read(root, 'packages/btd/README.md');
+ const packageJson = read(root, 'package.json');
+ const workflow = read(root, '.github/workflows/bitcode-gate-quality.yml');
+
+ for (const symbol of [
+ 'BtdWalletBtdSupportProjection',
+ 'buildBtdWalletBtdSupportProjection',
+ 'walletCapabilityRoot',
+ 'noCustody',
+ 'serverCustody',
+ 'networkReadiness',
+ 'ownerReadCount',
+ 'licensedReadCount',
+ 'pendingSettlementCount',
+ 'protectedSourceVisible',
+ 'settlementBlockers',
+ 'not_exchange_market_state',
+ ]) {
+ assertCheck(failures, btdSupport.includes(symbol), `BTD Auxillaries projection must own ${symbol}.`);
+ }
+
+ assertCheck(failures, btdIndex.includes("export * from './auxillaries-support'"), 'BTD package entrypoint must export Auxillaries support projection.');
+
+ for (const symbol of [
+ 'buildBtdWalletBtdSupportProjection',
+ 'AuxillariesWalletBtdPaneState',
+ 'walletCapabilityRoot',
+ 'networkReadiness',
+ 'btdReadRightSummary',
+ 'treasuryScope',
+ 'exchangeMarketState',
+ 'settlementBlockers',
+ ]) {
+ assertCheck(failures, contract.includes(symbol), `Auxillaries route contract must carry ${symbol}.`);
+ }
+
+ for (const routePhrase of [
+ 'read_right_state',
+ 'source_safe_preview_root',
+ 'readRightState',
+ 'sourceSafePreviewRoot',
+ ]) {
+ assertCheck(failures, route.includes(routePhrase), `Auxillaries data route must hydrate ${routePhrase}.`);
+ }
+
+ for (const uiPhrase of [
+ 'walletBtdPaneState',
+ 'auxillaries-wallet-btd-readiness',
+ 'Signer posture',
+ 'Network readiness',
+ 'BTD range cells',
+ 'Read-right mix',
+ 'Settlement readiness',
+ 'Not Exchange',
+ 'protected source visibility fixed false',
+ ]) {
+ assertCheck(
+ failures,
+ walletPane.includes(uiPhrase) || useUserData.includes(uiPhrase) || useUserDataJs.includes(uiPhrase),
+ `Wallet pane/read model must project ${uiPhrase}.`,
+ );
+ }
+
+ for (const testPhrase of [
+ 'buildBtdWalletBtdSupportProjection',
+ 'noCustody',
+ 'serverCustody',
+ 'totalRangeCells',
+ 'ownerReadCount',
+ 'licensedReadCount',
+ 'protectedSourceVisible',
+ 'not_exchange_market_state',
+ 'auxillaries-wallet-btd-readiness',
+ 'Not Exchange',
+ ]) {
+ assertCheck(
+ failures,
+ btdTest.includes(testPhrase) ||
+ contractTest.includes(testPhrase) ||
+ walletPaneTest.includes(testPhrase) ||
+ userDataRouteTest.includes(testPhrase),
+ `Gate 5 tests must cover ${testPhrase}.`,
+ );
+ }
+
+ for (const docPhrase of [
+ 'BtdWalletBtdSupportProjection',
+ 'no-custody signer posture',
+ 'range/read-right',
+ 'protected source',
+ 'not Exchange market state',
+ ]) {
+ assertCheck(
+ failures,
+ spec.includes(docPhrase) ||
+ delta.includes(docPhrase) ||
+ notes.includes(docPhrase) ||
+ parity.includes(docPhrase) ||
+ apiReadme.includes(docPhrase) ||
+ auxReadme.includes(docPhrase) ||
+ btdReadme.includes(docPhrase),
+ `V31 Gate 5 docs/spec must describe ${docPhrase}.`,
+ );
+ }
+
+ assertCheck(
+ failures,
+ /Current working gate: V31 Gate (?:5|6|7|8|9|10)\b/u.test(roadmap),
+ 'Roadmap must track V31 Gate 5 or a later V31 gate as current working gate.',
+ );
+ assertCheck(failures, delta.includes('Gate 5 implementation centers'), 'V31 DELTA must name Gate 5 implementation centers.');
+ assertCheck(failures, notes.includes('Gate 5 closure note'), 'V31 NOTES must include the Gate 5 closure note.');
+ assertCheck(failures, !/\|\s*Wallet panes consume no-custody signer posture\s*\|[^|]*\|\s*pending\s*\|/u.test(parity), 'Gate 5 wallet parity must not remain pending.');
+ assertCheck(failures, !/\|\s*BTD panes consume range\/read-right\/treasury summaries\s*\|[^|]*\|\s*pending\s*\|/u.test(parity), 'Gate 5 BTD parity must not remain pending.');
+ assertCheck(failures, packageJson.includes('"check:v31-gate5"'), 'package.json must expose check:v31-gate5.');
+ assertCheck(failures, workflow.includes('check-v31-gate5-wallet-btd-pane-readiness.mjs'), 'Gate workflow must run the V31 Gate 5 checker.');
+ assertCheck(failures, workflow.includes('auxillariesWalletPane.test.tsx'), 'Gate workflow must run the Wallet pane test.');
+
+ if (failures.length) {
+ process.stderr.write('V31 Gate 5 Wallet/BTD pane readiness check failed:\n');
+ for (const failure of failures) process.stderr.write(`- ${failure}\n`);
+ process.exit(1);
+ }
+
+ process.stdout.write(`V31 Gate 5 Wallet/BTD pane readiness check passed pointer=${pointer}\n`);
+}
+
+main();
diff --git a/uapi/app/auxillaries/README.md b/uapi/app/auxillaries/README.md
index 7320b4589..73c4cdd69 100644
--- a/uapi/app/auxillaries/README.md
+++ b/uapi/app/auxillaries/README.md
@@ -62,6 +62,13 @@ Auxillaries is the V28 prerequisite control plane for Terminal:
roots without exposing raw provider credentials.
- Profile owns optional email/contact/admin data and must not appear as the primary identity requirement. Its readiness summary consumes `profileState` from `/api/auxillaries/data`, including named repair routes for identity, wallet binding, preferences, notifications, and data-sharing posture.
- Wallet also reflects BTD range/share posture after the shared auxillary data read settles.
+- Wallet consumes package-owned `walletBtdPaneState` from
+ `/api/auxillaries/data`, including no-custody signer posture, wallet/network
+ readiness, BTD range cell counts, owner-read/licensed-read/pending/denied
+ read-right posture, account BTC fee treasury posture, settlement blockers,
+ and compact roots. The pane must not show protected AssetPack source before
+ paid unlock and must not treat account treasury support as Exchange market
+ activity.
- The top chrome must show a loading/readiness state until auxillary data determines whether a wallet exists; it must not briefly show `Connect Wallet` during unresolved connection reads.
- QA builds should enable `NEXT_PUBLIC_BITCODE_QA_VERBOSE=true` and `BITCODE_QA_VERBOSE=true` to trace client/server identity synchronization without logging secrets.
diff --git a/uapi/app/auxillaries/components/AuxillariesWalletPane.tsx b/uapi/app/auxillaries/components/AuxillariesWalletPane.tsx
index 669254099..be4557003 100644
--- a/uapi/app/auxillaries/components/AuxillariesWalletPane.tsx
+++ b/uapi/app/auxillaries/components/AuxillariesWalletPane.tsx
@@ -23,6 +23,7 @@ import AuxillariesPreferenceCards, {
} from "@/app/auxillaries/components/shared/AuxillariesPreferenceCards";
import AuxillariesStatGrid from "@/app/auxillaries/components/shared/AuxillariesStatGrid";
import AuxillariesWorkspaceSection from "@/app/auxillaries/components/shared/AuxillariesWorkspaceSection";
+import type { AuxillariesWalletBtdPaneState } from "@/app/auxillaries/auxillary-onboarding-contract";
export interface AuxillariesWalletPaneProps {
onSave: (data: any) => void;
@@ -140,6 +141,18 @@ function formatPolicyHash(hash: string | null) {
return hash.length > 18 ? `${hash.slice(0, 10)}...${hash.slice(-6)}` : hash;
}
+function formatReadiness(value: string | null | undefined) {
+ if (!value) {
+ return "Unknown";
+ }
+
+ return value
+ .split("_")
+ .filter(Boolean)
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
+ .join(" ");
+}
+
function resolveBtdAccessDisclosure(profile: Record | null) {
const policyId = readProfileString(
profile,
@@ -199,6 +212,7 @@ export default function AuxillariesWalletPane({
btdBalance = 0,
btcFeeBalance = null,
recentBtdAssetPacks = [],
+ walletBtdPaneState,
hasWalletConnection,
hasStoredVerifiedWalletConnection = false,
hasVerifiedWalletConnection,
@@ -210,6 +224,14 @@ export default function AuxillariesWalletPane({
const walletBinding = readBitcodeWalletBindingFromProfile(profile);
const btcFeeBalanceSource = btcFeeBalance ?? profile?.btc_balance;
const accessDisclosure = resolveBtdAccessDisclosure(profile);
+ const walletSupport = (walletBtdPaneState || null) as AuxillariesWalletBtdPaneState | null;
+ const supportWalletCapability = walletSupport?.walletCapability ?? null;
+ const supportSignerPosture = walletSupport?.signerPosture ?? null;
+ const supportNetworkReadiness = walletSupport?.networkReadiness ?? null;
+ const supportReadRights = walletSupport?.btdReadRightSummary ?? null;
+ const supportTreasury = walletSupport?.treasurySummary ?? null;
+ const supportSettlementReadiness = walletSupport?.settlementReadiness ?? null;
+ const displayBtdBalance = supportReadRights?.aggregateBtd ?? btdBalance;
const hasReadableBtcFeeBalance =
typeof btcFeeBalanceSource === "number" ||
(typeof btcFeeBalanceSource === "string" && Number.isFinite(Number(btcFeeBalanceSource)));
@@ -245,7 +267,7 @@ export default function AuxillariesWalletPane({
}));
}, [savedPreferences]);
- const ownedAssetPackCount = recentBtdAssetPacks.length;
+ const ownedAssetPackCount = supportReadRights?.assetPackCount ?? recentBtdAssetPacks.length;
const ownedAssetPackSummary =
ownedAssetPackCount === 1
? "1 AssetPack"
@@ -466,7 +488,7 @@ export default function AuxillariesWalletPane({
BTD balance
- {formatBtdHoldings(btdBalance)}
+ {formatBtdHoldings(displayBtdBalance)}
@@ -481,8 +503,10 @@ export default function AuxillariesWalletPane({
},
{
label: "BTC in wallet",
- value: formatBtcFeeBalance(btcFeeBalanceSource),
- detail: hasReadableBtcFeeBalance
+ value: formatBtcFeeBalance(supportTreasury?.btcFeeBalance ?? btcFeeBalanceSource),
+ detail: supportTreasury?.organizationTreasurySeparated
+ ? "Account treasury posture is source-safe and separate from organization treasury controls and Exchange market state."
+ : hasReadableBtcFeeBalance
? "Live BTC fee-liquidity posture supplied by the connected wallet posture."
: hasStoredVerifiedWalletConnection && !hasVerifiedWalletConnection
? "Saved verified wallet-provider signer posture exists, but the live signer session needs reconnect before signed settlement or refreshed BTC posture can resume."
@@ -541,6 +565,60 @@ export default function AuxillariesWalletPane({
columns={4}
/>
+
+
+
+
+ Support root {walletSupport?.btdSupportRoot ? formatPolicyHash(walletSupport.btdSupportRoot) : "pending"}.
+ Wallet root {supportWalletCapability?.walletCapabilityRoot ? ` ${formatPolicyHash(supportWalletCapability.walletCapabilityRoot)}` : " pending"}.
+
+
{
{ id: 'tm-2', display_name: 'Sora Ames', role: 'lead' },
],
},
+ walletBtdPaneState: {
+ walletCapability: {
+ hasBinding: true,
+ provider: 'leather',
+ address: 'bc1qbitcodeoperator',
+ verificationState: 'verified',
+ network: 'testnet',
+ noCustody: true,
+ serverCustody: false,
+ capabilities: ['message_sign', 'psbt_sign', 'rights_transfer'],
+ walletCapabilityRoot: 'wallet-capability-root-abcdef1234567890',
+ },
+ signerPosture: {
+ ready: true,
+ state: 'verified',
+ requiredAction: 'none',
+ canSignPsbt: true,
+ canSignRightsTransfer: true,
+ serverCustody: false,
+ },
+ networkReadiness: {
+ state: 'ready',
+ network: 'testnet',
+ requiredAction: 'none',
+ blocker: null,
+ },
+ btdReadRightSummary: {
+ aggregateBtd: 1200,
+ assetPackCount: 2,
+ recentAssetPackIds: ['asset-pack-1', 'asset-pack-2'],
+ rangeCount: 2,
+ totalRangeCells: 12,
+ ownerReadCount: 1,
+ licensedReadCount: 1,
+ pendingSettlementCount: 0,
+ deniedCount: 0,
+ unknownCount: 0,
+ protectedSourceVisible: false,
+ sourceSafePreviewRoots: ['source-safe-preview-root'],
+ },
+ treasurySummary: {
+ btcFeeBalance: 0.125,
+ feeAsset: 'BTC',
+ noCustody: true,
+ treasuryScope: 'account',
+ organizationTreasurySeparated: true,
+ exchangeMarketState: 'not_exchange_market_state',
+ },
+ settlementReadiness: 'ready',
+ settlementBlockers: [],
+ sourceSafetyClass: 'source_safe',
+ btdSupportRoot: 'btd-support-root-abcdef1234567890',
+ },
modelPreferences: {
existingSetting: 'keep-me',
btdDefaults: {
@@ -95,6 +148,59 @@ describe('AuxillariesWalletPane', () => {
},
hasGitHubConnection: true,
btdBalance: 1200,
+ walletBtdPaneState: {
+ walletCapability: {
+ hasBinding: true,
+ provider: 'leather',
+ address: 'bc1qbitcodeoperator',
+ verificationState: 'verified',
+ network: 'testnet',
+ noCustody: true,
+ serverCustody: false,
+ capabilities: ['message_sign', 'psbt_sign', 'rights_transfer'],
+ walletCapabilityRoot: 'wallet-capability-root-abcdef1234567890',
+ },
+ signerPosture: {
+ ready: true,
+ state: 'verified',
+ requiredAction: 'none',
+ canSignPsbt: true,
+ canSignRightsTransfer: true,
+ serverCustody: false,
+ },
+ networkReadiness: {
+ state: 'ready',
+ network: 'testnet',
+ requiredAction: 'none',
+ blocker: null,
+ },
+ btdReadRightSummary: {
+ aggregateBtd: 1200,
+ assetPackCount: 2,
+ recentAssetPackIds: ['asset-pack-1', 'asset-pack-2'],
+ rangeCount: 2,
+ totalRangeCells: 12,
+ ownerReadCount: 1,
+ licensedReadCount: 1,
+ pendingSettlementCount: 0,
+ deniedCount: 0,
+ unknownCount: 0,
+ protectedSourceVisible: false,
+ sourceSafePreviewRoots: ['source-safe-preview-root'],
+ },
+ treasurySummary: {
+ btcFeeBalance: 0.125,
+ feeAsset: 'BTC',
+ noCustody: true,
+ treasuryScope: 'account',
+ organizationTreasurySeparated: true,
+ exchangeMarketState: 'not_exchange_market_state',
+ },
+ settlementReadiness: 'ready',
+ settlementBlockers: [],
+ sourceSafetyClass: 'source_safe',
+ btdSupportRoot: 'btd-support-root-abcdef1234567890',
+ },
isLoading: false,
error: null,
refresh: jest.fn(),
@@ -125,6 +231,14 @@ describe('AuxillariesWalletPane', () => {
expect(screen.getByTestId('wallet-step-badge')).toHaveTextContent('Auxillary step 1');
expect(screen.getByText(/Keep BTC fees, BTD holdings, and wallet identity readable together/i)).toBeTruthy();
expect(screen.getByText('Access policy')).toBeInTheDocument();
+ expect(screen.getByTestId('auxillaries-wallet-btd-readiness')).toBeInTheDocument();
+ expect(screen.getByText('Signer posture')).toBeInTheDocument();
+ expect(screen.getByText('Network readiness')).toBeInTheDocument();
+ expect(screen.getByText('BTD range cells')).toBeInTheDocument();
+ expect(screen.getByText('12')).toBeInTheDocument();
+ expect(screen.getByText('1 owner / 1 licensed')).toBeInTheDocument();
+ expect(screen.getByText('Settlement readiness')).toBeInTheDocument();
+ expect(screen.getByText('Not Exchange')).toBeInTheDocument();
expect(screen.getByText('policy-main')).toBeInTheDocument();
expect(screen.getByText('policy-has...567890')).toBeInTheDocument();
expect(screen.getByText('1,200-1,211')).toBeInTheDocument();