Skip to content

feat(ecurrency): ledger → eVault sync script that verifies against each eVault - #1201

Merged
coodos merged 1 commit into
mainfrom
feat/ecurrency-ledger-sync
Oct 15, 2026
Merged

coodos merged 1 commit into
mainfrom
feat/ecurrency-ledger-sync

Conversation

@coodos

@coodos coodos commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Why

The live ledger sync is fire-and-forget: each row is pushed 3s after insert and again after its hash is saved, failures are only logged, and a restart inside that window drops it. On the network today 590 of 786 eCurrency ledger copies carry no hash, and 40 transfers exist in only one party's vault.

What

pnpm sync:ledger (platforms/ecurrency/api/src/scripts/sync-ledger.ts):

  • Gets a platform token from the Registry's /platforms/certification (as VITE_ECURRENCY_BASE_URL, like the live adapter).
  • For every ledger row, reads each party's eVault over GraphQL (sender and receiver, same rule as the subscriber) and classifies the copy as synced, missing, or outdated (present without its hash). It does not trust the mapping DB.
  • --apply creates missing copies with the live adapter's payload and acl, updates outdated ones in place, repoints the adapter mapping, then re-reads every vault and exits 1 if anything is still unsynced.
  • Duplicates are reported, never deleted. Rows newer than --settle (default 120s) are skipped to avoid racing the live sync. --only=@ename limits to one vault.

Testing

Ran against real eCurrency migrations on Postgres plus a mock Registry/eVault seeded with missing, outdated, duplicate and synced copies, a mapping pointing at a non-existent envelope, a 429, a 500, and a vault that silently drops one write:

  • Check mode reported exactly those, wrote nothing, exited 1.
  • First --apply wrote 11, updated 2, caught the dropped write on re-read, exited 1; second run fixed it and exited 0. The bad mapping was repointed.

Notes

  • New copies fire webhooks like any normal sync (skipWebhooks is only honoured for emover).
  • Rows whose parties have no eName, and "Account initialized" rows (no parties), can't be synced; they're counted.
  • Run in the same timezone as the eCurrency server.

@coderabbitai

coderabbitai Bot commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e13c6c7b-7d98-4503-b41a-b6de89109542
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T08:50:14.886991Z e2cec73 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coodos
coodos merged commit 4bfca03 into main Oct 15, 2026
4 checks passed
@coodos
coodos deleted the feat/ecurrency-ledger-sync branch October 15, 2026 08:43

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e2cec7350b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

async function expectedPayload(row: Ledger): Promise<Record<string, unknown>> {
const currencyGlobalId = await adapter.mappingDb.getGlobalId(row.currencyId);
return {
currencyId: currencyGlobalId ?? row.currencyId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refuse to write ledgers without a global currency ID

When the currency mapping is absent or stale—the same mapping-database failure mode this repair tool is intended to tolerate—this fallback writes the local PostgreSQL UUID as currencyId. The live adapter instead resolves this field through the mapping store, so the repaired ledger no longer references the eVault currency; because isCopyOf accepts a matching hash without checking currencyId, the verification pass nevertheless reports it as synced. Resolve and verify the currency envelope mapping, or fail the row rather than persisting a local ID.

Useful? React with 👍 / 👎.

const t = Date.parse(String(a));
if (Number.isNaN(t)) return false;
const diff = Math.abs(t - b.getTime());
return diff === 0 || (diff % (15 * 60_000) === 0 && diff <= 14 * 3_600_000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not equate every 15-minute timestamp difference

For hashless records, this treats any timestamps separated by a multiple of 15 minutes up to 14 hours as identical, not merely timestamps affected by a known timezone offset. If an account returns to the same balance after repeated same-value transactions—for example, equivalent debits at 10:00 and 10:15—the same hashless envelope can be selected as the update target for both rows; --apply then overwrites that envelope twice instead of creating the missing copy, corrupting the first copy before the final verification fails. Matching should use an explicit timezone conversion and additional immutable identity fields rather than this broad interval test.

Useful? React with 👍 / 👎.

const flag = (name: string) => args.find((a) => a.startsWith(`--${name}=`))?.split("=")[1];
const APPLY = args.includes("--apply");
const ONLY = flag("only") ? normalizeEname(flag("only") as string) : null;
const SETTLE_MS = Number(flag("settle") ?? 120) * 1000;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject nonnumeric settle values

If an operator supplies a malformed value such as --settle=120s, Number(...) produces NaN, the cutoff becomes an invalid date, and every row.createdAt <= cutoff comparison is false. The script consequently checks zero rows and exits successfully with a 0/0 report, which can make an automated repair job falsely appear complete; validate that the value is finite and nonnegative before querying.

Useful? React with 👍 / 👎.

if (ename) parties.add(ename);
else unnamed.add(`${type}:${id}`);
}
if (parties.size === 0) noParties++;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route mint rows to their account owner's vault

CurrencyService.mintCurrency creates each minted credit with the group's accountId and accountType but without sender or receiver fields, so this branch classifies every mint as having no parties and never adds it to byVault. As a result, neither check nor apply mode verifies or repairs minted-supply entries in the group's eVault, and the script can exit successfully while those ledger rows remain absent; non-initialization rows without explicit parties need to fall back to their ledger account owner.

Useful? React with 👍 / 👎.

Comment on lines +104 to +107
} catch (error) {
if (attempt >= 5) throw error;
await sleep(1000 * 2 ** attempt);
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not retry non-idempotent store mutations

This generic retry also wraps storeMetaEnvelope, whose identifier is generated by the eVault. If the eVault commits a missing-row write but the response is lost or the client times out, this catch retries the mutation and creates a second envelope for the same ledger row. The post-write verification then succeeds because at least one copy matches, and report.duplicates is not recomputed after writes, so the run can exit 0 without even reporting the duplicate it created; retries for stores need an idempotency key or an intervening read instead.

Useful? React with 👍 / 👎.

Comment on lines +321 to +324
if (current) await adapter.mappingDb.deleteMapping(localId);
await adapter.mappingDb.storeMapping({ localId, globalId });
} catch (error) {
console.warn(`[SYNC] mapping ${localId}: ${(error as Error).message}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail the run when a mapping cannot be repointed

If SQLite is locked, unwritable, or otherwise rejects storeMapping, this block only logs a warning; when an old mapping existed it has already been deleted on the preceding line. Vault verification does not inspect the mapping database, so --apply can still exit 0 while the adapter mapping remains stale or absent, contrary to the repair's stated guarantee and leaving subsequent live updates directed at the wrong envelope. Propagate the error into the vault report or verify the mapping before declaring success.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant