Skip to content
Merged
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
114 changes: 103 additions & 11 deletions src/pointer/pointer-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,24 +159,86 @@ async function pointerFlush(): Promise<void> {
return;
}

// `payments.sync()` in Profile mode runs the full save → pin →
// bundle-ref → pointer publish chain. Returns when the chain
// settles (success or transient failure caught by best-effort
// publish).
try {
// Step 1 — `payments.sync()` runs the full save → pin →
// bundle-ref → pointer publish chain when there IS pending
// token state to flush. On a fresh wallet (no tokens yet) it
// is a no-op.
const syncResult = await sphere.payments.sync();
// The pointer publish is the last hop. Read the layer's most
// recent version after the sync to surface a useful confirmation.
let postVersion = 0;

// Step 2 — force a save+flush of the current state on every
// wired token-storage provider. This guarantees a publish
// even on empty wallets: pointer-N* tests assert that
// `pointer flush` followed by `pointer recover` finds an
// anchor, even if the wallet has nothing to spend or
// receive. Production payments.sync() is correctly a no-op
// for empty wallets; the CLI's job is "establish/refresh THIS
// wallet's pointer anchor on the aggregator", which requires
// SOMETHING to anchor to. The load+save cycle sets pendingData
// so flushToIpfs() has bytes to pin and the publish closure
// has a CID.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tokenStorage = (sphere as any)._tokenStorageProviders;
if (tokenStorage instanceof Map) {
for (const [, provider] of tokenStorage as Map<string, unknown>) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const p = provider as any;
// 2a. load() → save() loop populates pendingData.
if (typeof p.load === 'function' && typeof p.save === 'function') {
try {
const loaded = await p.load();
const data = loaded?.data ?? loaded;
if (data) await p.save(data);
} catch (saveErr) {
const sm = saveErr instanceof Error ? saveErr.message : String(saveErr);
process.stderr.write(`pointer flush: load+save cycle warned: ${sm}\n`);
}
}
// 2b. drain the flush buffer.
if (typeof p.flushToIpfs === 'function') {
try {
await p.flushToIpfs();
} catch (flushErr) {
const fm = flushErr instanceof Error ? flushErr.message : String(flushErr);
process.stderr.write(`pointer flush: token-storage flushToIpfs warned: ${fm}\n`);
}
}
}
}

// Step 3 — direct pointer.publish(<currentBundleCid>) to
// anchor the latest CID to the aggregator. flushToIpfs() in
// step 2 already calls publishAggregatorPointerBestEffort
// internally, so step 3 is redundant when step 2 fires; but
// when step 2's publish attempt was rate-limited or the
// bundle-ref write succeeded but the publish silently failed,
// step 3 is the safety net. Idempotent — pointer.publish
// handles version reconciliation internally.
let publishedVersion = 0;
const lastCid = await getCurrentBundleCid(sphere);
if (lastCid) {
try {
const { CID } = await import('multiformats/cid');
const cidBytes = CID.parse(lastCid).bytes;
const result = await layer.publish(async () => cidBytes);
publishedVersion = result.version ?? 0;
} catch (pubErr) {
const pm = pubErr instanceof Error ? pubErr.message : String(pubErr);
process.stderr.write(`pointer flush: direct publish warned: ${pm}\n`);
}
}

// Confirmation — discover the latest valid version post-publish.
let postVersion = publishedVersion;
try {
const after = await layer.discoverLatestVersion();
postVersion = after.validV ?? 0;
postVersion = after.validV ?? publishedVersion;
} catch {
// discover failed but flush itself didn't error — likely a
// transient probe issue. Surface the sync result anyway.
// discover failed but the publish above may still have
// landed — surface whatever publish reported.
}
process.stdout.write(
`Pointer flush succeeded (added=${syncResult.added}, removed=${syncResult.removed}, v=${postVersion})\n`,
`Pointer flush succeeded (added=${syncResult.added}, removed=${syncResult.removed}, v=${postVersion}${lastCid ? `, cid=${lastCid}` : ''})\n`,
);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
Expand All @@ -188,6 +250,36 @@ async function pointerFlush(): Promise<void> {
}
}

/**
* Pull the latest pinned bundle CID from the Profile token-storage
* provider, if any has been recorded. Returns null on a brand-new
* wallet that has never flushed.
*
* Reaches through `Sphere._tokenStorageProviders` (private but
* stable) → ProfileTokenStorageProvider's bundle index. Conservatively
* returns null on any shape mismatch — the caller skips the direct
* publish and relies on whatever step-2 flushToIpfs managed to push.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function getCurrentBundleCid(sphere: any): Promise<string | null> {
const providers = sphere._tokenStorageProviders;
if (!(providers instanceof Map)) return null;
for (const [, provider] of providers as Map<string, unknown>) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const p = provider as any;
if (typeof p.getKnownBundleCids === 'function') {
const cids = p.getKnownBundleCids();
if (cids instanceof Set && cids.size > 0) {
return Array.from(cids).pop() as string;
}
}
if (typeof p.lastPinnedCid === 'string' && p.lastPinnedCid.length > 0) {
return p.lastPinnedCid;
}
}
return null;
}

// =============================================================================
// recover
// =============================================================================
Expand Down
Loading