🔁 PAY_DIAMOND Migration Tool — REAL DATA MIGRATION ⚠️ MAINNET

Tool v1.1.0

⚠️ MOVES REAL MEMBER STATE FROM ONE PAY_DIAMOND TO ANOTHER. This tool is for the situation where the OLD PAY_DIAMOND can no longer be upgraded via diamondCut (e.g. its own diamondCut/Loupe/owner selectors were destroyed) and a brand-new PAY_DIAMOND proxy has been deployed via pay-diamond-deploy.html, reusing all existing facet contracts. This tool does NOT deploy anything and does NOT run diamondCut — it only reads member state off the OLD diamond (read-only, zero gas) and writes a best-effort reconstruction of that state onto the NEW diamond via existing admin functions, wallet-by-wallet, fully resumable. Go through the steps below in order, at your own pace.
⚠️ Known data-migration gaps — read before migrating anyone
This is a best-effort reconstruction using only admin functions that already exist in the deployed facets (grantMembership, adjustMemberSubscription, setMemberLockedPrice, setMemberCountry, addCreditNoteWithExpiry, addPaymentRecordsBatch, deactivateMember, plus the 5 new T190 setters — setMemberReferralId, setCreditExpiry, setMemberLockedPlanPrice, setMemberLockSnapshotMeta, setHasEverPurchasedPlan — see PaymentStorage.sol / MembershipFacet.sol / PaymentAdminFacet.sol (v8.14.0) / PaymentCoreFacet.sol / RefundFacet.sol in the holger-protocol repo). It is NOT a byte-for-byte storage clone. The following fields/behaviors are honestly NOT fully restorable with what exists today: Review this list per-wallet in Step 4 (DRY-RUN PREVIEW) before migrating anyone.
Progress backup
Scan results + per-wallet migration status auto-save to this browser (localStorage key 'payDiamondMigrateV1'). Export/Import move progress across devices/browsers; Reset clears everything (asks to confirm). Large member lists may exceed localStorage quota — if so, use EXPORT after every scan/migrate batch as your real backup.

Step 1 — Connect wallet, verify OWNER + network

Strict OWNER gate, same as every other tool in this project — if the connected wallet isn't exactly OWNER, every write button stays disabled. Read-only Steps 2-4 work even before connecting, since they only ever call view functions.

Step 2 — Set OLD (source) and NEW (target) PAY_DIAMOND addresses

OLD is pre-filled with the real T119 mainnet address (0x561ff332b76a7a79346a105cad4fddfb2f219ce8) — edit only if the diamond that needs migrating away from is different. NEW must be a PAY_DIAMOND already deployed with all 10 facets wired (via pay-diamond-deploy.html) — this tool does not deploy or wire anything itself.

Step 3 — SCAN OLD DIAMOND (read-only, zero gas)

Calls getAllMemberWallets() then, for every wallet, getStats(wallet) + getPayments(wallet) + getMemberCountry(wallet) — all view functions, no wallet signature needed, no gas spent. Large member counts take a while (one RPC round-trip per wallet per call); progress is shown live and saved after each wallet so a page refresh mid-scan doesn't lose completed wallets.

Step 4 — DRY-RUN PREVIEW (read-only)

No transactions here — this just renders, per wallet, what Step 5 will attempt to write, and whether the wallet already exists on the NEW diamond (in which case it will be skipped automatically to avoid double-migration/overwrites).

Step 5 — MIGRATE (writes to NEW diamond — real transactions, real gas)

How expiry restore works: for each wallet, grantMembership(wallet, emailHash, now+90s) is sent first (a near-future placeholder — grantMembership reverts if given a non-future timestamp, so a lapsed/expired OLD member's real expiry can't be passed directly). Immediately after, adjustMemberSubscription(wallet, delta) is sent with delta = oldExpiry − (now+90s), which — per the real adjustMemberSubscription logic in PaymentAdminFacet.sol — lands the NEW diamond's subscriptionExpiry exactly on the OLD diamond's real value, whether that's in the future (active member) or the past (lapsed member), regardless of the exact number of seconds the transactions took to confirm.

Each wallet's migration is broken into independent sub-steps you can see fail/retry individually: 1) grant+expiry, 2) locked price (only if lockedPrice>0), 3) country (only if set), 4) credit balance (only if creditBalance>0 — see gap note above re: expiry), 5) payment history replay (chunked in batches of 20 records to stay under gas limits — only if any payments exist), 6) inactive flag (only if the OLD member was inactive). "MIGRATE ALL" runs every pending wallet's full sequence in order; each wallet can also be migrated/skipped individually from the table.