Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Supabase
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
SUPABASE_SECRET_KEY=

# Circle
CIRCLE_API_KEY=
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ jobs:
- name: Install dependencies
run: npm install

scan:
needs: lint-and-test
if: ${{ github.event_name == 'pull_request' && github.event.repository.private == false }}
uses: circlefin/circle-public-github-workflows/.github/workflows/pr-scan.yaml@v1
# scan:
# needs: lint-and-test
# if: ${{ github.event_name == 'pull_request' && github.event.repository.private == false }}
# uses: circlefin/circle-public-github-workflows/.github/workflows/pr-scan.yaml@v1

release-sbom:
needs: lint-and-test
if: github.event_name == 'push'
uses: circlefin/circle-public-github-workflows/.github/workflows/attach-release-assets.yaml@v1
# release-sbom:
# needs: lint-and-test
# if: github.event_name == 'push'
# uses: circlefin/circle-public-github-workflows/.github/workflows/attach-release-assets.yaml@v1
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
126 changes: 84 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ Integrate USDC as a payment method for purchasing credits on Arc. This sample ap

<img width="830" height="646" alt="User dashboard for credit purchase" src="public/screenshot.png" />

## Table of Contents

- [Prerequisites](#prerequisites)
- [Clone and Run Locally](#clone-and-run-locally)
- [Environment Variables](#environment-variables)
- [User Accounts](#user-accounts)

## Prerequisites

- Node.js 20.x or newer
- npm (automatically installed when Node.js is installed)
- Docker (for running Supabase locally)
- [ngrok](https://ngrok.com/) (for local webhook testing)
- Circle Developer Controlled Wallets [API key](https://console.circle.com/signin) and [Entity Secret](https://developers.circle.com/wallets/dev-controlled/register-entity-secret)
- **Node.js v22+** — Install via [nvm](https://github.com/nvm-sh/nvm) (`nvm use` will read the `.nvmrc` file)
- **Supabase CLI** — Install via `npm install -g supabase` or see [Supabase CLI docs](https://supabase.com/docs/guides/cli/getting-started)
- **Docker Desktop** (only if using the local Supabase path) — [Install Docker Desktop](https://www.docker.com/products/docker-desktop/)
- **[ngrok](https://ngrok.com/)** - for local webhook testing)
- Circle Developer Controlled Wallets **[API key](https://console.circle.com/signin)** and **[Entity Secret](https://developers.circle.com/wallets/dev-controlled/register-entity-secret)**

## Getting Started

Expand All @@ -22,37 +29,44 @@ Integrate USDC as a payment method for purchasing credits on Arc. This sample ap
npm install
```

2. Create a `.env.local` file in the project root:
2. Set up the database — Choose one of the two paths below:

<details>
<summary><strong>Path 1: Local Supabase (Docker)</strong></summary>

Requires Docker Desktop installed and running.

```bash
cp .env.example .env.local
npx supabase start
npx supabase migration up
```

Required variables:
The output of `npx supabase start` will display the Supabase URL and API keys needed in the next step.

</details>

<details>
<summary><strong>Path 2: Remote Supabase (Cloud)</strong></summary>

Requires a [Supabase](https://supabase.com/) account and project.

```bash
# Supabase
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY=your_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key

# Circle
CIRCLE_API_KEY=your_circle_api_key
CIRCLE_ENTITY_SECRET=your_entity_secret
CIRCLE_BLOCKCHAIN=ARC-TESTNET
CIRCLE_USDC_TOKEN_ID=15dc2b5d-0994-58b0-bf8c-3a0501148ee8

# Misc
ADMIN_EMAIL=admin@admin.com
npx supabase link --project-ref <your-project-ref>
npx supabase db push
```

3. Start Supabase locally:
Retrieve your project URL and API keys from the Supabase dashboard under **Settings → API**.

</details>

3. Set up environment variables:

```bash
npx supabase start
npx supabase migration up
cp .env.example .env.local
```

Then edit `.env.local` and fill in all required values. Use the Supabase URL and keys from the previous step's output (see [Environment Variables](#environment-variables) section below).

4. Start the development server:

```bash
Expand All @@ -69,10 +83,10 @@ Integrate USDC as a payment method for purchasing credits on Arc. This sample ap
ngrok http 3000
```

Add the webhook endpoint in Circle Console:
Copy the HTTPS URL from ngrok (e.g., `https://your-ngrok-url.ngrok.io`) and add it to your Circle Console webhooks section:
- Navigate to Circle Console → Webhooks
- Add endpoint: `https://your-ngrok-url.ngrok.io/api/circle/webhook`
- Keep ngrok running to receive webhook events
- Add a new webhook endpoint: `https://your-ngrok-url.ngrok.io/api/circle/webhook`
- Keep ngrok running while developing to receive webhook events

## How It Works

Expand All @@ -84,26 +98,54 @@ Integrate USDC as a payment method for purchasing credits on Arc. This sample ap

## Environment Variables

Copy `.env.example` to `.env.local` and fill in the required values:

```bash
# Supabase
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY=
SUPABASE_SECRET_KEY=

# Circle
CIRCLE_API_KEY=
CIRCLE_ENTITY_SECRET=
CIRCLE_BLOCKCHAIN=ARC-TESTNET
CIRCLE_USDC_TOKEN_ID=

# Misc
ADMIN_EMAIL=admin@admin.com
```

| Variable | Scope | Purpose |
| ------------------------------------- | ----------- | ------------------------------------------------------------------------ |
| `NEXT_PUBLIC_SUPABASE_URL` | Public | Supabase project URL |
| `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY` | Public | Supabase anonymous/public key |
| `SUPABASE_SERVICE_ROLE_KEY` | Server-side | Service role for privileged database operations |
| `CIRCLE_API_KEY` | Server-side | Circle API key for webhook signature verification |
| `CIRCLE_ENTITY_SECRET` | Server-side | Circle entity secret for wallet operations |
| `CIRCLE_BLOCKCHAIN` | Server-side | Blockchain network identifier (e.g., "ARC-TESTNET") |
| `CIRCLE_USDC_TOKEN_ID` | Server-side | USDC token ID for the specified blockchain |
| `ADMIN_EMAIL` | Server-side | Email address that determines which user gets admin dashboard access |
| `NEXT_PUBLIC_SUPABASE_URL` | Public | Supabase project URL. |
| `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY` | Public | Supabase anonymous/public key. |
| `SUPABASE_SECRET_KEY` | Server-side | Secret key for privileged writes (e.g., transaction inserts). |
| `CIRCLE_API_KEY` | Server-side | Used to fetch Circle webhook public keys for signature verification. |
| `CIRCLE_ENTITY_SECRET` | Server-side | Circle entity secret for wallet operations. |
| `CIRCLE_BLOCKCHAIN` | Server-side | Blockchain network identifier (e.g., "ARC-TESTNET"). |
| `CIRCLE_USDC_TOKEN_ID` | Server-side | USDC token ID for the specified blockchain. Pre-filled for ARC-TESTNET. |
| `ADMIN_EMAIL` | Server-side | Admin user email address. |

## User Accounts

### Admin Account

On first startup, an admin user is automatically created with the following credentials:

- **Email:** `admin@admin.com`
- **Password:** `123456`

The admin account has access to the **Admin Dashboard**, which provides an overview of all users, wallets, and transactions in the system.

## Usage Notes
Regular users who sign up will see the **User Dashboard**, which allows them to purchase credits with USDC and view their own transaction history.

- Designed for Arc testnet only
- Requires valid Circle API credentials and Supabase configuration
- Admin wallet must have sufficient USDC and gas fees
- ngrok (or similar) required for local webhook testing
### Signup Rate Limits

## Scripts
Supabase limits email signups to **2 per hour** by default (unless custom SMTP is configured). If you hit an "email rate limit exceeded" error during testing:

- **Local Supabase (Docker):** Email verification is handled by the built-in [Inbucket](http://127.0.0.1:54324) mail server — check it to confirm signups. The rate limit can be adjusted in `supabase/config.toml` under `[auth.rate_limit]`.
- **Remote Supabase (Cloud):** Use real email addresses (disposable emails may fail verification). If you hit the limit, you can manually add users via the Supabase dashboard under **Authentication → Users**.
- `npm run dev`: Start Next.js development server with auto-reload
- `npx supabase start`: Start local Supabase instance
- `npx supabase migration up`: Apply database migrations
Expand All @@ -116,4 +158,4 @@ This sample application:
- Verifies webhook signatures for security
- Is not intended for production use without modification

See `SECURITY.md` for vulnerability reporting guidelines. Please report issues privately via Circle's bug bounty program.
See `SECURITY.md` for vulnerability reporting guidelines. Please report issues privately via Circle's bug bounty program.
4 changes: 1 addition & 3 deletions app/api/circle/webhook/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import { NextRequest, NextResponse } from "next/server";
import { supabaseAdminClient } from "@/lib/supabase/admin-client";
import { circleDeveloperSdk } from "@/lib/circle/developer-controlled-wallets-client";
import { convertToSmallestUnit } from "@/lib/utils/convert-to-smallest-unit";
import { encodeFunctionData } from "viem";
import type { Abi } from "viem";
import {
CHAIN_IDS_TO_MESSAGE_TRANSMITTER,
CHAIN_IDS_TO_TOKEN_MESSENGER,
Expand Down Expand Up @@ -418,7 +416,7 @@ async function updateAdminTransactionStatus(
});

if (insertError) {
if ((insertError as any).code === "23505") {
if ("code" in insertError && insertError.code === "23505") {
// Unique constraint hit (e.g., idempotency_key). Treat as success and continue.
console.log("[CCTP] Mint insert deduped by unique constraint.");
} else {
Expand Down
2 changes: 1 addition & 1 deletion app/dashboard/[txHash]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export default async function TransactionDetailsPage(
{event.new_status.charAt(0).toUpperCase() + event.new_status.slice(1)}
</Badge>
</TableCell>
<TableCell>{format(new Date(event.created_at), "PP")}</TableCell>
<TableCell>{format(new Date(event.created_at), "PPpp")}</TableCell>
</TableRow>
))
) : (
Expand Down
4 changes: 2 additions & 2 deletions app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function ProtectedLayout({
<div className="w-full max-w-5xl flex justify-between items-center p-3 px-5 text-sm">
<div className="flex gap-5 items-center font-semibold">
<ThemeSwitcher />
<Link href={"/"}>arc-commerce</Link>
<Link href={"/"}>Arc Commerce</Link>
</div>
<div className="flex items-center gap-3">
{/* The wallet buttons have been removed from here */}
Expand All @@ -51,4 +51,4 @@ export default function ProtectedLayout({
</main>
</WalletProvider>
);
}
}
4 changes: 2 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const defaultUrl = process.env.VERCEL_URL
export const metadata: Metadata = {
metadataBase: new URL(defaultUrl),
title: "Arc Commerce",
description: "Platform credit purchases using USDC and Circle Wallets",
description: "Purchase platform credits using USDC on Arc.",
};

export default function RootLayout({
Expand Down Expand Up @@ -58,4 +58,4 @@ export default function RootLayout({
</body>
</html>
);
}
}
4 changes: 2 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function Home() {
<div className="w-full max-w-5xl flex justify-between items-center p-3 px-5 text-sm">
<div className="flex gap-5 items-center font-semibold">
<ThemeSwitcher />
<Link href={"/"}>arc-commerce</Link>
<Link href={"/"}>Arc Commerce</Link>
</div>
{!hasEnvVars ? <EnvVarWarning /> : <AuthButton />}
</div>
Expand All @@ -42,4 +42,4 @@ export default function Home() {
</div>
</main>
);
}
}
5 changes: 3 additions & 2 deletions components/admin-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/

import type { Database } from "@/types/supabase";
import type { AdminTransaction } from "@/types/admin-transaction";
import { createClient } from "@supabase/supabase-js";
import { AdminWalletsTable } from "@/components/admin-wallets-table/table";
import { columns as walletColumns } from "@/components/admin-wallets-table/columns";
Expand All @@ -26,7 +27,7 @@ import { columns as transactionColumns } from "@/components/admin-transactions-t
export async function AdminDashboard() {
const supabaseAdmin = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
process.env.SUPABASE_SECRET_KEY!
);

// First fetch admin wallets to get their addresses
Expand Down Expand Up @@ -72,7 +73,7 @@ export async function AdminDashboard() {

<AdminWalletsTable columns={walletColumns} data={wallets ?? []} />
{/* Pass the initial data to the table component */}
<AdminTransactionsTable columns={transactionColumns} initialData={transactions as any ?? []} />
<AdminTransactionsTable columns={transactionColumns} initialData={transactions as AdminTransaction[] ?? []} />
</div>
);
}
3 changes: 1 addition & 2 deletions components/admin-wallets-table/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { ColumnDef, Row, Table } from "@tanstack/react-table";
import { Database } from "@/types/supabase";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { chainNameToId } from "@/lib/utils/chain-utils";
import { chainNameToId, getExplorerUrl } from "@/lib/utils/chain-utils";
import {
DropdownMenu,
DropdownMenuContent,
Expand Down Expand Up @@ -239,7 +239,6 @@ export const columns: ColumnDef<Wallet>[] = [

// Convert chain name to numeric ID for the utility function
const chainId = chain ? chainNameToId(chain) : undefined;
const { getExplorerUrl } = require("@/lib/utils/chain-utils");
const explorerUrl = chainId
? getExplorerUrl(chainId, undefined, address)
: `https://testnet.arcscan.app/address/${address}`;
Expand Down
1 change: 1 addition & 0 deletions components/ui/sonner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
duration={8000}
style={
{
"--normal-bg": "var(--popover)",
Expand Down
8 changes: 5 additions & 3 deletions components/user-transactions-table/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export function DataTableToolbar<TData>({ table }: DataTableToolbarProps<TData>)
<SelectItem value="confirmed">Confirmed</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="failed">Failed</SelectItem>
<SelectItem value="complete">Complete</SelectItem>
</SelectContent>
</Select>

Expand All @@ -114,9 +115,10 @@ export function DataTableToolbar<TData>({ table }: DataTableToolbarProps<TData>)
<SelectValue placeholder="Filter by Network" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Ethereum">Ethereum</SelectItem>
<SelectItem value="Polygon">Polygon</SelectItem>
<SelectItem value="Base">Base</SelectItem>
<SelectItem value="11155111">Ethereum Sepolia</SelectItem>
<SelectItem value="43113">Avalanche Fuji</SelectItem>
<SelectItem value="84532">Base Sepolia</SelectItem>
<SelectItem value="5042002">Arc Testnet</SelectItem>
</SelectContent>
</Select>

Expand Down
Loading