Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
Preparing Ring content
Preparing Ring content
Preparing Ring content
Filter with Founder / Developer in the docs sidebar. This page reflects the verified v1.6.4 codebase: PIN-wrapped encryption, atomic single-write provisioning, wallet_access_tokens, and oracle/platform settings persisted via db() (no private pg.Pool).
Ring Platform's wallet system provides two complementary layers:
WALLET_ENCRYPTION_KEY using AES-256-GCM + scrypt. PIN-wrapped v2 encryption for withdrawal operations.credit_balance field on user documents. Used for store purchases, subscriptions, and conversions to native tokens via the Token Desk widget.Browser routes: /wallet (dashboard), /wallet/topup, /wallet/send, /wallet/staking.
Do not conflate these money paths:
| # | Path | Entry | Result |
|---|---|---|---|
| 1 | Card top-up (wallet_topup) | /wallet/topup / CreditAddFsModal → PaymentConductor | Credit points (fiat ledger) — never on-chain RING |
| 2 | Token Desk (buy) | /wallet Desk widget → desk-service.ts (subscriber+) | Credit points → native RING (nativeOut = points / nativePerMainCurrency) |
| 3 | Store native_token rail | POST /api/store/payments/token | Spend native RING at checkout (separate from top-up) |
| 4 | Chain-proof credit add | POST /api/wallet/credit/topup (tx-hash verify) | Credit add after on-chain transfer proof |
| 5 | BuyNativeViaCard (native_token_onramp) | Confidential tabs when CONFIDENTIAL_TOKEN_ONRAMP=true | Card/PayPal → treasury RING (not credit) |
| 6 | Activity credit rewards | Contribution hooks → enqueueRewardCreditAddEvent | Credit points when credit.rewards.enabled is not false — see Credit Rewards |
Any signed-in user can buy credit points with a card (path 1). Converting points → RING is Token Desk (path 2), available to subscriber+. Optional CONFIDENTIAL_TOKEN_ONRAMP=true enables BuyNativeViaCard (native_token_onramp) for confidential / admin / superadmin — card or PayPal tabs that settle native RING from treasury (not credit points).
Balance hero showing credit balance + native-token balances per chain. Desk widget for converting credits ↔ native tokens.
Add credits via native-token transfer (tx-hash verify) or live Card / PayPal (CreditAddFsModal → PaymentConductor wallet_topup). Card uses env processor (WayForPay or Stripe); PayPal tab sends processor=paypal.
Gasless transfers to contacts — treasury sponsors transaction fees. Pick recipient from contacts or enter address.
Filter with Founder / Developer in the docs sidebar. This page reflects the verified v1.6.4 codebase: PIN-wrapped encryption, atomic single-write provisioning, wallet_access_tokens, and oracle/platform settings persisted via db() (no private pg.Pool).
Ring Platform's wallet system provides two complementary layers:
WALLET_ENCRYPTION_KEY using AES-256-GCM + scrypt. PIN-wrapped v2 encryption for withdrawal operations.credit_balance field on user documents. Used for store purchases, subscriptions, and conversions to native tokens via the Token Desk widget.Browser routes: /wallet (dashboard), /wallet/topup, /wallet/send, /wallet/staking.
Do not conflate these money paths:
| # | Path | Entry | Result |
|---|---|---|---|
| 1 | Card top-up (wallet_topup) | /wallet/topup / CreditAddFsModal → PaymentConductor | Credit points (fiat ledger) — never on-chain RING |
| 2 | Token Desk (buy) | /wallet Desk widget → desk-service.ts (subscriber+) | Credit points → native RING (nativeOut = points / nativePerMainCurrency) |
| 3 | Store native_token rail | POST /api/store/payments/token | Spend native RING at checkout (separate from top-up) |
| 4 | Chain-proof credit add | POST /api/wallet/credit/topup (tx-hash verify) | Credit add after on-chain transfer proof |
| 5 | BuyNativeViaCard (native_token_onramp) | Confidential tabs when CONFIDENTIAL_TOKEN_ONRAMP=true | Card/PayPal → treasury RING (not credit) |
| 6 | Activity credit rewards | Contribution hooks → enqueueRewardCreditAddEvent | Credit points when credit.rewards.enabled is not false — see Credit Rewards |
Any signed-in user can buy credit points with a card (path 1). Converting points → RING is Token Desk (path 2), available to subscriber+. Optional CONFIDENTIAL_TOKEN_ONRAMP=true enables BuyNativeViaCard (native_token_onramp) for confidential / admin / superadmin — card or PayPal tabs that settle native RING from treasury (not credit points).
Balance hero showing credit balance + native-token balances per chain. Desk widget for converting credits ↔ native tokens.
Add credits via native-token transfer (tx-hash verify) or live Card / PayPal (CreditAddFsModal → PaymentConductor wallet_topup). Card uses env processor (WayForPay or Stripe); PayPal tab sends processor=paypal.
Gasless transfers to contacts — treasury sponsors transaction fees. Pick recipient from contacts or enter address.
Filter with Founder / Developer in the docs sidebar. This page reflects the verified v1.6.4 codebase: PIN-wrapped encryption, atomic single-write provisioning, wallet_access_tokens, and oracle/platform settings persisted via db() (no private pg.Pool).
Ring Platform's wallet system provides two complementary layers:
WALLET_ENCRYPTION_KEY using AES-256-GCM + scrypt. PIN-wrapped v2 encryption for withdrawal operations.credit_balance field on user documents. Used for store purchases, subscriptions, and conversions to native tokens via the Token Desk widget.Browser routes: /wallet (dashboard), /wallet/topup, /wallet/send, /wallet/staking.
Do not conflate these money paths:
| # | Path | Entry | Result |
|---|---|---|---|
| 1 | Card top-up (wallet_topup) | /wallet/topup / CreditAddFsModal → PaymentConductor | Credit points (fiat ledger) — never on-chain RING |
| 2 | Token Desk (buy) | /wallet Desk widget → desk-service.ts (subscriber+) | Credit points → native RING (nativeOut = points / nativePerMainCurrency) |
| 3 | Store native_token rail | POST /api/store/payments/token | Spend native RING at checkout (separate from top-up) |
| 4 | Chain-proof credit add | POST /api/wallet/credit/topup (tx-hash verify) | Credit add after on-chain transfer proof |
| 5 | BuyNativeViaCard (native_token_onramp) | Confidential tabs when CONFIDENTIAL_TOKEN_ONRAMP=true | Card/PayPal → treasury RING (not credit) |
| 6 | Activity credit rewards | Contribution hooks → enqueueRewardCreditAddEvent | Credit points when credit.rewards.enabled is not false — see Credit Rewards |
Any signed-in user can buy credit points with a card (path 1). Converting points → RING is Token Desk (path 2), available to subscriber+. Optional CONFIDENTIAL_TOKEN_ONRAMP=true enables BuyNativeViaCard (native_token_onramp) for confidential / admin / superadmin — card or PayPal tabs that settle native RING from treasury (not credit points).
Balance hero showing credit balance + native-token balances per chain. Desk widget for converting credits ↔ native tokens.
Add credits via native-token transfer (tx-hash verify) or live Card / PayPal (CreditAddFsModal → PaymentConductor wallet_topup). Card uses env processor (WayForPay or Stripe); PayPal tab sends processor=paypal.
Gasless transfers to contacts — treasury sponsors transaction fees. Pick recipient from contacts or enter address.
Credit spend at checkout — ledger debits before on-chain settlement in some flows.
Credit balance is fiat points denominated in store mainCurrency (USD on ring-platform.org). Accounting multiplier is credit.creditBalanceUnitToMainCurrency in ring-config.json (usually 1 = 1:1). Members top up credits or, when credit.rewards.enabled is not false, earn them from contribution quests (Credit Rewards). Convert to native token only through the Token Desk (separate oracle rate in platform_settings.web3.oracle.nativePerMainCurrency).
Native token transfers are gasless. The clone treasury sponsors Solana transaction fees so members never need SOL for gas. An optional transfer tax can be configured per clone for high-frequency send projects.
Wallet provisioning is automatic. When a member signs in (Google, Apple, or credentials), the system provisions wallets for all enabled chains via a single atomic setUserWallets() write — no JSONB race, max one wallet per chain. No manual setup required.
PIN-wrapped encryption (2026-07-03): Wallet private keys are encrypted with AES-256-GCM at rest. Members who set a 4-digit PIN get their keys wrapped with PIN-derived entropy — the PIN is verified server-side before any withdrawal operation, never sent to the client in plaintext. Keys stored before PIN setup remain in v1 format (env-only encryption) and are lazily re-encrypted on first PIN-based operation.
credit:balance; custodial wallet rows refresh on wallet:list when Tunnel is connected (60s poll only as fallback).CreditAddFsModal): Card | PayPal tabs → WalletConductor.initiateTopUp → PaymentConductor (amount 25–2000). Browser follows redirect via followCheckoutResult. Balance updates after webhook Approved / capture — not when the browser returns to /wallet.Switching a clone’s card rail to Stripe: set PAYMENT_WALLET_TOPUP_PROCESSOR=stripe (or PAYMENT_DEFAULT_PROCESSOR=stripe). The Card tab stays the same — it does not hardcode WayForPay.
credit_balance debits the fiat ledger (same creditBalanceUnitToMainCurrency rate as ad-hoc spend).spendCredits / POST /api/wallet/credit/spend for non-checkout debits (services, membership fee paths).Custodial model: Ring signs transfers server-side for auto-provisioned wallets. External wallets (MetaMask, Coinbase Wallet, WalletConnect via the Wagmi v3 connector picker) connect client-side — they do not use custodial transfer routes. Credits and native-token balances are separate concepts.
Depends-on: live `credit:balance` and `wallet:list` updates while the wallet tab stays open.
Deep-dive: Server Actions and HTTP SSOT for list, credit, and transfer.
See-also: oracle rate, transfer tax, first-settler discounts.
Same-workflow: credit spend at checkout.
Prerequisite: Google/Apple/social sign-in triggers wallet provisioning.
Deep-dive: Wagmi v3 pathNeedsWeb3 gate, getEvmRpcUrl / getEvmTokenAddress SSOT.
Same-workflow: desk oracle nativePerMainCurrency converts public-pool card chip-ins to pledged native (not wallet_topup 1:1).
Depends-on: Ring Oracle facade for desk quotes, FX overlay, and credit accounting rates.
See-also: activity credit-balance awards and the credit.rewards.enabled kill switch — not card top-up.
getWalletActivity| Activity |
| Unified feed: credit + chain transactions |
| 5 | getCreditHistory | Credit | Paginated credit transaction history |
| 6 | getCreditBalance | Credit | Current credit balance (always fiat USD) |
| 7 | topUpCredits | Credit | Add credits after on-chain verification |
| 8 | spendCredits | Credit | Ad-hoc fiat debit via WalletConductor.spendCredits (rate = credit.creditBalanceUnitToMainCurrency, not desk oracle) |
| 9 | transferCredits | Credit | Admin credit adjustment |
| 10 | processMembershipFee | Credit | Monthly membership fee from credits |
| 11 | transferNativeTokens | Token | Gasless native-token transfer (treasury-sponsored) |
| 12 | getNativeTokenPerMainCurrencyRate | Oracle | Current oracle rate |
| 13 | setNativeTokenPerMainCurrencyRate | Oracle | Update oracle rate (admin-only) |
| 14 | signDeskQuote | Desk | Sign quote for credit↔token conversion |
| 15 | executeDeskQuote | Desk | Execute signed quote (buy/sell/refund) |
| 16 | verifyDeskQuote | Desk | Verify signed quote token |
| 17 | verifyTopUpTransaction | Verify | Verify on-chain ERC20/SPL transfer |
| 18 | listWalletTransactions | Activity | Read wallet_transactions collection directly |
| 19 | setPrimaryWallet | Wallet | Set default wallet address |
| 20 | getSpendSummary | Credit | Period-based credit spending summary |
| 21 | getRewardCreditAddEventSummary | Credit | Credit rewards summary; unused by the quest widget when credit.rewards.enabled is false |
| 22 | getRewardQuestBoard | Credit | Catalog + by-trigger board for UserProgressWidget; empty when rewards disabled |
| 23 | createPinAccessTokenAction | Security | Issue single-use PIN-gated access token (React 19 useActionState) |
| 24 | revokePinAccessTokensAction | Security | Revoke all active PIN access tokens for current user |
| 25 | initiateCreditTopupPayment | Credit / Payments | WayForPay card top-up via PaymentConductor wallet_topup (amount 25–2000) |
Server actions follow the SSOT pattern: dynamic import of service → auth check → operation → revalidatePath('/[locale]/wallet') for cache invalidation.
Import and call a server action
Server actions can be called directly from client components using React 19's useActionState hook.
Or use with form progressive enhancement
| Concern | File | Key exports |
|---|---|---|
| Credit CRUD | features/wallet/services/credit-balance-service.ts | addCredits, spendCredits, addFiatUsd, spendFiatUsd, getCreditHistory, hasSufficientBalance |
| Gasless transfers | features/wallet/chains/native-token-transfer-service.ts | transferNativeTokenForUser(userId, toAddress, amount) — treasury sponsors gas |
| Treasury ops | features/wallet/chains/solana/treasury-transfer-service.ts | transferNativeTokenFromTreasury, transferNativeTokenToTreasury, burnRingFromUser |
| Desk trades | features/wallet/chains/solana/desk-service.ts | quoteDesk, executeDesk — credit↔token conversion |
| Oracle rate | features/wallet/services/native-token-oracle.ts | getNativeTokenPerMainCurrencyRate, setNativeTokenPerMainCurrencyRate — platform_settings doc web3 via db().readDoc / updateDoc |
| Wallet DB | lib/wallet/user-wallet-db.ts | getNativeWallet, getUserWallets, setUserWallets, migrateUserWalletsToPin |
| Wallet provisioning | features/wallet/conductor/wallet-conductor.ts + ensure-wallet.ts | App callers use WalletConductor.ensureNativeWallet; low-level ensureWallets remains the atomic multi-chain writer |
| Balance fetch | features/wallet/services/list-wallets.ts | listWallets — gets on-chain balances via viem/Solana RPC |
| Activity feed | features/wallet/services/wallet-activity-feed.ts | getWalletActivityFeed — unified credit+chain feed with filters |
| Top-up verify | features/wallet/services/topup-verification.ts | verifyTopUpTransaction, reserveTopUpTxHash |
| Encryption | lib/wallet/encrypt-wallet-secret.ts | encryptWalletSecret, decryptSecretWithPin, reencryptLegacyWallet (v1→v2 migration) |
| Wallet decrypt | lib/wallet/decrypt-user-wallet.ts | decryptUserWalletPrivateKey, decryptSolanaWalletSecretKey (Uint8Array), decryptSolanaWalletSecretKeyWithPin |
| PIN access tokens | lib/wallet/pin-access-token-db.ts | issueAccessToken, consumeAccessToken, revokeActiveTokens |
| Credit schemas | lib/zod/credit-schemas.ts | CreditTopUpRequestSchema, CreditSpendRequestSchema, CreditTransactionType |
| Desk schemas | lib/zod/desk-schemas.ts | DeskExecuteRequestSchema, DeskQuoteRequestSchema |
| Route | Component | Description |
|---|---|---|
/wallet | wallet-client.tsx | Balance hero (credit + native-token), DeskWidget, transaction feed |
/wallet/topup | topup-client.tsx | Native-token transfer + live WayForPay card (initiateCreditTopupPayment) |
/wallet/send | send-tokens.tsx | Gasless transfers to contacts |
Wallets created after the 2026-07-03 refactor use the versioned v2 format:
v2:scryptSalt:iv:encrypted:authTag — key derived from WALLET_ENCRYPTION_KEY + random salt.:sha256(pin) appended — key derived from WALLET_ENCRYPTION_KEY + ":" + sha256(pin) + random salt.pinHash enables constant-time PIN rejection before running expensive scrypt (DoS defense).migrateUserWalletsToPin(userId, pin) re-encrypts all legacy v1 wallets for a user.Legacy v1 wallets (iv:encrypted:authTag) remain readable via decryptWalletSecretLegacy().
wallet_access_tokens table (JSONB, sha256-hashed, never raw token persisted)WALLET_ACCESS_TOKEN_TTL_SECONDS env var)usedAt timestamp after successful validation(userId, scope) revokes all prior active tokens — prevents replaywallet_transactions row with type pin_access_grantedThe chain identity model was consolidated in the 2026-07-03 refactor:
| Type | Definition | Purpose |
|---|---|---|
NativeChain | (typeof ringConfig.chains.native) | Single chain identity ('solana' on ring-platform.org) |
SupportedChains | (typeof ringConfig.chains.supported)[number] | All chains the clone supports ('solana' | 'evm' | 'base') |
EnabledChains | (typeof ringConfig.chains.enabled)[number] | Subset of supported chains actively in use |
WalletChain | EnabledChains | Chains a user can hold a wallet for (widened from NativeChain for story 5) |
DEFAULT_WALLET_CHAIN | 'evm' | Fallback for legacy chainless wallet rows (pre-Solana EVM era) |
All three previously scattered hardcoded fallbacks ('solana' in utils.ts, 'evm' in user-wallet-db.ts, 'native' bug in ensure-wallet.ts) now point to DEFAULT_WALLET_CHAIN.
All credit operations use lib/zod/credit-schemas.ts. Valid transaction types:
payment, airdrop, reimbursement, purchase, membership_fee, top_up, bonus, penalty, desk_buy, desk_sell, desk_refund.
Credit balance is always fiat USD on ring-platform.org — never native-token denomination:
The Token Desk converts at the oracle rate (desk-service.ts):
Rate stored in platform_settings collection, document id web3, field path oracle.nativePerMainCurrency. native-token-oracle.ts uses db() from @/lib/database — same persistence layer as platform-settings-service.ts. When PLATFORM_SETTINGS_DISABLE_DB=true, reads use RING_ORACLE_DEFAULT_RATE (fallback 100) and writes throw. Superadmin updates via setNativeTokenPerMainCurrencyRate action or POST /api/admin/web3/settings.
| Variable | Required | Used by |
|---|---|---|
WALLET_ENCRYPTION_KEY | Yes | encrypt-wallet-secret.ts — AES-256-GCM encryption of private keys |
SOLANA_RPC_URL | Yes | solana-client.ts — on-chain Solana operations |
SOLANA_TREASURY_PRIVATE_KEY | Yes | treasury-transfer-service.ts — sponsored gas transfers |
WALLET_ACCESS_TOKEN_TTL_SECONDS | No (default: 900) | pin-access-token-db.ts — single-use token TTL |
POLYGON_RPC_URL / getEvmRpcUrl() | For EVM chains | lib/ring-config-chain.ts + features/wallet/chains/evm/evm-token-transfer.ts; live balance via getWalletBalance in app/_actions/wallet.ts |
ORACLE_QUOTE_SECRET | For desk | native-token-oracle.ts — desk quote HMAC signing (falls back to WALLET_ENCRYPTION_KEY) |
RING_ORACLE_DEFAULT_RATE | No (default 100) | Oracle read fallback when DB row missing or PLATFORM_SETTINGS_DISABLE_DB=true |
PLATFORM_SETTINGS_DISABLE_DB | No | When true, oracle/settings reads use env defaults; writes blocked |
CONFIDENTIAL_TOKEN_ONRAMP | No (default false) | PaymentConductor native_token_onramp — confidential+ card/PayPal → treasury RING |
NEXT_PUBLIC_CONFIDENTIAL_TOKEN_ONRAMP | No (default false) | Client UI mirror for onramp tabs |
See lib/wallet/encrypt-wallet-secret.ts for the v2 envelope format cryptographic spec.
ensureWallets() provisions all missing chain wallets into an in-memory Map keyed by chain, then persists via one setUserWallets() write. This eliminates the JSONB read-modify-write race that previous parallel Promise.all(provisionChainWallet()) designs produced, and guarantees the "max one wallet per enabled chain" invariant (appendWalletIfMissing enforces per-chain idempotency).
WalletConductor.initiateTopUp / initiateCreditTopupPayment → PaymentConductor.createCheckout({ purpose: 'wallet_topup' }) → processor from env or form processor → normalizeCheckoutResult → UI followCheckoutResult (lib/payments/checkout-redirect.ts).
Tab (CreditAddFsModal) | Form | Processor |
|---|---|---|
| Card | No processor field | PAYMENT_WALLET_TOPUP_PROCESSOR / PAYMENT_DEFAULT_PROCESSOR (WayForPay HPP form_post or Stripe navigate) |
| PayPal | processor=paypal | PayPal Orders v2 approve URL (navigate) when credentials configured |
Webhook → handlers/wallet-topup.ts / wallet-topup-stripe.ts / wallet-topup-paypal.ts → creditBalanceService.addFiatUsd (1:1 fiat points). Amount gate: 25–2000. Card top-up credits the fiat ledger only — it never mints on-chain RING. Entry UI: /wallet/topup and features/wallet/components/credit-add-fs-modal.tsx.
Deep HPP / returnUrl vs serviceUrl: WayForPay · PaymentConductor architecture.
To spend native RING directly, use the store native_token rail (POST /api/store/payments/token) — a distinct path from top-up. See PaymentConductor.
POST /api/wallet/credit/topupDistinct from PaymentConductor card top-up. Verifies an on-chain transfer to treasury: when isChainProofRequired(), the request must include a tx_hash which is reserved (reserveTopUpTxHash, anti-double-spend) and verified (verifyTopUpTransaction) before creditBalanceService.addCredits. Non-top_up transaction types (airdrop/bonus) require platform-admin role.
features/wallet/conductor/wallet-conductor.ts is the SSOT orchestration layer for custodial native-token web3 + credit money paths. Thin adapters (app/_actions/wallet.ts, /api/wallet/token/*, /api/wallet/desk/*) call the conductor; it owns PaymentConductor checkouts for wallet_topup / native_token_onramp, Token Desk, send, and user credit-spend. Legacy /api/wallet/ring/* aliases are removed — use /api/wallet/token/*.
| Method | Purpose |
|---|---|
ensureNativeWallet(userOverride) | Provision native + enabled-chain wallets; returns { ok, native, wallets } (OAuth / override-safe; no session required) |
ensureFunded(minCredits) | Session-gated: provision via ensureNativeWallet, assert minimum credit balance |
initiateTopUp | Card / PayPal → credit points (wallet_topup); returns Conductor redirect |
initiateNativeOnramp | Confidential+ card/PayPal → treasury native (native_token_onramp; gated by isNativeTokenOnrampEnabled) |
quoteDesk / executeDesk | Credit points → native RING (subscriber+; Solana desk) |
getNativeBalance | Custodial native balance |
transferNative | Gasless custodial native send (chains.native / chains.enabled) |
spendCredits | User credit-spend API / FormData path — fiat rate from credit.creditBalanceUnitToMainCurrency (not desk oracle) |
Not in WalletConductor: external EVM wallet USDT/POL via POST /api/wallet/transfer (SupportedCrypto when chains.enabled includes evm) — keep separate from Solana custodial SSOT.
Fiat accounting (getMainCurrencyCreditAccountingRate → credit.creditBalanceUnitToMainCurrency) applies to ledger debits. Desk oracle (nativePerMainCurrency) applies only to credit ↔ native conversion. Mixing them understates or overstates store/ad-hoc spends.
| Path | Entry | Rate helper |
|---|---|---|
| Ad-hoc debit | spendCredits action / POST /api/wallet/credit/spend → WalletConductor.spendCredits | getMainCurrencyCreditAccountingRate() |
| Store checkout | PaymentConductor credit_balance → credit-balance.processor.ts | getMainCurrencyCreditAccountingRate() |
| Desk buy/sell | quoteDesk / executeDesk | getNativeTokenPerMainCurrencyRate() only |
Deep HTTP tables and Mermaid: Wallet API.
Oracle rate, transfer tax, first-settler discounts.
v2:<scryptSaltHex>:<ivHex>:<encryptedHex>:<authTagHex>[:<pinHashHex>]
nativeOut = (points × pointFiatValue) / nativePerMainCurrency // pointFiatValue = 1
100 credit points (USD) at nativePerMainCurrency=100 → 1 native tokenCredit spend at checkout — ledger debits before on-chain settlement in some flows.
Credit balance is fiat points denominated in store mainCurrency (USD on ring-platform.org). Accounting multiplier is credit.creditBalanceUnitToMainCurrency in ring-config.json (usually 1 = 1:1). Members top up credits or, when credit.rewards.enabled is not false, earn them from contribution quests (Credit Rewards). Convert to native token only through the Token Desk (separate oracle rate in platform_settings.web3.oracle.nativePerMainCurrency).
Native token transfers are gasless. The clone treasury sponsors Solana transaction fees so members never need SOL for gas. An optional transfer tax can be configured per clone for high-frequency send projects.
Wallet provisioning is automatic. When a member signs in (Google, Apple, or credentials), the system provisions wallets for all enabled chains via a single atomic setUserWallets() write — no JSONB race, max one wallet per chain. No manual setup required.
PIN-wrapped encryption (2026-07-03): Wallet private keys are encrypted with AES-256-GCM at rest. Members who set a 4-digit PIN get their keys wrapped with PIN-derived entropy — the PIN is verified server-side before any withdrawal operation, never sent to the client in plaintext. Keys stored before PIN setup remain in v1 format (env-only encryption) and are lazily re-encrypted on first PIN-based operation.
credit:balance; custodial wallet rows refresh on wallet:list when Tunnel is connected (60s poll only as fallback).CreditAddFsModal): Card | PayPal tabs → WalletConductor.initiateTopUp → PaymentConductor (amount 25–2000). Browser follows redirect via followCheckoutResult. Balance updates after webhook Approved / capture — not when the browser returns to /wallet.Switching a clone’s card rail to Stripe: set PAYMENT_WALLET_TOPUP_PROCESSOR=stripe (or PAYMENT_DEFAULT_PROCESSOR=stripe). The Card tab stays the same — it does not hardcode WayForPay.
credit_balance debits the fiat ledger (same creditBalanceUnitToMainCurrency rate as ad-hoc spend).spendCredits / POST /api/wallet/credit/spend for non-checkout debits (services, membership fee paths).Custodial model: Ring signs transfers server-side for auto-provisioned wallets. External wallets (MetaMask, Coinbase Wallet, WalletConnect via the Wagmi v3 connector picker) connect client-side — they do not use custodial transfer routes. Credits and native-token balances are separate concepts.
Depends-on: live `credit:balance` and `wallet:list` updates while the wallet tab stays open.
Deep-dive: Server Actions and HTTP SSOT for list, credit, and transfer.
See-also: oracle rate, transfer tax, first-settler discounts.
Same-workflow: credit spend at checkout.
Prerequisite: Google/Apple/social sign-in triggers wallet provisioning.
Deep-dive: Wagmi v3 pathNeedsWeb3 gate, getEvmRpcUrl / getEvmTokenAddress SSOT.
Same-workflow: desk oracle nativePerMainCurrency converts public-pool card chip-ins to pledged native (not wallet_topup 1:1).
Depends-on: Ring Oracle facade for desk quotes, FX overlay, and credit accounting rates.
See-also: activity credit-balance awards and the credit.rewards.enabled kill switch — not card top-up.
getWalletActivity| Activity |
| Unified feed: credit + chain transactions |
| 5 | getCreditHistory | Credit | Paginated credit transaction history |
| 6 | getCreditBalance | Credit | Current credit balance (always fiat USD) |
| 7 | topUpCredits | Credit | Add credits after on-chain verification |
| 8 | spendCredits | Credit | Ad-hoc fiat debit via WalletConductor.spendCredits (rate = credit.creditBalanceUnitToMainCurrency, not desk oracle) |
| 9 | transferCredits | Credit | Admin credit adjustment |
| 10 | processMembershipFee | Credit | Monthly membership fee from credits |
| 11 | transferNativeTokens | Token | Gasless native-token transfer (treasury-sponsored) |
| 12 | getNativeTokenPerMainCurrencyRate | Oracle | Current oracle rate |
| 13 | setNativeTokenPerMainCurrencyRate | Oracle | Update oracle rate (admin-only) |
| 14 | signDeskQuote | Desk | Sign quote for credit↔token conversion |
| 15 | executeDeskQuote | Desk | Execute signed quote (buy/sell/refund) |
| 16 | verifyDeskQuote | Desk | Verify signed quote token |
| 17 | verifyTopUpTransaction | Verify | Verify on-chain ERC20/SPL transfer |
| 18 | listWalletTransactions | Activity | Read wallet_transactions collection directly |
| 19 | setPrimaryWallet | Wallet | Set default wallet address |
| 20 | getSpendSummary | Credit | Period-based credit spending summary |
| 21 | getRewardCreditAddEventSummary | Credit | Credit rewards summary; unused by the quest widget when credit.rewards.enabled is false |
| 22 | getRewardQuestBoard | Credit | Catalog + by-trigger board for UserProgressWidget; empty when rewards disabled |
| 23 | createPinAccessTokenAction | Security | Issue single-use PIN-gated access token (React 19 useActionState) |
| 24 | revokePinAccessTokensAction | Security | Revoke all active PIN access tokens for current user |
| 25 | initiateCreditTopupPayment | Credit / Payments | WayForPay card top-up via PaymentConductor wallet_topup (amount 25–2000) |
Server actions follow the SSOT pattern: dynamic import of service → auth check → operation → revalidatePath('/[locale]/wallet') for cache invalidation.
Import and call a server action
Server actions can be called directly from client components using React 19's useActionState hook.
Or use with form progressive enhancement
| Concern | File | Key exports |
|---|---|---|
| Credit CRUD | features/wallet/services/credit-balance-service.ts | addCredits, spendCredits, addFiatUsd, spendFiatUsd, getCreditHistory, hasSufficientBalance |
| Gasless transfers | features/wallet/chains/native-token-transfer-service.ts | transferNativeTokenForUser(userId, toAddress, amount) — treasury sponsors gas |
| Treasury ops | features/wallet/chains/solana/treasury-transfer-service.ts | transferNativeTokenFromTreasury, transferNativeTokenToTreasury, burnRingFromUser |
| Desk trades | features/wallet/chains/solana/desk-service.ts | quoteDesk, executeDesk — credit↔token conversion |
| Oracle rate | features/wallet/services/native-token-oracle.ts | getNativeTokenPerMainCurrencyRate, setNativeTokenPerMainCurrencyRate — platform_settings doc web3 via db().readDoc / updateDoc |
| Wallet DB | lib/wallet/user-wallet-db.ts | getNativeWallet, getUserWallets, setUserWallets, migrateUserWalletsToPin |
| Wallet provisioning | features/wallet/conductor/wallet-conductor.ts + ensure-wallet.ts | App callers use WalletConductor.ensureNativeWallet; low-level ensureWallets remains the atomic multi-chain writer |
| Balance fetch | features/wallet/services/list-wallets.ts | listWallets — gets on-chain balances via viem/Solana RPC |
| Activity feed | features/wallet/services/wallet-activity-feed.ts | getWalletActivityFeed — unified credit+chain feed with filters |
| Top-up verify | features/wallet/services/topup-verification.ts | verifyTopUpTransaction, reserveTopUpTxHash |
| Encryption | lib/wallet/encrypt-wallet-secret.ts | encryptWalletSecret, decryptSecretWithPin, reencryptLegacyWallet (v1→v2 migration) |
| Wallet decrypt | lib/wallet/decrypt-user-wallet.ts | decryptUserWalletPrivateKey, decryptSolanaWalletSecretKey (Uint8Array), decryptSolanaWalletSecretKeyWithPin |
| PIN access tokens | lib/wallet/pin-access-token-db.ts | issueAccessToken, consumeAccessToken, revokeActiveTokens |
| Credit schemas | lib/zod/credit-schemas.ts | CreditTopUpRequestSchema, CreditSpendRequestSchema, CreditTransactionType |
| Desk schemas | lib/zod/desk-schemas.ts | DeskExecuteRequestSchema, DeskQuoteRequestSchema |
| Route | Component | Description |
|---|---|---|
/wallet | wallet-client.tsx | Balance hero (credit + native-token), DeskWidget, transaction feed |
/wallet/topup | topup-client.tsx | Native-token transfer + live WayForPay card (initiateCreditTopupPayment) |
/wallet/send | send-tokens.tsx | Gasless transfers to contacts |
Wallets created after the 2026-07-03 refactor use the versioned v2 format:
v2:scryptSalt:iv:encrypted:authTag — key derived from WALLET_ENCRYPTION_KEY + random salt.:sha256(pin) appended — key derived from WALLET_ENCRYPTION_KEY + ":" + sha256(pin) + random salt.pinHash enables constant-time PIN rejection before running expensive scrypt (DoS defense).migrateUserWalletsToPin(userId, pin) re-encrypts all legacy v1 wallets for a user.Legacy v1 wallets (iv:encrypted:authTag) remain readable via decryptWalletSecretLegacy().
wallet_access_tokens table (JSONB, sha256-hashed, never raw token persisted)WALLET_ACCESS_TOKEN_TTL_SECONDS env var)usedAt timestamp after successful validation(userId, scope) revokes all prior active tokens — prevents replaywallet_transactions row with type pin_access_grantedThe chain identity model was consolidated in the 2026-07-03 refactor:
| Type | Definition | Purpose |
|---|---|---|
NativeChain | (typeof ringConfig.chains.native) | Single chain identity ('solana' on ring-platform.org) |
SupportedChains | (typeof ringConfig.chains.supported)[number] | All chains the clone supports ('solana' | 'evm' | 'base') |
EnabledChains | (typeof ringConfig.chains.enabled)[number] | Subset of supported chains actively in use |
WalletChain | EnabledChains | Chains a user can hold a wallet for (widened from NativeChain for story 5) |
DEFAULT_WALLET_CHAIN | 'evm' | Fallback for legacy chainless wallet rows (pre-Solana EVM era) |
All three previously scattered hardcoded fallbacks ('solana' in utils.ts, 'evm' in user-wallet-db.ts, 'native' bug in ensure-wallet.ts) now point to DEFAULT_WALLET_CHAIN.
All credit operations use lib/zod/credit-schemas.ts. Valid transaction types:
payment, airdrop, reimbursement, purchase, membership_fee, top_up, bonus, penalty, desk_buy, desk_sell, desk_refund.
Credit balance is always fiat USD on ring-platform.org — never native-token denomination:
The Token Desk converts at the oracle rate (desk-service.ts):
Rate stored in platform_settings collection, document id web3, field path oracle.nativePerMainCurrency. native-token-oracle.ts uses db() from @/lib/database — same persistence layer as platform-settings-service.ts. When PLATFORM_SETTINGS_DISABLE_DB=true, reads use RING_ORACLE_DEFAULT_RATE (fallback 100) and writes throw. Superadmin updates via setNativeTokenPerMainCurrencyRate action or POST /api/admin/web3/settings.
| Variable | Required | Used by |
|---|---|---|
WALLET_ENCRYPTION_KEY | Yes | encrypt-wallet-secret.ts — AES-256-GCM encryption of private keys |
SOLANA_RPC_URL | Yes | solana-client.ts — on-chain Solana operations |
SOLANA_TREASURY_PRIVATE_KEY | Yes | treasury-transfer-service.ts — sponsored gas transfers |
WALLET_ACCESS_TOKEN_TTL_SECONDS | No (default: 900) | pin-access-token-db.ts — single-use token TTL |
POLYGON_RPC_URL / getEvmRpcUrl() | For EVM chains | lib/ring-config-chain.ts + features/wallet/chains/evm/evm-token-transfer.ts; live balance via getWalletBalance in app/_actions/wallet.ts |
ORACLE_QUOTE_SECRET | For desk | native-token-oracle.ts — desk quote HMAC signing (falls back to WALLET_ENCRYPTION_KEY) |
RING_ORACLE_DEFAULT_RATE | No (default 100) | Oracle read fallback when DB row missing or PLATFORM_SETTINGS_DISABLE_DB=true |
PLATFORM_SETTINGS_DISABLE_DB | No | When true, oracle/settings reads use env defaults; writes blocked |
CONFIDENTIAL_TOKEN_ONRAMP | No (default false) | PaymentConductor native_token_onramp — confidential+ card/PayPal → treasury RING |
NEXT_PUBLIC_CONFIDENTIAL_TOKEN_ONRAMP | No (default false) | Client UI mirror for onramp tabs |
See lib/wallet/encrypt-wallet-secret.ts for the v2 envelope format cryptographic spec.
ensureWallets() provisions all missing chain wallets into an in-memory Map keyed by chain, then persists via one setUserWallets() write. This eliminates the JSONB read-modify-write race that previous parallel Promise.all(provisionChainWallet()) designs produced, and guarantees the "max one wallet per enabled chain" invariant (appendWalletIfMissing enforces per-chain idempotency).
WalletConductor.initiateTopUp / initiateCreditTopupPayment → PaymentConductor.createCheckout({ purpose: 'wallet_topup' }) → processor from env or form processor → normalizeCheckoutResult → UI followCheckoutResult (lib/payments/checkout-redirect.ts).
Tab (CreditAddFsModal) | Form | Processor |
|---|---|---|
| Card | No processor field | PAYMENT_WALLET_TOPUP_PROCESSOR / PAYMENT_DEFAULT_PROCESSOR (WayForPay HPP form_post or Stripe navigate) |
| PayPal | processor=paypal | PayPal Orders v2 approve URL (navigate) when credentials configured |
Webhook → handlers/wallet-topup.ts / wallet-topup-stripe.ts / wallet-topup-paypal.ts → creditBalanceService.addFiatUsd (1:1 fiat points). Amount gate: 25–2000. Card top-up credits the fiat ledger only — it never mints on-chain RING. Entry UI: /wallet/topup and features/wallet/components/credit-add-fs-modal.tsx.
Deep HPP / returnUrl vs serviceUrl: WayForPay · PaymentConductor architecture.
To spend native RING directly, use the store native_token rail (POST /api/store/payments/token) — a distinct path from top-up. See PaymentConductor.
POST /api/wallet/credit/topupDistinct from PaymentConductor card top-up. Verifies an on-chain transfer to treasury: when isChainProofRequired(), the request must include a tx_hash which is reserved (reserveTopUpTxHash, anti-double-spend) and verified (verifyTopUpTransaction) before creditBalanceService.addCredits. Non-top_up transaction types (airdrop/bonus) require platform-admin role.
features/wallet/conductor/wallet-conductor.ts is the SSOT orchestration layer for custodial native-token web3 + credit money paths. Thin adapters (app/_actions/wallet.ts, /api/wallet/token/*, /api/wallet/desk/*) call the conductor; it owns PaymentConductor checkouts for wallet_topup / native_token_onramp, Token Desk, send, and user credit-spend. Legacy /api/wallet/ring/* aliases are removed — use /api/wallet/token/*.
| Method | Purpose |
|---|---|
ensureNativeWallet(userOverride) | Provision native + enabled-chain wallets; returns { ok, native, wallets } (OAuth / override-safe; no session required) |
ensureFunded(minCredits) | Session-gated: provision via ensureNativeWallet, assert minimum credit balance |
initiateTopUp | Card / PayPal → credit points (wallet_topup); returns Conductor redirect |
initiateNativeOnramp | Confidential+ card/PayPal → treasury native (native_token_onramp; gated by isNativeTokenOnrampEnabled) |
quoteDesk / executeDesk | Credit points → native RING (subscriber+; Solana desk) |
getNativeBalance | Custodial native balance |
transferNative | Gasless custodial native send (chains.native / chains.enabled) |
spendCredits | User credit-spend API / FormData path — fiat rate from credit.creditBalanceUnitToMainCurrency (not desk oracle) |
Not in WalletConductor: external EVM wallet USDT/POL via POST /api/wallet/transfer (SupportedCrypto when chains.enabled includes evm) — keep separate from Solana custodial SSOT.
Fiat accounting (getMainCurrencyCreditAccountingRate → credit.creditBalanceUnitToMainCurrency) applies to ledger debits. Desk oracle (nativePerMainCurrency) applies only to credit ↔ native conversion. Mixing them understates or overstates store/ad-hoc spends.
| Path | Entry | Rate helper |
|---|---|---|
| Ad-hoc debit | spendCredits action / POST /api/wallet/credit/spend → WalletConductor.spendCredits | getMainCurrencyCreditAccountingRate() |
| Store checkout | PaymentConductor credit_balance → credit-balance.processor.ts | getMainCurrencyCreditAccountingRate() |
| Desk buy/sell | quoteDesk / executeDesk | getNativeTokenPerMainCurrencyRate() only |
Deep HTTP tables and Mermaid: Wallet API.
Oracle rate, transfer tax, first-settler discounts.
v2:<scryptSaltHex>:<ivHex>:<encryptedHex>:<authTagHex>[:<pinHashHex>]
nativeOut = (points × pointFiatValue) / nativePerMainCurrency // pointFiatValue = 1
100 credit points (USD) at nativePerMainCurrency=100 → 1 native tokenCredit spend at checkout — ledger debits before on-chain settlement in some flows.
Credit balance is fiat points denominated in store mainCurrency (USD on ring-platform.org). Accounting multiplier is credit.creditBalanceUnitToMainCurrency in ring-config.json (usually 1 = 1:1). Members top up credits or, when credit.rewards.enabled is not false, earn them from contribution quests (Credit Rewards). Convert to native token only through the Token Desk (separate oracle rate in platform_settings.web3.oracle.nativePerMainCurrency).
Native token transfers are gasless. The clone treasury sponsors Solana transaction fees so members never need SOL for gas. An optional transfer tax can be configured per clone for high-frequency send projects.
Wallet provisioning is automatic. When a member signs in (Google, Apple, or credentials), the system provisions wallets for all enabled chains via a single atomic setUserWallets() write — no JSONB race, max one wallet per chain. No manual setup required.
PIN-wrapped encryption (2026-07-03): Wallet private keys are encrypted with AES-256-GCM at rest. Members who set a 4-digit PIN get their keys wrapped with PIN-derived entropy — the PIN is verified server-side before any withdrawal operation, never sent to the client in plaintext. Keys stored before PIN setup remain in v1 format (env-only encryption) and are lazily re-encrypted on first PIN-based operation.
credit:balance; custodial wallet rows refresh on wallet:list when Tunnel is connected (60s poll only as fallback).CreditAddFsModal): Card | PayPal tabs → WalletConductor.initiateTopUp → PaymentConductor (amount 25–2000). Browser follows redirect via followCheckoutResult. Balance updates after webhook Approved / capture — not when the browser returns to /wallet.Switching a clone’s card rail to Stripe: set PAYMENT_WALLET_TOPUP_PROCESSOR=stripe (or PAYMENT_DEFAULT_PROCESSOR=stripe). The Card tab stays the same — it does not hardcode WayForPay.
credit_balance debits the fiat ledger (same creditBalanceUnitToMainCurrency rate as ad-hoc spend).spendCredits / POST /api/wallet/credit/spend for non-checkout debits (services, membership fee paths).Custodial model: Ring signs transfers server-side for auto-provisioned wallets. External wallets (MetaMask, Coinbase Wallet, WalletConnect via the Wagmi v3 connector picker) connect client-side — they do not use custodial transfer routes. Credits and native-token balances are separate concepts.
Depends-on: live `credit:balance` and `wallet:list` updates while the wallet tab stays open.
Deep-dive: Server Actions and HTTP SSOT for list, credit, and transfer.
See-also: oracle rate, transfer tax, first-settler discounts.
Same-workflow: credit spend at checkout.
Prerequisite: Google/Apple/social sign-in triggers wallet provisioning.
Deep-dive: Wagmi v3 pathNeedsWeb3 gate, getEvmRpcUrl / getEvmTokenAddress SSOT.
Same-workflow: desk oracle nativePerMainCurrency converts public-pool card chip-ins to pledged native (not wallet_topup 1:1).
Depends-on: Ring Oracle facade for desk quotes, FX overlay, and credit accounting rates.
See-also: activity credit-balance awards and the credit.rewards.enabled kill switch — not card top-up.
getWalletActivity| Activity |
| Unified feed: credit + chain transactions |
| 5 | getCreditHistory | Credit | Paginated credit transaction history |
| 6 | getCreditBalance | Credit | Current credit balance (always fiat USD) |
| 7 | topUpCredits | Credit | Add credits after on-chain verification |
| 8 | spendCredits | Credit | Ad-hoc fiat debit via WalletConductor.spendCredits (rate = credit.creditBalanceUnitToMainCurrency, not desk oracle) |
| 9 | transferCredits | Credit | Admin credit adjustment |
| 10 | processMembershipFee | Credit | Monthly membership fee from credits |
| 11 | transferNativeTokens | Token | Gasless native-token transfer (treasury-sponsored) |
| 12 | getNativeTokenPerMainCurrencyRate | Oracle | Current oracle rate |
| 13 | setNativeTokenPerMainCurrencyRate | Oracle | Update oracle rate (admin-only) |
| 14 | signDeskQuote | Desk | Sign quote for credit↔token conversion |
| 15 | executeDeskQuote | Desk | Execute signed quote (buy/sell/refund) |
| 16 | verifyDeskQuote | Desk | Verify signed quote token |
| 17 | verifyTopUpTransaction | Verify | Verify on-chain ERC20/SPL transfer |
| 18 | listWalletTransactions | Activity | Read wallet_transactions collection directly |
| 19 | setPrimaryWallet | Wallet | Set default wallet address |
| 20 | getSpendSummary | Credit | Period-based credit spending summary |
| 21 | getRewardCreditAddEventSummary | Credit | Credit rewards summary; unused by the quest widget when credit.rewards.enabled is false |
| 22 | getRewardQuestBoard | Credit | Catalog + by-trigger board for UserProgressWidget; empty when rewards disabled |
| 23 | createPinAccessTokenAction | Security | Issue single-use PIN-gated access token (React 19 useActionState) |
| 24 | revokePinAccessTokensAction | Security | Revoke all active PIN access tokens for current user |
| 25 | initiateCreditTopupPayment | Credit / Payments | WayForPay card top-up via PaymentConductor wallet_topup (amount 25–2000) |
Server actions follow the SSOT pattern: dynamic import of service → auth check → operation → revalidatePath('/[locale]/wallet') for cache invalidation.
Import and call a server action
Server actions can be called directly from client components using React 19's useActionState hook.
Or use with form progressive enhancement
| Concern | File | Key exports |
|---|---|---|
| Credit CRUD | features/wallet/services/credit-balance-service.ts | addCredits, spendCredits, addFiatUsd, spendFiatUsd, getCreditHistory, hasSufficientBalance |
| Gasless transfers | features/wallet/chains/native-token-transfer-service.ts | transferNativeTokenForUser(userId, toAddress, amount) — treasury sponsors gas |
| Treasury ops | features/wallet/chains/solana/treasury-transfer-service.ts | transferNativeTokenFromTreasury, transferNativeTokenToTreasury, burnRingFromUser |
| Desk trades | features/wallet/chains/solana/desk-service.ts | quoteDesk, executeDesk — credit↔token conversion |
| Oracle rate | features/wallet/services/native-token-oracle.ts | getNativeTokenPerMainCurrencyRate, setNativeTokenPerMainCurrencyRate — platform_settings doc web3 via db().readDoc / updateDoc |
| Wallet DB | lib/wallet/user-wallet-db.ts | getNativeWallet, getUserWallets, setUserWallets, migrateUserWalletsToPin |
| Wallet provisioning | features/wallet/conductor/wallet-conductor.ts + ensure-wallet.ts | App callers use WalletConductor.ensureNativeWallet; low-level ensureWallets remains the atomic multi-chain writer |
| Balance fetch | features/wallet/services/list-wallets.ts | listWallets — gets on-chain balances via viem/Solana RPC |
| Activity feed | features/wallet/services/wallet-activity-feed.ts | getWalletActivityFeed — unified credit+chain feed with filters |
| Top-up verify | features/wallet/services/topup-verification.ts | verifyTopUpTransaction, reserveTopUpTxHash |
| Encryption | lib/wallet/encrypt-wallet-secret.ts | encryptWalletSecret, decryptSecretWithPin, reencryptLegacyWallet (v1→v2 migration) |
| Wallet decrypt | lib/wallet/decrypt-user-wallet.ts | decryptUserWalletPrivateKey, decryptSolanaWalletSecretKey (Uint8Array), decryptSolanaWalletSecretKeyWithPin |
| PIN access tokens | lib/wallet/pin-access-token-db.ts | issueAccessToken, consumeAccessToken, revokeActiveTokens |
| Credit schemas | lib/zod/credit-schemas.ts | CreditTopUpRequestSchema, CreditSpendRequestSchema, CreditTransactionType |
| Desk schemas | lib/zod/desk-schemas.ts | DeskExecuteRequestSchema, DeskQuoteRequestSchema |
| Route | Component | Description |
|---|---|---|
/wallet | wallet-client.tsx | Balance hero (credit + native-token), DeskWidget, transaction feed |
/wallet/topup | topup-client.tsx | Native-token transfer + live WayForPay card (initiateCreditTopupPayment) |
/wallet/send | send-tokens.tsx | Gasless transfers to contacts |
Wallets created after the 2026-07-03 refactor use the versioned v2 format:
v2:scryptSalt:iv:encrypted:authTag — key derived from WALLET_ENCRYPTION_KEY + random salt.:sha256(pin) appended — key derived from WALLET_ENCRYPTION_KEY + ":" + sha256(pin) + random salt.pinHash enables constant-time PIN rejection before running expensive scrypt (DoS defense).migrateUserWalletsToPin(userId, pin) re-encrypts all legacy v1 wallets for a user.Legacy v1 wallets (iv:encrypted:authTag) remain readable via decryptWalletSecretLegacy().
wallet_access_tokens table (JSONB, sha256-hashed, never raw token persisted)WALLET_ACCESS_TOKEN_TTL_SECONDS env var)usedAt timestamp after successful validation(userId, scope) revokes all prior active tokens — prevents replaywallet_transactions row with type pin_access_grantedThe chain identity model was consolidated in the 2026-07-03 refactor:
| Type | Definition | Purpose |
|---|---|---|
NativeChain | (typeof ringConfig.chains.native) | Single chain identity ('solana' on ring-platform.org) |
SupportedChains | (typeof ringConfig.chains.supported)[number] | All chains the clone supports ('solana' | 'evm' | 'base') |
EnabledChains | (typeof ringConfig.chains.enabled)[number] | Subset of supported chains actively in use |
WalletChain | EnabledChains | Chains a user can hold a wallet for (widened from NativeChain for story 5) |
DEFAULT_WALLET_CHAIN | 'evm' | Fallback for legacy chainless wallet rows (pre-Solana EVM era) |
All three previously scattered hardcoded fallbacks ('solana' in utils.ts, 'evm' in user-wallet-db.ts, 'native' bug in ensure-wallet.ts) now point to DEFAULT_WALLET_CHAIN.
All credit operations use lib/zod/credit-schemas.ts. Valid transaction types:
payment, airdrop, reimbursement, purchase, membership_fee, top_up, bonus, penalty, desk_buy, desk_sell, desk_refund.
Credit balance is always fiat USD on ring-platform.org — never native-token denomination:
The Token Desk converts at the oracle rate (desk-service.ts):
Rate stored in platform_settings collection, document id web3, field path oracle.nativePerMainCurrency. native-token-oracle.ts uses db() from @/lib/database — same persistence layer as platform-settings-service.ts. When PLATFORM_SETTINGS_DISABLE_DB=true, reads use RING_ORACLE_DEFAULT_RATE (fallback 100) and writes throw. Superadmin updates via setNativeTokenPerMainCurrencyRate action or POST /api/admin/web3/settings.
| Variable | Required | Used by |
|---|---|---|
WALLET_ENCRYPTION_KEY | Yes | encrypt-wallet-secret.ts — AES-256-GCM encryption of private keys |
SOLANA_RPC_URL | Yes | solana-client.ts — on-chain Solana operations |
SOLANA_TREASURY_PRIVATE_KEY | Yes | treasury-transfer-service.ts — sponsored gas transfers |
WALLET_ACCESS_TOKEN_TTL_SECONDS | No (default: 900) | pin-access-token-db.ts — single-use token TTL |
POLYGON_RPC_URL / getEvmRpcUrl() | For EVM chains | lib/ring-config-chain.ts + features/wallet/chains/evm/evm-token-transfer.ts; live balance via getWalletBalance in app/_actions/wallet.ts |
ORACLE_QUOTE_SECRET | For desk | native-token-oracle.ts — desk quote HMAC signing (falls back to WALLET_ENCRYPTION_KEY) |
RING_ORACLE_DEFAULT_RATE | No (default 100) | Oracle read fallback when DB row missing or PLATFORM_SETTINGS_DISABLE_DB=true |
PLATFORM_SETTINGS_DISABLE_DB | No | When true, oracle/settings reads use env defaults; writes blocked |
CONFIDENTIAL_TOKEN_ONRAMP | No (default false) | PaymentConductor native_token_onramp — confidential+ card/PayPal → treasury RING |
NEXT_PUBLIC_CONFIDENTIAL_TOKEN_ONRAMP | No (default false) | Client UI mirror for onramp tabs |
See lib/wallet/encrypt-wallet-secret.ts for the v2 envelope format cryptographic spec.
ensureWallets() provisions all missing chain wallets into an in-memory Map keyed by chain, then persists via one setUserWallets() write. This eliminates the JSONB read-modify-write race that previous parallel Promise.all(provisionChainWallet()) designs produced, and guarantees the "max one wallet per enabled chain" invariant (appendWalletIfMissing enforces per-chain idempotency).
WalletConductor.initiateTopUp / initiateCreditTopupPayment → PaymentConductor.createCheckout({ purpose: 'wallet_topup' }) → processor from env or form processor → normalizeCheckoutResult → UI followCheckoutResult (lib/payments/checkout-redirect.ts).
Tab (CreditAddFsModal) | Form | Processor |
|---|---|---|
| Card | No processor field | PAYMENT_WALLET_TOPUP_PROCESSOR / PAYMENT_DEFAULT_PROCESSOR (WayForPay HPP form_post or Stripe navigate) |
| PayPal | processor=paypal | PayPal Orders v2 approve URL (navigate) when credentials configured |
Webhook → handlers/wallet-topup.ts / wallet-topup-stripe.ts / wallet-topup-paypal.ts → creditBalanceService.addFiatUsd (1:1 fiat points). Amount gate: 25–2000. Card top-up credits the fiat ledger only — it never mints on-chain RING. Entry UI: /wallet/topup and features/wallet/components/credit-add-fs-modal.tsx.
Deep HPP / returnUrl vs serviceUrl: WayForPay · PaymentConductor architecture.
To spend native RING directly, use the store native_token rail (POST /api/store/payments/token) — a distinct path from top-up. See PaymentConductor.
POST /api/wallet/credit/topupDistinct from PaymentConductor card top-up. Verifies an on-chain transfer to treasury: when isChainProofRequired(), the request must include a tx_hash which is reserved (reserveTopUpTxHash, anti-double-spend) and verified (verifyTopUpTransaction) before creditBalanceService.addCredits. Non-top_up transaction types (airdrop/bonus) require platform-admin role.
features/wallet/conductor/wallet-conductor.ts is the SSOT orchestration layer for custodial native-token web3 + credit money paths. Thin adapters (app/_actions/wallet.ts, /api/wallet/token/*, /api/wallet/desk/*) call the conductor; it owns PaymentConductor checkouts for wallet_topup / native_token_onramp, Token Desk, send, and user credit-spend. Legacy /api/wallet/ring/* aliases are removed — use /api/wallet/token/*.
| Method | Purpose |
|---|---|
ensureNativeWallet(userOverride) | Provision native + enabled-chain wallets; returns { ok, native, wallets } (OAuth / override-safe; no session required) |
ensureFunded(minCredits) | Session-gated: provision via ensureNativeWallet, assert minimum credit balance |
initiateTopUp | Card / PayPal → credit points (wallet_topup); returns Conductor redirect |
initiateNativeOnramp | Confidential+ card/PayPal → treasury native (native_token_onramp; gated by isNativeTokenOnrampEnabled) |
quoteDesk / executeDesk | Credit points → native RING (subscriber+; Solana desk) |
getNativeBalance | Custodial native balance |
transferNative | Gasless custodial native send (chains.native / chains.enabled) |
spendCredits | User credit-spend API / FormData path — fiat rate from credit.creditBalanceUnitToMainCurrency (not desk oracle) |
Not in WalletConductor: external EVM wallet USDT/POL via POST /api/wallet/transfer (SupportedCrypto when chains.enabled includes evm) — keep separate from Solana custodial SSOT.
Fiat accounting (getMainCurrencyCreditAccountingRate → credit.creditBalanceUnitToMainCurrency) applies to ledger debits. Desk oracle (nativePerMainCurrency) applies only to credit ↔ native conversion. Mixing them understates or overstates store/ad-hoc spends.
| Path | Entry | Rate helper |
|---|---|---|
| Ad-hoc debit | spendCredits action / POST /api/wallet/credit/spend → WalletConductor.spendCredits | getMainCurrencyCreditAccountingRate() |
| Store checkout | PaymentConductor credit_balance → credit-balance.processor.ts | getMainCurrencyCreditAccountingRate() |
| Desk buy/sell | quoteDesk / executeDesk | getNativeTokenPerMainCurrencyRate() only |
Deep HTTP tables and Mermaid: Wallet API.
Oracle rate, transfer tax, first-settler discounts.
v2:<scryptSaltHex>:<ivHex>:<encryptedHex>:<authTagHex>[:<pinHashHex>]
nativeOut = (points × pointFiatValue) / nativePerMainCurrency // pointFiatValue = 1
100 credit points (USD) at nativePerMainCurrency=100 → 1 native token