aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-01 09:00:18 -0700
committerAdam Soltys <[email protected]>2026-08-01 09:00:18 -0700
commitd0fc8569c478111bfa5da90570ee3a0ac449a60d (patch)
tree9d15eb0cc1bc5185cb5181fed31d45988be41284 /src
parent0953013271fd9fbc857d3da94a5d65662423b806 (diff)
downloadcoinosv3-d0fc8569c478111bfa5da90570ee3a0ac449a60d.tar.gz
coinosv3-d0fc8569c478111bfa5da90570ee3a0ac449a60d.zip
Never render another ASP's Ark state before connecting
The previous fix still let a phantom balance through: loadArkState fell back to the pre-namespacing key when a namespace was empty, and the UI renders from that before any server is contacted — so the guard, which runs on connect, never got a say. loadArkState now returns only the namespaced state. Legacy state is claimed through adoptArkState(serverPubkey), which proves ownership first: from the state's own stamp, or failing that by decoding a vtxo, whose wire format names its cosigner. No network call, and it cannot guess wrong. On a match the state moves into the namespace and the legacy key is retired; on a mismatch it is left untouched for its real owner. Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/ark/manager.js7
-rw-r--r--src/features/ark.js35
2 files changed, 35 insertions, 7 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js
index 4814a26..03e8e18 100644
--- a/src/ark/manager.js
+++ b/src/ark/manager.js
@@ -103,6 +103,13 @@ export class ArkManager {
this.psa = hs?.psa;
this.serverPub = hex.decode(this.info.serverPubkey);
+ // Pre-namespacing state can now be proven to belong to this server (the
+ // vtxo wire format names its cosigner), so claim it before deciding.
+ if (!this.state.serverPubkey && this.storage.adopt) {
+ const adopted = this.storage.adopt(this.info.serverPubkey);
+ if (adopted) this.state = adopted;
+ }
+
// A vtxo is only meaningful against the server that cosigned it. State
// carried over from a different ASP (a provider switch, or a snapshot
// adopted from the pre-namespacing key) would show phantom balance and
diff --git a/src/features/ark.js b/src/features/ark.js
index 27dd871..b5c2cd9 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -46,15 +46,35 @@ export function installArkWallet(wallet) {
_arkKey(url) { return this._cacheKey() + ':ark:' + this._arkNs(url); },
_arkLegacyKey() { return this._cacheKey() + ':ark'; },
loadArkState(url) {
+ // NB deliberately no fallback to the legacy key: the UI renders from
+ // this before any server is contacted, so returning another ASP's state
+ // here is exactly how you get a phantom balance. Pre-namespacing state
+ // is claimed via adoptArkState() once we know who cosigned it.
try {
const cur = localStorage.getItem(this._arkKey(url));
- if (cur) return JSON.parse(cur);
- // One-time adoption of pre-namespacing state. The manager verifies the
- // server pubkey on connect and discards it if it belongs elsewhere,
- // so a wrong guess here is self-correcting; the legacy key is left in
- // place so the rightful ASP can still claim it.
- const legacy = localStorage.getItem(this._arkLegacyKey());
- return legacy ? JSON.parse(legacy) : null;
+ return cur ? JSON.parse(cur) : null;
+ } catch { return null; }
+ },
+ // Offer up pre-namespacing state, but only to the ASP that actually
+ // cosigned it. Its owner is read from the state's own stamp, or failing
+ // that decoded straight out of a vtxo — the vtxo wire format carries the
+ // server pubkey, so this needs no network call and cannot guess wrong.
+ adoptArkState(serverPubkey, url) {
+ try {
+ const raw = localStorage.getItem(this._arkLegacyKey());
+ if (!raw) return null;
+ const st = JSON.parse(raw);
+ let owner = st.serverPubkey || null;
+ if (!owner) {
+ const v = (st.vtxos || []).find((x) => x.bytes);
+ if (v) { try { owner = decodeVtxo(hex.decode(v.bytes)).serverPubkey; } catch {} }
+ }
+ if (owner && owner !== serverPubkey) return null; // belongs to another ASP
+ if (!owner) return null; // can't prove ownership: leave it
+ st.serverPubkey = owner;
+ this.saveArkState(st, url);
+ localStorage.removeItem(this._arkLegacyKey()); // now safely namespaced
+ return st;
} catch { return null; }
},
saveArkState(state, url) {
@@ -218,6 +238,7 @@ export function arkFeature(ctx) {
account: wallet.account(),
storage: {
load: () => wallet.loadArkState(cfg.ark),
+ adopt: (serverPubkey) => wallet.adoptArkState(serverPubkey, cfg.ark),
// merge-on-save: the MANAGER is authoritative for the state of the
// vtxos it tracks (its reconcile/spends must persist), so its state
// is the first arg and wins; storage only contributes vtxos the