Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
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 Coindesk widget.Browser routes: /wallet (dashboard), /wallet/topup, /wallet/send, /wallet/staking.
Balance hero showing credit balance + native-token balances per chain. Desk widget for converting credits ↔ native tokens.
Add credits via native-token transfer or WayForPay card payment (processing backend TBD).
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 always fiat USD. Members earn or top up credits. These credits can be converted to your clone's native token through the Coindesk widget on /wallet. The conversion rate is set via the oracle (platform_settings.web3.oracle.ringPerUsd) and managed by superadmins.
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.
Custodial model: Ring signs transfers server-side for auto-provisioned wallets. External wallets (MetaMask via RainbowKit) connect client-side — they do not use custodial transfer routes. Credits and native-token balances are separate concepts.
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 Coindesk widget.Browser routes: /wallet (dashboard), /wallet/topup, /wallet/send, /wallet/staking.
Balance hero showing credit balance + native-token balances per chain. Desk widget for converting credits ↔ native tokens.
Add credits via native-token transfer or WayForPay card payment (processing backend TBD).
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 always fiat USD. Members earn or top up credits. These credits can be converted to your clone's native token through the Coindesk widget on /wallet. The conversion rate is set via the oracle (platform_settings.web3.oracle.ringPerUsd) and managed by superadmins.
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.
Custodial model: Ring signs transfers server-side for auto-provisioned wallets. External wallets (MetaMask via RainbowKit) connect client-side — they do not use custodial transfer routes. Credits and native-token balances are separate concepts.
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 Coindesk widget.Browser routes: /wallet (dashboard), /wallet/topup, /wallet/send, /wallet/staking.
Balance hero showing credit balance + native-token balances per chain. Desk widget for converting credits ↔ native tokens.
Add credits via native-token transfer or WayForPay card payment (processing backend TBD).
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 always fiat USD. Members earn or top up credits. These credits can be converted to your clone's native token through the Coindesk widget on /wallet. The conversion rate is set via the oracle (platform_settings.web3.oracle.ringPerUsd) and managed by superadmins.
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.
Custodial model: Ring signs transfers server-side for auto-provisioned wallets. External wallets (MetaMask via RainbowKit) connect client-side — they do not use custodial transfer routes. Credits and native-token balances are separate concepts.
| 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 | Spend credits for purchases/services |
| 9 | transferCredits | Credit | Admin credit adjustment |
| 10 | processMembershipFee | Credit | Monthly membership fee from credits |
| 11 | transferNativeTokens | Token | Gasless native-token transfer (treasury-sponsored) |
| 12 | getRingPerUsdRate | Oracle | Current oracle rate |
| 13 | setRingPerUsdRate | 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 |
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/ring-token-oracle.ts | getRingPerUsdRate, setRingPerUsdRate — platform_settings doc web3 via db().readDoc / updateDoc |
| Wallet DB | lib/wallet/user-wallet-db.ts | getNativeWallet, getUserWallets, setUserWallets, migrateUserWalletsToPin |
| Wallet provisioning | features/wallet/services/ensure-wallet.ts | ensureWallet, ensureWallets (multi-chain with encryption, atomic single-write) |
| 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 + WayForPay (backend TBD) |
/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:
Coindesk widget converts at the oracle rate:
Rate stored in platform_settings collection, document id web3, field path oracle.ringPerUsd. ring-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 setRingPerUsdRate 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 | For EVM chains | get-wallet-balance.ts — EVM balance fallback path |
ORACLE_QUOTE_SECRET | For desk | ring-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 |
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).
v2:<scryptSaltHex>:<ivHex>:<encryptedHex>:<authTagHex>[:<pinHashHex>]
100 user-credit-points (USD) → X native tokens at Y USD per 1 native token| 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 | Spend credits for purchases/services |
| 9 | transferCredits | Credit | Admin credit adjustment |
| 10 | processMembershipFee | Credit | Monthly membership fee from credits |
| 11 | transferNativeTokens | Token | Gasless native-token transfer (treasury-sponsored) |
| 12 | getRingPerUsdRate | Oracle | Current oracle rate |
| 13 | setRingPerUsdRate | 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 |
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/ring-token-oracle.ts | getRingPerUsdRate, setRingPerUsdRate — platform_settings doc web3 via db().readDoc / updateDoc |
| Wallet DB | lib/wallet/user-wallet-db.ts | getNativeWallet, getUserWallets, setUserWallets, migrateUserWalletsToPin |
| Wallet provisioning | features/wallet/services/ensure-wallet.ts | ensureWallet, ensureWallets (multi-chain with encryption, atomic single-write) |
| 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 + WayForPay (backend TBD) |
/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:
Coindesk widget converts at the oracle rate:
Rate stored in platform_settings collection, document id web3, field path oracle.ringPerUsd. ring-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 setRingPerUsdRate 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 | For EVM chains | get-wallet-balance.ts — EVM balance fallback path |
ORACLE_QUOTE_SECRET | For desk | ring-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 |
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).
v2:<scryptSaltHex>:<ivHex>:<encryptedHex>:<authTagHex>[:<pinHashHex>]
100 user-credit-points (USD) → X native tokens at Y USD per 1 native token| 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 | Spend credits for purchases/services |
| 9 | transferCredits | Credit | Admin credit adjustment |
| 10 | processMembershipFee | Credit | Monthly membership fee from credits |
| 11 | transferNativeTokens | Token | Gasless native-token transfer (treasury-sponsored) |
| 12 | getRingPerUsdRate | Oracle | Current oracle rate |
| 13 | setRingPerUsdRate | 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 |
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/ring-token-oracle.ts | getRingPerUsdRate, setRingPerUsdRate — platform_settings doc web3 via db().readDoc / updateDoc |
| Wallet DB | lib/wallet/user-wallet-db.ts | getNativeWallet, getUserWallets, setUserWallets, migrateUserWalletsToPin |
| Wallet provisioning | features/wallet/services/ensure-wallet.ts | ensureWallet, ensureWallets (multi-chain with encryption, atomic single-write) |
| 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 + WayForPay (backend TBD) |
/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:
Coindesk widget converts at the oracle rate:
Rate stored in platform_settings collection, document id web3, field path oracle.ringPerUsd. ring-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 setRingPerUsdRate 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 | For EVM chains | get-wallet-balance.ts — EVM balance fallback path |
ORACLE_QUOTE_SECRET | For desk | ring-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 |
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).
v2:<scryptSaltHex>:<ivHex>:<encryptedHex>:<authTagHex>[:<pinHashHex>]
100 user-credit-points (USD) → X native tokens at Y USD per 1 native token