Skip to content

feat: add MiniPay (Celo) V1 support - #155

Open
RonTuretzky wants to merge 6 commits into
developmentfrom
issue-154
Open

feat: add MiniPay (Celo) V1 support#155
RonTuretzky wants to merge 6 commits into
developmentfrom
issue-154

Conversation

@RonTuretzky

Copy link
Copy Markdown

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), and pnpm format:check; V2 yield-vault work is intentionally deferred as a fast-follow.

🤖 Generated with Claude Code

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
@netlify

netlify Bot commented Jul 10, 2026

Copy link
Copy Markdown

Deploy Preview for app-stacks ready!

Name Link
🔨 Latest commit 006f1a8
🔍 Latest deploy log https://app.netlify.com/projects/app-stacks/deploys/6a7ddeaa232712000826c5c0
😎 Deploy Preview https://deploy-preview-155--app-stacks.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 feeCurrency selection.
  • 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.

Comment on lines +104 to +111
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;
}
};

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 111 to 113
const balance = data
? formatBalance(Number(data) / 10 ** BREAD_DECIMALS, 2)
? formatBalance(Number(data) / 10 ** DEPOSIT_TOKEN.decimals, 2)
: 0;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 10 to 13
const { userId, ready } = useUserIdentity();

console.log({ modalShown: modalShown.current, user, ready });
console.log({ modalShown: modalShown.current, userId, ready });

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in e2ad08e.

Comment on lines +1 to +23
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);
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • verifyUserToken now returns an external user id for both Privy and MiniPay tokens, so privyUserId is 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;

Comment on lines 111 to 114
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.
Comment thread src/lib/server-minipay.ts
Comment on lines +4 to +7
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.
@RonTuretzky

Copy link
Copy Markdown
Author

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):

  • addMembers(id, address[]) — owner adds members directly pre-start, authorized by msg.sender == owner instead of a signature.
  • removeMember(id, address) — owner or the member can remove pre-start (recovery for typo'd/dead addresses).
  • MAX_MEMBERS=25 enforced on addMembers and redeemInvite (bounds the decommission O(n²) refund loop); both guard against finished circles. Storage layout unchanged → upgrade-safe. 22 new tests, 190 passing.

App:

  • AddMembersCard (create-success modal + owner stack overview): resolve ENS or paste an address, review, add in one tx; remove pre-start members on-chain.
  • Cross-chain resolver safeguards (resolve on Ethereum mainnet, spend on Celo): prefer the ENSIP-11 Celo record, hard-block mainnet contract wallets (uncontrolled on Celo → lost funds), warn on never-used-on-Celo.
  • Celo mainnet deploy: DeployCelo.s.sol (real USDT, no mock) + make deploy-celo with Celoscan verification.

Adversarial risk review (push-membership). Confirmed findings and disposition:

  • HIGH — ENS-on-mainnet → payout to an uncontrollable Celo address: mitigated by the resolver's contract-wallet block + ENSIP-11 preference + on-chain removeMember. Residual (push payouts are unconditional) noted for a future pull-payout/liveness design.
  • MEDIUM — no undo for a wrong pushed address: fixed by removeMember.
  • LOW — MAX_MEMBERS didn't bound decommission (redeemInvite uncapped): fixed by capping both paths.
  • LOW — no consent / owner-chosen payout order: bounded (deposits pull from msg.sender; owner==index 0 is inherent to a ROSCA); removeMember gives an exit; VRF shuffling at start() left as a future enhancement.

⚠️ The contract change needs a fresh audit before mainnet — it's the one piece that isn't already-audited code.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants