@unicitylabs/sphere-sdk 0.9.2-dev.0 → 0.9.2-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/index.cjs +107 -36
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.js +107 -36
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +107 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +107 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -10552,6 +10552,14 @@ declare class RemoteTokenStorageProvider<TData extends TxfStorageDataBase = TxfS
|
|
|
10552
10552
|
private hashValue;
|
|
10553
10553
|
/** Live (non-tombstoned) entry count in the internal last-known state (tests/diagnostics). */
|
|
10554
10554
|
knownCount(): number;
|
|
10555
|
+
/**
|
|
10556
|
+
* True if the vault's last-known view has this token's wireKey TOMBSTONED — i.e.
|
|
10557
|
+
* another device SPENT it. PaymentsModule.load uses this to DROP a stale local
|
|
10558
|
+
* token the vault marked spent, so the `delete-resurrect` sync path can't
|
|
10559
|
+
* re-create it and inflate the balance (finding: vault-cross-device-resurrect).
|
|
10560
|
+
* `plainKey` is the genesis tokenId (v2TokenId === engine.tokenId === keyFromTokenId base).
|
|
10561
|
+
*/
|
|
10562
|
+
isPlainKeyDeleted(plainKey: string): boolean;
|
|
10555
10563
|
/** Snapshot the internal last-known state as a merkle `EntryState` map. */
|
|
10556
10564
|
private knownAsEntryState;
|
|
10557
10565
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10552,6 +10552,14 @@ declare class RemoteTokenStorageProvider<TData extends TxfStorageDataBase = TxfS
|
|
|
10552
10552
|
private hashValue;
|
|
10553
10553
|
/** Live (non-tombstoned) entry count in the internal last-known state (tests/diagnostics). */
|
|
10554
10554
|
knownCount(): number;
|
|
10555
|
+
/**
|
|
10556
|
+
* True if the vault's last-known view has this token's wireKey TOMBSTONED — i.e.
|
|
10557
|
+
* another device SPENT it. PaymentsModule.load uses this to DROP a stale local
|
|
10558
|
+
* token the vault marked spent, so the `delete-resurrect` sync path can't
|
|
10559
|
+
* re-create it and inflate the balance (finding: vault-cross-device-resurrect).
|
|
10560
|
+
* `plainKey` is the genesis tokenId (v2TokenId === engine.tokenId === keyFromTokenId base).
|
|
10561
|
+
*/
|
|
10562
|
+
isPlainKeyDeleted(plainKey: string): boolean;
|
|
10555
10563
|
/** Snapshot the internal last-known state as a merkle `EntryState` map. */
|
|
10556
10564
|
private knownAsEntryState;
|
|
10557
10565
|
}
|
package/dist/index.js
CHANGED
|
@@ -3068,9 +3068,14 @@ function planOps(params) {
|
|
|
3068
3068
|
ops.push({ plainKey, wireKey: wk, baseVersion: cur.version, sealVersion: cur.version + 1, isDelete: false, value });
|
|
3069
3069
|
}
|
|
3070
3070
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3071
|
+
const sweptWire = /* @__PURE__ */ new Set();
|
|
3072
|
+
for (const tokenId of params.tombstonedTokenIds ?? []) {
|
|
3073
|
+
const wk = wireKeyOf(tokenId);
|
|
3074
|
+
if (sweptWire.has(wk)) continue;
|
|
3075
|
+
sweptWire.add(wk);
|
|
3076
|
+
const cur = known.get(wk);
|
|
3077
|
+
if (cur && !cur.deleted && !liveWire.has(wk) && !reserved?.has(wk)) {
|
|
3078
|
+
ops.push({ plainKey: tokenId, wireKey: wk, baseVersion: cur.version, sealVersion: cur.version + 1, isDelete: true });
|
|
3074
3079
|
}
|
|
3075
3080
|
}
|
|
3076
3081
|
return ops;
|
|
@@ -3182,10 +3187,13 @@ var LoadDeltaTracker = class {
|
|
|
3182
3187
|
*/
|
|
3183
3188
|
async beginLoad(known) {
|
|
3184
3189
|
this.baseline = await this.loadBaseline();
|
|
3185
|
-
this.
|
|
3186
|
-
|
|
3187
|
-
this.baseline
|
|
3190
|
+
if (this.baseline) {
|
|
3191
|
+
const entries = new Map(this.baseline.entries ?? []);
|
|
3192
|
+
const sigOk = verifyRoot(this.bindingFor(this.baseline.cursor, this.baseline.root), this.baseline.sig, this.ownerPubForVerify());
|
|
3193
|
+
const entriesOk = (this.baseline.entries?.length ?? 0) > 0 && computeRoot(entries) === this.baseline.root;
|
|
3194
|
+
if (!sigOk || !entriesOk) this.baseline = null;
|
|
3188
3195
|
}
|
|
3196
|
+
this.accState = this.baseline ? new Map(this.baseline.entries ?? []) : new Map(known);
|
|
3189
3197
|
}
|
|
3190
3198
|
/**
|
|
3191
3199
|
* Re-arm the tracker for a SANCTIONED-RESET re-pass (Task 8.2). After a verified
|
|
@@ -3217,7 +3225,7 @@ var LoadDeltaTracker = class {
|
|
|
3217
3225
|
if (!mono.ok) return mono;
|
|
3218
3226
|
this.fold(page.entries);
|
|
3219
3227
|
if (this.baseline) {
|
|
3220
|
-
const verdict = this.
|
|
3228
|
+
const verdict = this.checkRegression();
|
|
3221
3229
|
if (!verdict.ok) return verdict;
|
|
3222
3230
|
}
|
|
3223
3231
|
return { ok: true, reset: false };
|
|
@@ -3253,14 +3261,22 @@ var LoadDeltaTracker = class {
|
|
|
3253
3261
|
this.accState = state;
|
|
3254
3262
|
}
|
|
3255
3263
|
/**
|
|
3256
|
-
*
|
|
3257
|
-
*
|
|
3258
|
-
*
|
|
3264
|
+
* Multi-device anti-rollback (finding: vault-anti-rollback-single-writer). Alarm
|
|
3265
|
+
* ONLY on a REGRESSION of a committed entry — a baseline key now MISSING from, or
|
|
3266
|
+
* at a LOWER version than, the folded server state. New keys and higher versions
|
|
3267
|
+
* are legitimate FORWARD progress (this OR another device wrote them), never a
|
|
3268
|
+
* rollback. Vault versions are strictly monotonic per key (create→update→delete→
|
|
3269
|
+
* resurrect all increment), so a version going backwards (or a known entry
|
|
3270
|
+
* vanishing) is the rollback signature, independent of which device authored the
|
|
3271
|
+
* forward writes. (The old root-EQUALITY check assumed a SINGLE writer and so
|
|
3272
|
+
* false-alarmed on every peer-device write — which broke multi-device sync.)
|
|
3259
3273
|
*/
|
|
3260
|
-
|
|
3261
|
-
const
|
|
3262
|
-
|
|
3263
|
-
|
|
3274
|
+
checkRegression() {
|
|
3275
|
+
for (const [key, base] of this.baseline.entries ?? []) {
|
|
3276
|
+
const cur = this.accState.get(key);
|
|
3277
|
+
if (!cur || cur.version < base.version) {
|
|
3278
|
+
return { ok: false, reason: "rollback" };
|
|
3279
|
+
}
|
|
3264
3280
|
}
|
|
3265
3281
|
return { ok: true };
|
|
3266
3282
|
}
|
|
@@ -3280,9 +3296,10 @@ var LoadDeltaTracker = class {
|
|
|
3280
3296
|
*/
|
|
3281
3297
|
async commitBaseline(cursor, epoch, _epochSig) {
|
|
3282
3298
|
if (this.config.baseline && this.walletPriv) {
|
|
3283
|
-
const
|
|
3299
|
+
const acc = this.accState ?? /* @__PURE__ */ new Map();
|
|
3300
|
+
const root = computeRoot(acc);
|
|
3284
3301
|
const sig = signRoot(this.walletPriv, this.bindingFor(cursor, root));
|
|
3285
|
-
const baseline = { cursor, root, sig, epoch };
|
|
3302
|
+
const baseline = { cursor, root, sig, epoch, entries: [...acc] };
|
|
3286
3303
|
await this.config.baseline.set(this.baselineKey(), JSON.stringify(baseline));
|
|
3287
3304
|
}
|
|
3288
3305
|
this.accState = null;
|
|
@@ -3299,7 +3316,7 @@ var LoadDeltaTracker = class {
|
|
|
3299
3316
|
const root = computeRoot(known);
|
|
3300
3317
|
const epoch = this.baseline?.epoch ?? 0;
|
|
3301
3318
|
const sig = signRoot(this.walletPriv, this.bindingFor(cursor, root));
|
|
3302
|
-
const baseline = { cursor, root, sig, epoch };
|
|
3319
|
+
const baseline = { cursor, root, sig, epoch, entries: [...known] };
|
|
3303
3320
|
this.baseline = baseline;
|
|
3304
3321
|
await this.config.baseline.set(this.baselineKey(), JSON.stringify(baseline));
|
|
3305
3322
|
}
|
|
@@ -3547,7 +3564,13 @@ var RemoteTokenStorageProvider = class {
|
|
|
3547
3564
|
this.wakeClient = new VaultWakeClient({
|
|
3548
3565
|
vaultUrl: this.config.vaultUrl,
|
|
3549
3566
|
auth: this.auth,
|
|
3550
|
-
|
|
3567
|
+
// Only wake when this provider is actually connected. A stale wake (e.g. fired
|
|
3568
|
+
// after shutdown/identity-switch, before stop() lands) must NOT drive a receive/
|
|
3569
|
+
// sync against torn-down wallet storage ("IndexedDBStorageProvider not connected").
|
|
3570
|
+
onWake: () => {
|
|
3571
|
+
if (this.status !== "connected") return;
|
|
3572
|
+
this.emit("storage:remote-updated", { source: "vault-wake" });
|
|
3573
|
+
}
|
|
3551
3574
|
});
|
|
3552
3575
|
this.wakeClient.start();
|
|
3553
3576
|
}
|
|
@@ -3631,11 +3654,13 @@ var RemoteTokenStorageProvider = class {
|
|
|
3631
3654
|
}
|
|
3632
3655
|
/** Diff the TXF snapshot vs internal last-known state into a list of CAS ops. */
|
|
3633
3656
|
planFlush(localData) {
|
|
3657
|
+
const tombstones = localData._tombstones ?? [];
|
|
3634
3658
|
return planOps({
|
|
3635
3659
|
tokens: extractTokens(localData),
|
|
3636
3660
|
known: this.known,
|
|
3637
3661
|
wireKeyOf: (plainKey) => this.wireKeyFor(plainKey),
|
|
3638
|
-
changed: (wk, value) => this.hasChanged(wk, value)
|
|
3662
|
+
changed: (wk, value) => this.hasChanged(wk, value),
|
|
3663
|
+
tombstonedTokenIds: tombstones.map((t) => t.tokenId)
|
|
3639
3664
|
});
|
|
3640
3665
|
}
|
|
3641
3666
|
hasChanged(wk, value) {
|
|
@@ -4047,6 +4072,16 @@ var RemoteTokenStorageProvider = class {
|
|
|
4047
4072
|
for (const e of this.known.values()) if (!e.deleted) n += 1;
|
|
4048
4073
|
return n;
|
|
4049
4074
|
}
|
|
4075
|
+
/**
|
|
4076
|
+
* True if the vault's last-known view has this token's wireKey TOMBSTONED — i.e.
|
|
4077
|
+
* another device SPENT it. PaymentsModule.load uses this to DROP a stale local
|
|
4078
|
+
* token the vault marked spent, so the `delete-resurrect` sync path can't
|
|
4079
|
+
* re-create it and inflate the balance (finding: vault-cross-device-resurrect).
|
|
4080
|
+
* `plainKey` is the genesis tokenId (v2TokenId === engine.tokenId === keyFromTokenId base).
|
|
4081
|
+
*/
|
|
4082
|
+
isPlainKeyDeleted(plainKey) {
|
|
4083
|
+
return this.known.get(this.wireKeyFor(plainKey))?.deleted === true;
|
|
4084
|
+
}
|
|
4050
4085
|
/** Snapshot the internal last-known state as a merkle `EntryState` map. */
|
|
4051
4086
|
knownAsEntryState() {
|
|
4052
4087
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -13619,26 +13654,52 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
13619
13654
|
await TokenRegistry.waitForReady();
|
|
13620
13655
|
const providers = this.getTokenStorageProviders();
|
|
13621
13656
|
const ownAddresses = await this.ownMetaAddresses();
|
|
13657
|
+
let mergedData = null;
|
|
13658
|
+
const mergedHistory = [];
|
|
13659
|
+
const tombKeys = /* @__PURE__ */ new Set();
|
|
13622
13660
|
for (const [id, provider] of providers) {
|
|
13623
|
-
|
|
13624
|
-
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13629
|
-
|
|
13630
|
-
|
|
13631
|
-
|
|
13632
|
-
|
|
13633
|
-
|
|
13634
|
-
|
|
13635
|
-
|
|
13661
|
+
const result = await provider.load().catch((err) => {
|
|
13662
|
+
logger.error("Payments", `Failed to load from provider ${id}:`, err);
|
|
13663
|
+
return null;
|
|
13664
|
+
});
|
|
13665
|
+
if (!result || !result.success || !result.data) continue;
|
|
13666
|
+
const data = result.data;
|
|
13667
|
+
if (!this.isOwnAddress(data?._meta?.address, ownAddresses)) {
|
|
13668
|
+
logger.warn("Payments", `Load: rejecting data from provider ${id} \u2014 address mismatch (got=${data._meta?.address?.slice(0, 20)}... expected one of this wallet's addresses)`);
|
|
13669
|
+
continue;
|
|
13670
|
+
}
|
|
13671
|
+
if (!mergedData) mergedData = { _meta: data._meta };
|
|
13672
|
+
for (const [k, v] of Object.entries(data)) {
|
|
13673
|
+
if (k === "_meta" || k === "_history" || k === "_tombstones") continue;
|
|
13674
|
+
if (!(k in mergedData)) mergedData[k] = v;
|
|
13675
|
+
}
|
|
13676
|
+
for (const t of data._tombstones ?? []) {
|
|
13677
|
+
const tk = `${t.tokenId}:${t.stateHash}`;
|
|
13678
|
+
if (tombKeys.has(tk)) continue;
|
|
13679
|
+
tombKeys.add(tk);
|
|
13680
|
+
(mergedData._tombstones ??= []).push(t);
|
|
13681
|
+
}
|
|
13682
|
+
if (data._history?.length) mergedHistory.push(...data._history);
|
|
13683
|
+
logger.debug("Payments", `Merged token data from provider ${id}`);
|
|
13684
|
+
}
|
|
13685
|
+
if (mergedData) {
|
|
13686
|
+
const deletionCheckers = [...providers.values()].filter(
|
|
13687
|
+
(p) => typeof p.isPlainKeyDeleted === "function"
|
|
13688
|
+
);
|
|
13689
|
+
if (deletionCheckers.length > 0) {
|
|
13690
|
+
for (const k of Object.keys(mergedData)) {
|
|
13691
|
+
if (!/^_[0-9a-f]{64}$/i.test(k)) continue;
|
|
13692
|
+
const plainKey = k.slice(1);
|
|
13693
|
+
if (deletionCheckers.some((p) => p.isPlainKeyDeleted(plainKey))) {
|
|
13694
|
+
delete mergedData[k];
|
|
13695
|
+
logger.debug("Payments", `load(): dropping ${plainKey.slice(0, 12)}\u2026 \u2014 vault marks it spent (cross-device)`);
|
|
13636
13696
|
}
|
|
13637
|
-
logger.debug("Payments", `Loaded metadata from provider ${id}`);
|
|
13638
|
-
break;
|
|
13639
13697
|
}
|
|
13640
|
-
}
|
|
13641
|
-
|
|
13698
|
+
}
|
|
13699
|
+
this.loadFromStorageData(mergedData);
|
|
13700
|
+
await this.rebuildParsedTokenCache();
|
|
13701
|
+
if (mergedHistory.length > 0) {
|
|
13702
|
+
await this.importRemoteHistoryEntries(mergedHistory);
|
|
13642
13703
|
}
|
|
13643
13704
|
}
|
|
13644
13705
|
for (const [id, token] of this.tokens) {
|
|
@@ -15452,12 +15513,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
15452
15513
|
}
|
|
15453
15514
|
this.syncDebounceTimer = setTimeout(() => {
|
|
15454
15515
|
this.syncDebounceTimer = null;
|
|
15516
|
+
const storage = this.deps?.storage;
|
|
15517
|
+
if (storage?.isConnected && !storage.isConnected()) {
|
|
15518
|
+
logger.debug("Payments", "Remote-update wake ignored \u2014 wallet storage not connected");
|
|
15519
|
+
return;
|
|
15520
|
+
}
|
|
15455
15521
|
void (async () => {
|
|
15456
15522
|
try {
|
|
15457
15523
|
await this.receive();
|
|
15458
15524
|
} catch (err) {
|
|
15459
15525
|
logger.debug("Payments", "Auto-receive from remote update failed:", err);
|
|
15460
15526
|
}
|
|
15527
|
+
try {
|
|
15528
|
+
await this.load();
|
|
15529
|
+
} catch (err) {
|
|
15530
|
+
logger.debug("Payments", "Auto-load (reconcile) from remote update failed:", err);
|
|
15531
|
+
}
|
|
15461
15532
|
try {
|
|
15462
15533
|
const result = await this.sync();
|
|
15463
15534
|
const data = eventData;
|