11import { Hono } from 'hono' ;
22import { EmailMessage } from 'cloudflare:email' ;
33import { createMimeMessage } from 'mimetext' ;
4- import { createPublicClient , http , parseAbi , type Hex } from 'viem' ;
5- import { baseSepolia } from 'viem/chains' ;
4+ import { createPublicClient , http , parseAbi , type Hex , type Chain } from 'viem' ;
5+ import { base , baseSepolia } from 'viem/chains' ;
66import { AppBindings } from '../types' ;
77import { authMiddleware } from '../auth' ;
88
9- // ── USDC Hackathon (TESTNET ONLY — Base Sepolia) ──
10- const BASE_SEPOLIA_RPC = 'https://sepolia.base.org' ;
11- const BASE_SEPOLIA_USDC = '0x036CbD53842c5426634e7929541eC2318f3dCF7e' ;
9+ // ── USDC Network Configs ──
10+ const USDC_NETWORKS : Record < string , { chain : Chain ; rpc : string ; usdc : string ; label : string ; explorer : string } > = {
11+ 'base-mainnet' : {
12+ chain : base ,
13+ rpc : 'https://mainnet.base.org' ,
14+ usdc : '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' ,
15+ label : 'Base Mainnet' ,
16+ explorer : 'https://basescan.org' ,
17+ } ,
18+ 'base-sepolia' : {
19+ chain : baseSepolia ,
20+ rpc : 'https://sepolia.base.org' ,
21+ usdc : '0x036CbD53842c5426634e7929541eC2318f3dCF7e' ,
22+ label : 'Base Sepolia (Testnet)' ,
23+ explorer : 'https://sepolia.basescan.org' ,
24+ } ,
25+ } ;
1226const USDC_TRANSFER_ABI = parseAbi ( [ 'event Transfer(address indexed from, address indexed to, uint256 value)' ] ) ;
1327
1428export const sendRoutes = new Hono < AppBindings > ( ) ;
1529
1630sendRoutes . use ( '/*' , authMiddleware ( ) ) ;
1731
32+ // Auto-migrate: add usdc columns if missing
33+ let migrated = false ;
34+ sendRoutes . use ( '/*' , async ( c , next ) => {
35+ if ( ! migrated ) {
36+ migrated = true ;
37+ for ( const col of [ 'usdc_amount TEXT' , 'usdc_tx TEXT' , 'usdc_network TEXT' ] ) {
38+ try { await c . env . DB . prepare ( `ALTER TABLE emails ADD COLUMN ${ col } ` ) . run ( ) ; } catch { }
39+ }
40+ }
41+ await next ( ) ;
42+ } ) ;
43+
1844// ── Email Signature (appended for free-tier users) ──
1945const TEXT_SIGNATURE = `\n\n--\nSent via BaseMail.ai — Email Identity for AI Agents on Base\nhttps://basemail.ai` ;
2046
@@ -29,6 +55,7 @@ interface Attachment {
2955interface UsdcPayment {
3056 tx_hash : string ;
3157 amount : string ; // human-readable e.g. "10.00"
58+ network ?: string ; // 'base-mainnet' | 'base-sepolia' (default: 'base-sepolia' for backward compat)
3259}
3360
3461/**
@@ -86,12 +113,18 @@ sendRoutes.post('/', async (c) => {
86113 }
87114 }
88115
89- // ── USDC Payment Verification (Base Sepolia TESTNET ) ──
90- let verifiedUsdc : { amount : string ; tx_hash : string } | null = null ;
116+ // ── USDC Payment Verification (supports Base Mainnet + Base Sepolia ) ──
117+ let verifiedUsdc : { amount : string ; tx_hash : string ; network : string } | null = null ;
91118
92119 if ( usdc_payment ?. tx_hash ) {
120+ const networkKey = usdc_payment . network || 'base-sepolia' ;
121+ const netConfig = USDC_NETWORKS [ networkKey ] ;
122+ if ( ! netConfig ) {
123+ return c . json ( { error : `Unsupported USDC network: ${ networkKey } . Use 'base-mainnet' or 'base-sepolia'` } , 400 ) ;
124+ }
125+
93126 try {
94- const client = createPublicClient ( { chain : baseSepolia , transport : http ( BASE_SEPOLIA_RPC ) } ) ;
127+ const client = createPublicClient ( { chain : netConfig . chain , transport : http ( netConfig . rpc ) } ) ;
95128 const receipt = await client . waitForTransactionReceipt ( {
96129 hash : usdc_payment . tx_hash as Hex ,
97130 timeout : 15_000 ,
@@ -103,7 +136,7 @@ sendRoutes.post('/', async (c) => {
103136
104137 // Parse Transfer events from USDC contract
105138 const transferLog = receipt . logs . find (
106- ( log ) => log . address . toLowerCase ( ) === BASE_SEPOLIA_USDC . toLowerCase ( ) && log . topics [ 0 ] === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
139+ ( log ) => log . address . toLowerCase ( ) === netConfig . usdc . toLowerCase ( ) && log . topics [ 0 ] === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
107140 ) ;
108141
109142 if ( ! transferLog || ! transferLog . topics [ 1 ] || ! transferLog . topics [ 2 ] ) {
@@ -133,7 +166,7 @@ sendRoutes.post('/', async (c) => {
133166 return c . json ( { error : 'USDC recipient does not match email recipient wallet' } , 400 ) ;
134167 }
135168
136- verifiedUsdc = { amount : humanAmount , tx_hash : usdc_payment . tx_hash } ;
169+ verifiedUsdc = { amount : humanAmount , tx_hash : usdc_payment . tx_hash , network : networkKey } ;
137170 } catch ( e : any ) {
138171 return c . json ( { error : `USDC verification failed: ${ e . message } ` } , 400 ) ;
139172 }
@@ -178,7 +211,7 @@ sendRoutes.post('/', async (c) => {
178211 if ( verifiedUsdc ) {
179212 msg . setHeader ( 'X-BaseMail-USDC-Payment' , `${ verifiedUsdc . amount } USDC` ) ;
180213 msg . setHeader ( 'X-BaseMail-USDC-TxHash' , verifiedUsdc . tx_hash ) ;
181- msg . setHeader ( 'X-BaseMail-USDC-Network' , 'Base Sepolia (Testnet)' ) ;
214+ msg . setHeader ( 'X-BaseMail-USDC-Network' , USDC_NETWORKS [ verifiedUsdc . network ] ?. label || verifiedUsdc . network ) ;
182215 }
183216
184217 // Reply headers
@@ -257,8 +290,8 @@ sendRoutes.post('/', async (c) => {
257290 await c . env . EMAIL_STORE . put ( inboxR2Key , rawMime ) ;
258291
259292 await c . env . DB . prepare (
260- `INSERT INTO emails (id, handle, folder, from_addr, to_addr, subject, snippet, r2_key, size, read, created_at, usdc_amount, usdc_tx)
261- VALUES (?, ?, 'inbox', ?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`
293+ `INSERT INTO emails (id, handle, folder, from_addr, to_addr, subject, snippet, r2_key, size, read, created_at, usdc_amount, usdc_tx, usdc_network )
294+ VALUES (?, ?, 'inbox', ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ? )`
262295 ) . bind (
263296 inboxEmailId ,
264297 recipientHandle ,
@@ -271,6 +304,7 @@ sendRoutes.post('/', async (c) => {
271304 now ,
272305 verifiedUsdc ?. amount || null ,
273306 verifiedUsdc ?. tx_hash || null ,
307+ verifiedUsdc ?. network || null ,
274308 ) . run ( ) ;
275309 } else {
276310 // ── External sending (paid, costs 1 credit) ──
@@ -372,8 +406,8 @@ sendRoutes.post('/', async (c) => {
372406 await c . env . EMAIL_STORE . put ( sentR2Key , rawMime ) ;
373407
374408 await c . env . DB . prepare (
375- `INSERT INTO emails (id, handle, folder, from_addr, to_addr, subject, snippet, r2_key, size, read, created_at, usdc_amount, usdc_tx)
376- VALUES (?, ?, 'sent', ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)`
409+ `INSERT INTO emails (id, handle, folder, from_addr, to_addr, subject, snippet, r2_key, size, read, created_at, usdc_amount, usdc_tx, usdc_network )
410+ VALUES (?, ?, 'sent', ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ? )`
377411 ) . bind (
378412 emailId ,
379413 auth . handle ,
@@ -386,6 +420,7 @@ sendRoutes.post('/', async (c) => {
386420 now ,
387421 verifiedUsdc ?. amount || null ,
388422 verifiedUsdc ?. tx_hash || null ,
423+ verifiedUsdc ?. network || null ,
389424 ) . run ( ) ;
390425
391426 // Auto-resolve attention bond if replying to a bonded email
@@ -426,7 +461,7 @@ sendRoutes.post('/', async (c) => {
426461 verified : true ,
427462 amount : verifiedUsdc . amount ,
428463 tx_hash : verifiedUsdc . tx_hash ,
429- network : 'Base Sepolia (Testnet)' ,
464+ network : USDC_NETWORKS [ verifiedUsdc . network ] ?. label || verifiedUsdc . network ,
430465 } ,
431466 } : { } ) ,
432467 } ) ;
0 commit comments