Skip to content

Commit d85ae50

Browse files
zepeng811dalgrandeakelani-circlehjchen-circle
authored
merge Implement multichain wallet architecture with Gateway improvements (#5) into master (#6)
this is for merging #5 to master, squash merged the PR into a temp branch to workaround commit signing issues. Co-authored-by: Frederico Dal Grande <32470527+dalgrande@users.noreply.github.com> Co-authored-by: Anthony Kelani <anthony.kelani@circle.com> Co-authored-by: HJ Chen <221941357+hjchen-circle@users.noreply.github.com>
1 parent e27b7b5 commit d85ae50

File tree

20 files changed

+15745
-125
lines changed

20 files changed

+15745
-125
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ jobs:
1818
- name: Install dependencies
1919
run: npm install
2020

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

26-
release-sbom:
27-
needs: lint-and-test
28-
if: github.event_name == 'push'
29-
uses: circlefin/circle-public-github-workflows/.github/workflows/attach-release-assets.yaml@v1
26+
# release-sbom:
27+
# needs: lint-and-test
28+
# if: github.event_name == 'push'
29+
# uses: circlefin/circle-public-github-workflows/.github/workflows/attach-release-assets.yaml@v1

README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ A sample application demonstrating how to build optimal USDC interoperability UX
2020
cd arc-multichain-wallet
2121
npm install
2222
```
23-
23+
2424
2. Create a `.env.local` file in the project root:
2525

2626
```bash
@@ -39,10 +39,21 @@ A sample application demonstrating how to build optimal USDC interoperability UX
3939
CIRCLE_ENTITY_SECRET=your_entity_secret
4040
```
4141

42-
3. Start Supabase locally:
42+
3. Set up Supabase (Local)
43+
This project uses **local Supabase** via Docker for development:
4344

4445
```bash
46+
# Start local Supabase (requires Docker)
4547
npx supabase start
48+
49+
# Push database migrations
50+
npx supabase db push
51+
```
52+
**Note:** If you prefer cloud-hosted Supabase, you can use:
53+
54+
```bash
55+
npx supabase link
56+
npx supabase db push
4657
```
4758

4859
4. Start the development server:
@@ -108,6 +119,34 @@ This sample application:
108119

109120
See `SECURITY.md` for vulnerability reporting guidelines. Please report issues privately via Circle's bug bounty program.
110121

122+
## Getting Testnet USDC
123+
124+
To test the application, you'll need testnet USDC on the supported chains. Use the Circle Faucet to get free testnet tokens:
125+
126+
### Using the Circle Faucet
127+
128+
1. **Get Your Wallet Address**: After signing up, your Circle Wallet addresses will be displayed in the dashboard
129+
2. **Visit the Faucet**: Go to [https://faucet.circle.com/](https://faucet.circle.com/)
130+
3. **Request Tokens**:
131+
- Enter your wallet address
132+
- Select the desired testnet (Arc Testnet, Base Sepolia, or Avalanche Fuji)
133+
- Request USDC
134+
4. **Wait for Confirmation**: Transactions typically confirm within a few minutes
135+
5. **Deposit to Gateway**: Once received, use the "Deposit" tab to add USDC to your Gateway balance
136+
137+
### Supported Testnets
138+
139+
- **Arc Testnet**: Primary chain for deposits and Gateway operations
140+
- **Base Sepolia**: Ethereum Layer 2 testnet
141+
- **Avalanche Fuji**: Avalanche testnet
142+
143+
### Note on Gas Fees
144+
145+
When transferring USDC cross-chain, you'll need native tokens on the destination chain to pay for gas fees:
146+
- **Arc Testnet**: USDC (no additional gas token needed)
147+
- **Base Sepolia**: ETH (get from [Base Sepolia Faucet](https://www.alchemy.com/faucets/base-sepolia))
148+
- **Avalanche Fuji**: AVAX (get from [Avalanche Faucet](https://core.app/tools/testnet-faucet/))
149+
111150
## Resources
112151

113152
- [Circle Gateway Documentation](https://developers.circle.com/gateway)

app/api/gateway/deposit/route.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
import { createClient } from "@/lib/supabase/server";
2525

2626
export async function POST(req: NextRequest) {
27+
let requestBody: any = {};
28+
2729
try {
2830
const supabase = await createClient();
2931
const {
@@ -34,7 +36,8 @@ export async function POST(req: NextRequest) {
3436
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
3537
}
3638

37-
const { chain, amount } = await req.json();
39+
requestBody = await req.json();
40+
const { chain, amount } = requestBody;
3841

3942
if (!chain || !amount) {
4043
return NextResponse.json(
@@ -73,26 +76,42 @@ export async function POST(req: NextRequest) {
7376

7477
const amountInAtomicUnits = BigInt(Math.floor(parsedAmount * 1_000_000));
7578

76-
// Custodial flow (Circle Wallet)
79+
// Get the user's multichain SCA wallet
7780
const { data: wallets, error: walletError } = await supabase
7881
.from("wallets")
79-
.select("circle_wallet_id")
82+
.select("circle_wallet_id, wallet_set_id, address")
8083
.eq("user_id", user.id)
84+
.eq("type", "sca")
8185
.limit(1);
8286

83-
if (walletError || !wallets || wallets.length === 0) {
87+
if (walletError) {
88+
console.error("Database error fetching wallets:", walletError);
8489
return NextResponse.json(
85-
{ error: "No Circle wallet found for this user." },
90+
{ error: "Database error when fetching wallets." },
91+
{ status: 500 }
92+
);
93+
}
94+
95+
if (!wallets || wallets.length === 0) {
96+
console.log(`No SCA wallet found for user ${user.id}`);
97+
return NextResponse.json(
98+
{ error: "No Circle wallet found. Please ensure wallet is created during signup." },
8699
{ status: 404 }
87100
);
88101
}
89102

90103
const wallet = wallets[0];
91104

105+
// Get or create EOA signer wallet (multichain)
106+
const { getOrCreateGatewayEOAWallet } = await import("@/lib/circle/create-gateway-eoa-wallets");
107+
const { address: eoaAddress } = await getOrCreateGatewayEOAWallet(user.id, chain);
108+
109+
// Deposit to Gateway and add EOA as delegate (allows EOA to sign burn intents)
92110
const txHash = await initiateDepositFromCustodialWallet(
93111
wallet.circle_wallet_id,
94112
chain as SupportedChain,
95-
amountInAtomicUnits
113+
amountInAtomicUnits,
114+
eoaAddress as `0x${string}`
96115
);
97116

98117
// Store transaction in database
@@ -126,14 +145,13 @@ export async function POST(req: NextRequest) {
126145
data: { user },
127146
} = await supabase.auth.getUser();
128147

129-
if (user) {
130-
const body = await req.json();
148+
if (user && requestBody.chain) {
131149
await supabase.from("transaction_history").insert([
132150
{
133151
user_id: user.id,
134-
chain: body.chain,
152+
chain: requestBody.chain,
135153
tx_type: "deposit",
136-
amount: parseFloat(body.amount || 0),
154+
amount: parseFloat(requestBody.amount || 0),
137155
status: "failed",
138156
reason: error.message || "Unknown error",
139157
created_at: new Date().toISOString(),
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Copyright 2026 Circle Internet Group, Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
19+
import { NextRequest, NextResponse } from "next/server";
20+
import { createClient } from "@/lib/supabase/server";
21+
import { circleDeveloperSdk } from "@/lib/circle/sdk";
22+
import type { SupportedChain } from "@/lib/circle/gateway-sdk";
23+
24+
export async function GET(req: NextRequest) {
25+
const supabase = await createClient();
26+
const {
27+
data: { user },
28+
} = await supabase.auth.getUser();
29+
30+
if (!user) {
31+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
32+
}
33+
34+
try {
35+
const { getGatewayEOAWalletId } = await import("@/lib/circle/create-gateway-eoa-wallets");
36+
37+
const chains = ["BASE-SEPOLIA", "AVAX-FUJI", "ARC-TESTNET"];
38+
const chainMap: Record<string, SupportedChain> = {
39+
"BASE-SEPOLIA": "baseSepolia",
40+
"AVAX-FUJI": "avalancheFuji",
41+
"ARC-TESTNET": "arcTestnet",
42+
};
43+
44+
const wallets = await Promise.all(
45+
chains.map(async (blockchain) => {
46+
try {
47+
const { walletId, address } = await getGatewayEOAWalletId(user.id, blockchain);
48+
return {
49+
chain: chainMap[blockchain],
50+
blockchain,
51+
walletId,
52+
address,
53+
};
54+
} catch (error) {
55+
console.error(`Error fetching EOA wallet for ${blockchain}:`, error);
56+
return null;
57+
}
58+
})
59+
);
60+
61+
return NextResponse.json({
62+
wallets: wallets.filter(w => w !== null),
63+
});
64+
} catch (error: any) {
65+
console.error("Error fetching EOA wallets:", error);
66+
return NextResponse.json(
67+
{ error: error.message || "Failed to fetch EOA wallets" },
68+
{ status: 500 }
69+
);
70+
}
71+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Copyright 2026 Circle Internet Group, Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
19+
import { NextRequest, NextResponse } from "next/server";
20+
import { createClient } from "@/lib/supabase/server";
21+
import { storeGatewayEOAWalletForUser } from "@/lib/circle/create-gateway-eoa-wallets";
22+
23+
export async function POST(req: NextRequest) {
24+
const supabase = await createClient();
25+
const {
26+
data: { user },
27+
} = await supabase.auth.getUser();
28+
29+
if (!user) {
30+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
31+
}
32+
33+
try {
34+
// Check if EOA wallet already exists for this user
35+
const { data: existingWallet } = await supabase
36+
.from("wallets")
37+
.select("circle_wallet_id, address")
38+
.eq("user_id", user.id)
39+
.eq("type", "gateway_signer")
40+
.limit(1);
41+
42+
if (existingWallet && existingWallet.length > 0) {
43+
return NextResponse.json({
44+
success: true,
45+
message: "Gateway EOA wallet already exists for this user",
46+
wallet: existingWallet[0],
47+
});
48+
}
49+
50+
// Get the user's wallet_set_id from their SCA wallet
51+
const { data: scaWallet, error: scaError } = await supabase
52+
.from("wallets")
53+
.select("wallet_set_id")
54+
.eq("user_id", user.id)
55+
.eq("type", "sca")
56+
.limit(1)
57+
.single();
58+
59+
if (scaError || !scaWallet) {
60+
return NextResponse.json(
61+
{ error: "No SCA wallet found. Please create a wallet first." },
62+
{ status: 404 }
63+
);
64+
}
65+
66+
// Create multichain EOA wallet for the user
67+
const wallet = await storeGatewayEOAWalletForUser(user.id, scaWallet.wallet_set_id);
68+
69+
return NextResponse.json({
70+
success: true,
71+
message: "Gateway EOA wallet created successfully",
72+
wallet: wallet[0],
73+
});
74+
} catch (error: any) {
75+
console.error("Error initializing EOA wallet:", error);
76+
return NextResponse.json(
77+
{ error: error.message || "Failed to initialize EOA wallet" },
78+
{ status: 500 }
79+
);
80+
}
81+
}

0 commit comments

Comments
 (0)