feat: add MiniPay (Celo) V1 support - #155
Conversation
Implements issue #154: bring Stacks to MiniPay as a Celo Mini App alongside the existing Gnosis/Privy build. - Branch the provider stack at runtime: a plain-wagmi injected-wallet MiniPay stack (auto-connect, no Privy/RainbowKit wallet list) mounts only when the MiniPay browser is detected and the deployment chain is Celo; Gnosis keeps the Privy stack. - Auth without message signing (MiniPay supports none): trust the injected address, mint an HS256 session JWT at /api/minipay/session, onboard as minipay:<address> in the existing users table; /api/profile accepts both Privy and MiniPay tokens. Client identity now flows through a useUserIdentity context instead of usePrivy directly. - Route contract writes through a TxSender context: Privy sponsored-send on Gnosis, wagmi send with CIP-64 feeCurrency (stablecoin gas, legacy tx) on MiniPay. - Add Celo mainnet/Sepolia to chain config; make the deposit token (address/symbol/decimals) configurable and replace hardcoded 18-decimal formatting with deposit-token-aware helpers for 6-decimal USDT/USDC. - Gate Gnosis-only rails (xDAI funding, bake, LiFi) off on Celo; gate circle creation off in MiniPay (invite links need EIP-712 signing); deep-link low balance to MiniPay Add Cash; apply MiniPay copy rules. Refs #154
✅ Deploy Preview for app-stacks ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
Adds V1 MiniPay (Celo) support alongside the existing Gnosis/Privy build by branching the provider stack at runtime, introducing signing-free MiniPay session auth, and making the deposit token configurable so 6‑decimal stablecoins display/parse correctly.
Changes:
- Add Celo (mainnet + Sepolia) network configuration and Celo-specific CIP‑64
feeCurrencyselection. - Introduce a MiniPay provider stack (injected wallet, auto-connect, stablecoin-gas tx sending) and an identity abstraction to support both Privy and MiniPay auth.
- Make deposit token symbol/decimals configurable and replace hardcoded 18‑dec formatting/copy across the UI; gate Gnosis-only rails and MiniPay-incompatible flows.
Reviewed changes
Copilot reviewed 66 out of 66 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/network.ts | Adds Celo mainnet/Sepolia to the network map. |
| src/utils/minipay.ts | Adds MiniPay browser detection, user-id format, and add-cash deeplink. |
| src/utils/celo.ts | Adds Celo chain detection and CIP‑64 fee-currency lookup. |
| src/lib/server-minipay.ts | Adds SSR MiniPay user-agent hinting. |
| src/lib/envs/server.ts | Adds optional server env var for MiniPay session HMAC secret. |
| src/lib/env.ts | Adds configurable deposit token symbol/decimals + fee-currency env var. |
| src/lib/deposit-token.ts | Centralizes deposit-token address/symbol/decimals + parse/format helpers. |
| src/hooks/use-watch-funded-xdai.ts | Gates xDAI funding watcher off on Celo deployments. |
| src/hooks/use-user-circles-list.ts | Switches withdraw amount formatting to deposit-token-aware helpers. |
| src/hooks/use-sponsored-tx.ts | Routes sponsored tx sending through provider-stack TxSender context. |
| src/hooks/use-post-native-funding-bake.ts | Disables post-funding “bake” flow on Celo. |
| src/hooks/use-my-profile.ts | Uses provider-agnostic user identity + bearer token for profile writes. |
| src/hooks/use-is-minipay.ts | Adds hook to detect MiniPay + Celo deployment at runtime. |
| src/hooks/use-fund-with-connected-wallet.ts | Makes funding amount parsing token-decimal-aware; blocks xDAI option on Celo. |
| src/hooks/use-automatic-claims.ts | Uses provider-agnostic user identity instead of Privy user directly. |
| src/hooks/use-auto-bake-bread.ts | Hard-disables baking on Celo. |
| src/hooks/use-all-circles.ts | Switches deposit/pool formatting to deposit-token-aware helpers. |
| src/components/start-circle-button.tsx | Displays deposit amount with configurable token symbol/decimals. |
| src/components/stack.tsx | Updates stack UI calculations/copy to use configurable deposit token. |
| src/components/providers/web3.tsx | Adds Celo chains and transports to the main wagmi config. |
| src/components/providers/user-identity.tsx | Adds UserIdentity context + Privy implementation. |
| src/components/providers/tx-sender.tsx | Adds TxSender context + Privy implementation (sponsorship preserved). |
| src/components/providers/minipay.tsx | Adds full MiniPay provider stack + session onboarding + CIP‑64 tx sending. |
| src/components/providers/index.tsx | Branches runtime provider stack between Privy and MiniPay; wires identity + tx sender contexts. |
| src/components/onboard/visitor-tracker.tsx | Switches visitor onboarding gating from Privy user to identity abstraction. |
| src/components/Navbar/nav-links.tsx | Hides “Create” link in MiniPay. |
| src/components/Navbar/bread-text.tsx | Updates navbar guidance copy for Celo vs Gnosis rails. |
| src/components/Navbar/action-items.tsx | Deep-links MiniPay “Deposit” instead of opening Privy funding modal. |
| src/components/modal/modals/withdraw-bread.tsx | Makes withdraw modal token-decimal-aware and updates token copy. |
| src/components/modal/modals/wallet-funding-status.tsx | Updates funding-status copy for Celo vs Gnosis rails. |
| src/components/modal/modals/stack-result.tsx | Uses identity abstraction and token symbol in stack creation result copy. |
| src/components/modal/modals/set-alias.tsx | Uses identity abstraction so alias setting works for MiniPay sessions. |
| src/components/modal/modals/new-user-onboarding.tsx | Replaces UI-kit bread balance hook with direct ERC-20 read to support 6 decimals; updates copy. |
| src/components/modal/modals/fund-wallet/stacks-balance.tsx | Replaces UI-kit bread balance hook with direct ERC-20 read to support 6 decimals. |
| src/components/modal/modals/fund-wallet/fund-with-connected-wallet.tsx | Gates native-balance hints and “More” section off on Celo. |
| src/components/modal/modals/fund-wallet/fund-with-connected-wallet-modal-amount.tsx | Makes token selection/balances amount formatting decimal-aware; hides xDAI on Celo. |
| src/components/modal/modals/fund-wallet/fund-wallet.tsx | Updates funding modal copy/sections for Celo vs Gnosis. |
| src/components/modal/modals/deposit-result.tsx | Adds MiniPay-specific insufficient-balance CTA to deeplink into add-cash. |
| src/components/modal/modals/deposit-init.tsx | Displays deposit amounts using deposit-token helpers + symbol. |
| src/components/modal/modals/claim.tsx | Updates claim modal deposit label to use token helpers + symbol. |
| src/components/modal/context.tsx | Adds insufficientBalance flag for MiniPay deposit-result UX. |
| src/components/deposit-button.tsx | Adds MiniPay-only fresh balance check to surface add-cash CTA on insufficient funds. |
| src/components/claim-button.tsx | Updates claim button label to show configurable token symbol. |
| src/components/_home/hero-banner.tsx | Gates stack creation CTA off in MiniPay. |
| src/components/_home/header.tsx | Gates stack creation CTA off in MiniPay. |
| src/app/stacks/join/_components/invite-details.tsx | Updates invite details copy to use configurable token symbol. |
| src/app/stacks/join/_components/accept-invite.tsx | Uses identity abstraction when marking invite links as used. |
| src/app/stacks/[id]/_components/total-stacked.tsx | Uses deposit-token formatting and symbol in stack detail UI. |
| src/app/stacks/[id]/_components/stack-details.tsx | Uses deposit-token formatting and symbol in stack detail UI. |
| src/app/stacks/[id]/_components/overview.tsx | Uses deposit-token formatting and symbol in stack overview UI. |
| src/app/stacks/[id]/_components/overall-stacked.tsx | Uses deposit-token formatting for overall-stacked display. |
| src/app/stacks/[id]/_components/members.tsx | Uses deposit-token formatting in member deposit calculations. |
| src/app/stacks/[id]/_components/members-info.tsx | Uses deposit-token formatting + symbol in member info displays. |
| src/app/stacks/[id]/_components/last-claim.tsx | Uses aliases in MiniPay to avoid showing raw addresses as primary identifier. |
| src/app/stacks/[id]/_components/back-meta.tsx | Uses deposit-token formatting + symbol in back-meta summary. |
| src/app/new/_components/onboarding/index.tsx | Blocks stack creation flow inside MiniPay with explanatory copy. |
| src/app/new/_components/form/overview.tsx | Uses deposit-token parsing and symbol in stack creation tx + copy. |
| src/app/new/_components/form/form.tsx | Updates form copy/iconography to use deposit-token symbol. |
| src/app/layout.tsx | Passes SSR MiniPay hint into provider stack selection. |
| src/app/api/profile/route.ts | Accepts either Privy access token or MiniPay session token for authenticated profile writes. |
| src/app/api/minipay/session/route.ts | Adds MiniPay session onboarding endpoint (mints HS256 session token). |
| src/app/api/minipay/auth.ts | Adds mint/verify helpers for MiniPay HS256 session JWTs. |
| src/app/account/_components/profile-section.tsx | Switches to identity abstraction for owner profile edits. |
| src/app/account/_components/account-overview-card.tsx | Deep-links MiniPay deposits instead of showing Privy funding modal. |
| .github/ISSUE_TEMPLATE/design.md | Template formatting tweaks (whitespace/structure). |
| .env.local.example | Documents/configures new Celo/MiniPay-related env vars. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const isTokenExpired = (token: string): boolean => { | ||
| try { | ||
| const { exp } = JSON.parse(atob(token.split(".")[1])) as { exp?: number }; | ||
| return typeof exp !== "number" || Date.now() / 1000 > exp - 60; | ||
| } catch { | ||
| return true; | ||
| } | ||
| }; |
There was a problem hiding this comment.
Good catch — fixed in e2ad08e. isTokenExpired now converts the base64url payload to base64 (-→+, _→/) before atob, so a valid token is no longer treated as expired and re-minted on every getAuthToken call.
| const balance = data | ||
| ? formatBalance(Number(data) / 10 ** BREAD_DECIMALS, 2) | ||
| ? formatBalance(Number(data) / 10 ** DEPOSIT_TOKEN.decimals, 2) | ||
| : 0; |
There was a problem hiding this comment.
Fixed in e2ad08e — the balance now goes through formatDepositAmount(data) (viem formatUnits) instead of Number(bigint) / 10 ** decimals, matching the helper used elsewhere in the modal and avoiding the precision loss.
| const { userId, ready } = useUserIdentity(); | ||
|
|
||
| console.log({ modalShown: modalShown.current, user, ready }); | ||
| console.log({ modalShown: modalShown.current, userId, ready }); | ||
|
|
| import { serverEnv } from "@/lib/envs/server"; | ||
| import { createClient } from "@supabase/supabase-js"; | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { getAddress, isAddress } from "viem"; | ||
| import { createErrorResponse } from "../../utils"; | ||
| import { minipayUserId } from "@/utils/minipay"; | ||
| import { mintMiniPaySessionToken } from "../auth"; | ||
|
|
||
| const supabaseAdmin = createClient( | ||
| serverEnv.NEXT_PUBLIC_SUPABASE_URL, | ||
| serverEnv.SUPABASE_SERVICE_ROLE_KEY | ||
| ); | ||
|
|
||
| // Onboards a MiniPay user from their injected wallet address and mints a | ||
| // session token, mirroring /api/onboard + the Privy access token. There is | ||
| // no signature to verify — MiniPay has no message signing, so the in-app | ||
| // browser is the trust boundary (issue #154 § 2, option 1). | ||
| export async function POST(req: NextRequest) { | ||
| try { | ||
| if (!serverEnv.MINIPAY_SESSION_SECRET) { | ||
| return createErrorResponse("MiniPay sessions are not configured", 503); | ||
| } | ||
|
|
There was a problem hiding this comment.
This is an intentional, documented V1 trade-off, not an oversight — see issue #154 § 2 ("Critical constraint: no message signing"). MiniPay supports no personal_sign/eth_signTypedData at all, so SIWE / proof-of-key-possession is impossible; the spec's recommended V1 approach (option 1) is to trust the injected address, because MiniPay's in-app browser is the trust boundary and a third party can't spoof window.ethereum + eth_requestAccounts from outside it. The endpoint already validates the address and is chain-gated (the MiniPay stack only mounts on a Celo deployment).
Scope of the exposure is narrow: the only route this session token authenticates is /api/profile (setting an alias) — a cosmetic write, and exactly the accepted risk in the spec. The stack-metadata/invite/onboard routes don't verify this token at all; they trust a client-supplied id in the body, which is pre-existing behaviour identical to the current Privy build and unchanged by this PR.
Stronger identity (ODIS phone attestation, or requiring a first on-chain tx as proof) is called out in the spec as options 2/3 and deliberately deferred past V1 — worth revisiting if abuse appears. Flagging explicitly for the team per the spec's request.
…ance - Decode the MiniPay session JWT as base64url in isTokenExpired so the expiry check no longer throws and force-refetches a new token every call. - Format the withdraw balance via formatDepositAmount instead of Number(bigint) / 10 ** decimals, avoiding precision loss on large balances. - Drop a debug console.log of identity state from OnboardVisitorTracker. Refs #154
# Conflicts: # src/app/stacks/[id]/_components/overview.tsx # src/components/deposit-button.tsx
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 68 out of 68 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
src/app/api/profile/route.ts:52
verifyUserTokennow returns an external user id for both Privy and MiniPay tokens, soprivyUserIdis a misleading variable name here (it’s not necessarily a Privy ID). Renaming avoids confusion when adding more token types later.
const privyUserId = await verifyUserToken(req);
if (!privyUserId) return createErrorResponse("Unauthorized", 401);
let body: unknown;
| const balance = data | ||
| ? formatBalance(Number(data) / 10 ** BREAD_DECIMALS, 2) | ||
| ? formatBalance(Number(formatDepositAmount(data)), 2) | ||
| : 0; | ||
| console.log("Balance data", { data, isLoading, error, balance }); |
| together faster | ||
| </h1> | ||
| <p className="text-xl font-bold text-paper-2 md:text-2xl"> | ||
| Invite friends deposit, get payed out. |
| export async function isServerMiniPay() { | ||
| const headersList = await headers(); | ||
| return isMiniPayUserAgent(headersList.get("user-agent")); | ||
| } |
Builds on the signing-free addMembers/removeMember contract change (saving-circles #185) so MiniPay users can create circles and invite members without EIP-712 signatures, and un-gates circle creation in MiniPay. - Bump saving-circles submodule to feat/add-members; add addMembers, removeMember, MAX_MEMBERS + MemberAdded/MemberRemoved to the ABI. - AddMembersCard: resolve ENS (on Ethereum mainnet) or paste an address, stage a review list, and add everyone in one addMembers tx; owners can remove pre-start members on-chain. - Resolver safeguards for the cross-chain (resolve-on-mainnet, spend-on-Celo) hazard: prefer the ENSIP-11 Celo address record, hard-block mainnet contract wallets (uncontrolled on Celo), warn on never-used-on-Celo. - Wire the card into the create-success modal (MiniPay variant) and the owner's stack overview (Celo); allow empty invite_links in stack metadata. - Un-gate creation in MiniPay (home, hero, navbar, /new) now that invites no longer require signing. - Celo mainnet deployment: DeployCelo.s.sol (real USDT deposit token, no mock), make deploy-celo with Celoscan verification and anvil-key guards.
Follow-up: signing-free member invites (replaces the create-in-MiniPay gate)Earlier this branch gated circle creation off in MiniPay because invite links need an EIP-712 owner signature that MiniPay can't produce. This is now solved end-to-end, so creation is un-gated in MiniPay. Contract (separate PR — BreadchainCoop/saving-circles#185, submodule bumped here):
App:
Adversarial risk review (push-membership). Confirmed findings and disposition:
|
The Celo mainnet deploy belongs with the contracts, not the app. Remove the app-side DeployCelo.s.sol, Celoscan config, and make deploy-celo target; bump the submodule to the saving-circles commit that carries DeployCelo + the deploy:celo script. Deploy from saving-circles, then point this app's NEXT_PUBLIC_* env at the result.
useIsMiniPay derived its answer from window.ethereum in an effect, so it was false on the first render: inside MiniPay the create-Stack modal mounted the Privy variant for one commit, calling Privy hooks outside PrivyProvider and starting EIP-712 invite generation — the one flow MiniPay cannot do. - Publish isMiniPay from the provider branch itself (new is-minipay context) so consumers are correct on render #1; drop the effect-based hook. - Only fall back to the Privy stack after ethereum#initialized or a short grace period, so a late-injected provider no longer strands a real MiniPay session on the wrong stack. - Wait for receipts on the mounted wagmi config (useConfig) instead of importing the Privy/RainbowKit one directly. - Derive the removable-member list from getCircle().owner rather than assuming the owner is still at index 0. - Code-split both provider stacks (extract PrivyProviders, next/dynamic both): the layout chunk no longer carries Privy, RainbowKit or the Celo wagmi config, so each client downloads only the stack it uses. - Bundle the RainbowKit stylesheet instead of fetching it from unpkg on every page — a render-blocking third-party request and an extra origin. - Convert the hero image to WebP (117KB -> 58KB) and Celo-gate the onboarding copy that still told Celo users to send BREAD or xDAI. - Document the MiniPay build, network manifest and submission checklist in docs/MINIPAY.md, including why exact dependency pinning and ignore-scripts=true are deliberately not adopted. Refs #154 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implements #154, bringing Stacks to MiniPay as a Celo Mini App alongside the existing Gnosis/Privy build. It branches the provider stack at runtime — a plain-wagmi injected-wallet MiniPay stack (auto-connect, CIP-64 stablecoin gas, signing-free auth that trusts the injected address and mints an HS256 session JWT) mounts only when the MiniPay browser is detected on a Celo deployment, while Gnosis keeps the Privy stack. It adds Celo mainnet/Sepolia chain config, makes the deposit token (address/symbol/decimals) configurable, and replaces hardcoded 18-decimal formatting with deposit-token-aware helpers so 6-decimal USDT/USDC display correctly. Gnosis-only rails (xDAI funding, bake, LiFi) are gated off on Celo, circle creation is gated off in MiniPay because invite links require EIP-712 signing MiniPay can't do, and low balances deep-link to MiniPay's Add Cash with MiniPay-compliant copy. Verified with
pnpm lint,pnpm build(both Gnosis and Celo configs), andpnpm format:check; V2 yield-vault work is intentionally deferred as a fast-follow.🤖 Generated with Claude Code