Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
Preparing Ring Platform content
Preparing Ring Platform content
Preparing Ring Platform 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.
Four separate money paths — do not conflate them:
| # | 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) |
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.
Four separate money paths — do not conflate them:
| # | 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) |
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.
Four separate money paths — do not conflate them:
| # | 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) |
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 earn or top up credits; 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.
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 for user |
| 22 | createPinAccessTokenAction | Security | Issue single-use PIN-gated access token (React 19 useActionState) |
| 23 | revokePinAccessTokensAction | Security | Revoke all active PIN access tokens for current user |
| 24 | 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 earn or top up credits; 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.
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 for user |
| 22 | createPinAccessTokenAction | Security | Issue single-use PIN-gated access token (React 19 useActionState) |
| 23 | revokePinAccessTokensAction | Security | Revoke all active PIN access tokens for current user |
| 24 | 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 earn or top up credits; 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.
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 for user |
| 22 | createPinAccessTokenAction | Security | Issue single-use PIN-gated access token (React 19 useActionState) |
| 23 | revokePinAccessTokensAction | Security | Revoke all active PIN access tokens for current user |
| 24 | 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