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
98 changes: 98 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { parseAbi } from 'viem'
import type { CredentialType } from './types.js'

export type SupportedChain =
| 'base'
Expand All @@ -10,6 +11,12 @@ export type SupportedChain =
| 'linea'
| 'unichain'
| 'base-sepolia'
| 'ethereum-sepolia'
| 'arbitrum-sepolia'
| 'optimism-sepolia'
| 'polygon-amoy'
| 'scroll-sepolia'
| 'linea-sepolia'

export const CHAIN_IDS: Record<SupportedChain, number> = {
base: 8453,
Expand All @@ -21,6 +28,12 @@ export const CHAIN_IDS: Record<SupportedChain, number> = {
linea: 59144,
unichain: 130,
'base-sepolia': 84532,
'ethereum-sepolia': 11155111,
'arbitrum-sepolia': 421614,
'optimism-sepolia': 11155420,
'polygon-amoy': 80002,
'scroll-sepolia': 534351,
'linea-sepolia': 59141,
}

export const USDC_CONTRACTS: Record<SupportedChain, `0x${string}`> = {
Expand All @@ -33,6 +46,12 @@ export const USDC_CONTRACTS: Record<SupportedChain, `0x${string}`> = {
linea: '0x176211869cA2b568f2A7D4EE941E073a821EE1ff',
unichain: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
'base-sepolia': '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
'ethereum-sepolia': '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
'arbitrum-sepolia': '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
'optimism-sepolia': '0x5fd84259d66Cd46123540766Be93DFE6D43130D7',
'polygon-amoy': '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582',
'scroll-sepolia': '0x4d7ff95a5e86b0aaade01df5adadded72c54a698',
'linea-sepolia': '0xFEce4462D57bD51A6A552365A011b95f0E16d9B7',
}

export const DEFAULT_CONFIRMATIONS: Record<SupportedChain, number> = {
Expand All @@ -45,6 +64,79 @@ export const DEFAULT_CONFIRMATIONS: Record<SupportedChain, number> = {
linea: 1,
unichain: 1,
'base-sepolia': 1,
'ethereum-sepolia': 0,
'arbitrum-sepolia': 0,
'optimism-sepolia': 0,
'polygon-amoy': 0,
'scroll-sepolia': 0,
'linea-sepolia': 0,
}

export type SupportedToken = 'USDC' | 'EURC' | 'WETH' | 'USDT'

// `Partial` because not every token is deployed on every chain. Admin layer
// must reject creating a payment option for a (chain, token) pair that
// resolves to undefined.
//
// Provenance rule (see agent-proxy/docs/decisions.md): every entry must be
// either issuer-deployed (Circle for USDC/EURC, Tether for mainnet USDT,
// chain team for canonical WETH wrappers) or verified on-chain against a
// known audited bytecode hash. Community-deployed tokens are not first-class.
//
// WETH carve-out: only canonical native ETH wrappers are listed. Polygon
// (native MATIC) and Avalanche (native AVAX) carry "WETH" only as bridged
// assets, which is a different trust model — those entries are intentionally
// absent.
export const TOKEN_CONTRACTS: Record<
SupportedToken,
Partial<Record<SupportedChain, `0x${string}`>>
> = {
USDC: { ...USDC_CONTRACTS },
EURC: {
ethereum: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
base: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
avalanche: '0xC891EB4cbdEFf6e073e859e987815Ed1505c2ACD',
'ethereum-sepolia': '0x08210f9170f89ab7658f0b5e3ff39b0e03c594d4',
},
WETH: {
ethereum: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
base: '0x4200000000000000000000000000000000000006',
optimism: '0x4200000000000000000000000000000000000006',
arbitrum: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
linea: '0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f',
unichain: '0x4200000000000000000000000000000000000006',
'ethereum-sepolia': '0x7b79995e5f793a07bc00c21412e50ecae098e7f9',
'arbitrum-sepolia': '0x2836ae2ea2c013acd38028fd0c77b92cccfa2ee4',
'polygon-amoy': '0x52ef3d68bab452a294342dc3e5f464d7f610f72e',
'scroll-sepolia': '0x5300000000000000000000000000000000000004',
},
// Mainnet only — Tether issues these. Testnet USDT is intentionally absent
// because the candidates are community deployments without provenance.
USDT: {
ethereum: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
arbitrum: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9',
optimism: '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58',
polygon: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
avalanche: '0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7',
},
}

export const TOKEN_DECIMALS: Record<SupportedToken, number> = {
USDC: 6,
EURC: 6,
WETH: 18,
USDT: 6,
}

// Per-token credential-type compatibility. EIP-3009 transferWithAuthorization
// is implemented by Circle's FiatToken family (USDC, EURC). WETH and USDT
// lack it (USDT mainnet uses its own non-standard approve/transferFrom),
// so those tokens accept Permit2 + on-chain hash only.
export const TOKEN_CREDENTIAL_TYPES: Record<SupportedToken, readonly CredentialType[]> = {
USDC: ['permit2', 'authorization', 'hash'],
EURC: ['permit2', 'authorization', 'hash'],
WETH: ['permit2', 'hash'],
USDT: ['permit2', 'hash'],
}

export const PERMIT2_ADDRESS: `0x${string}` = '0x000000000022D473030F116dDEE9F6B43aC78BA3'
Expand Down Expand Up @@ -91,6 +183,12 @@ export const CHAIN_SLUGS: Record<SupportedChain, string | null> = {
linea: 'linea-mainnet',
unichain: 'unichain-mainnet',
'base-sepolia': 'base-sepolia',
'ethereum-sepolia': 'ethereum-sepolia',
'arbitrum-sepolia': 'arbitrum-sepolia',
'optimism-sepolia': 'optimism-sepolia',
'polygon-amoy': 'matic-amoy',
'scroll-sepolia': 'scroll-sepolia',
'linea-sepolia': 'linea-sepolia',
}

/**
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export type { SupportedChain } from './constants.js'
export type { SupportedChain, SupportedToken } from './constants.js'
export {
TOKEN_CONTRACTS,
TOKEN_CREDENTIAL_TYPES,
TOKEN_DECIMALS,
} from './constants.js'
export { QuickNodeRateLimitError } from './errors.js'
export { charge } from './Methods.js'
export type {
Expand Down
12 changes: 12 additions & 0 deletions src/internal/chain.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import type { Chain } from 'viem'
import {
arbitrum,
arbitrumSepolia,
avalanche,
base,
baseSepolia,
linea,
lineaSepolia,
mainnet,
optimism,
optimismSepolia,
polygon,
polygonAmoy,
scrollSepolia,
sepolia,
unichain,
} from 'viem/chains'
import type { SupportedChain } from '../constants.js'
Expand All @@ -22,6 +28,12 @@ const chains: Record<SupportedChain, Chain> = {
linea,
unichain,
'base-sepolia': baseSepolia,
'ethereum-sepolia': sepolia,
'arbitrum-sepolia': arbitrumSepolia,
'optimism-sepolia': optimismSepolia,
'polygon-amoy': polygonAmoy,
'scroll-sepolia': scrollSepolia,
'linea-sepolia': lineaSepolia,
}

export function getViemChain(chain: SupportedChain): Chain {
Expand Down
48 changes: 37 additions & 11 deletions src/server/Charge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@ import {
defaultRpcUrl,
ERC20_ABI,
PERMIT2_ADDRESS,
USDC_CONTRACTS,
type SupportedToken,
TOKEN_CONTRACTS,
TOKEN_CREDENTIAL_TYPES,
TOKEN_DECIMALS,
} from '../constants.js'
import { resolveSigner } from '../internal/account.js'
import { logDefaultTransportOnce } from '../internal/transport.js'
import { charge as chargeMethod } from '../Methods.js'
import {
type AuthorizationPayload,
type CredentialType,
credentialTypes,
type HashPayload,
type Permit2Payload,
type ServerParameters,
import type {
AuthorizationPayload,
CredentialType,
HashPayload,
Permit2Payload,
ServerParameters,
} from '../types.js'
import { getPublicClient, getWalletClient } from './rpc.js'
import { verifyAuthorization } from './verifiers/authorization.js'
Expand Down Expand Up @@ -52,8 +54,32 @@ export function charge(parameters: ServerParameters) {
store: storeInput,
submitter,
} = parameters
const acceptedTypes: readonly CredentialType[] = acceptedTypesInput ?? credentialTypes
const tokenAddress = USDC_CONTRACTS[chain]
const tokenSymbol: SupportedToken = parameters.token ?? 'USDC'
Comment thread
cursor[bot] marked this conversation as resolved.
const tokenAddress = TOKEN_CONTRACTS[tokenSymbol]?.[chain]
if (!tokenAddress) {
const supportedOnChain = (Object.keys(TOKEN_CONTRACTS) as SupportedToken[])
.filter((t) => TOKEN_CONTRACTS[t][chain])
.join(', ')
throw new Error(
`${tokenSymbol} is not deployed on ${chain}. Supported tokens for this chain: ${
supportedOnChain || '(none)'
}`,
)
}
const tokenDecimals = TOKEN_DECIMALS[tokenSymbol]
const allowedTypes = TOKEN_CREDENTIAL_TYPES[tokenSymbol]
// When the caller omits `credentialTypes`, default to the per-token allowed
// set rather than the universal one. Otherwise tokens like WETH and USDT —
// which lack EIP-3009 — would throw on every zero-config construction
// because the universal default includes 'authorization'.
const acceptedTypes: readonly CredentialType[] = acceptedTypesInput ?? allowedTypes
const invalidTypes = acceptedTypes.filter((t) => !allowedTypes.includes(t))
if (invalidTypes.length) {
throw new Error(
`${tokenSymbol} does not support credential types: ${invalidTypes.join(', ')}. ` +
`Supported on ${tokenSymbol}: ${allowedTypes.join(', ')}.`,
)
}
const chainId = CHAIN_IDS[chain]
const confirmations = confirmationsInput ?? DEFAULT_CONFIRMATIONS[chain]
const store = storeInput ?? (Store.memory() as NonNullable<ServerParameters['store']>)
Expand Down Expand Up @@ -111,7 +137,7 @@ export function charge(parameters: ServerParameters) {
return Method.toServer(chargeMethod, {
defaults: {
currency: tokenAddress,
decimals: 6,
decimals: tokenDecimals,
recipient,
},
async request({ request }) {
Expand Down
12 changes: 9 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Account, Address, Hash, Hex } from 'viem'
import type { SupportedChain } from './constants.js'
import type { SupportedChain, SupportedToken } from './constants.js'

export const credentialTypes = ['permit2', 'authorization', 'hash'] as const

Expand Down Expand Up @@ -48,8 +48,14 @@ export type ServerParameters = {
rpcUrl?: string
/** Target chain for settlement. */
chain: SupportedChain
/** ERC-20 token symbol — resolves to a contract address via the chain's token map. @default 'USDC' */
token?: 'USDC'
/**
* ERC-20 token symbol — resolves to a contract address via TOKEN_CONTRACTS.
* Testnet token availability is sparse outside USDC: EURC ships only on
* ethereum-sepolia, WETH on a subset of testnets. Authorization (EIP-3009)
* is supported only by Circle's FiatToken family (USDC, EURC); WETH is
* permit2 + hash only. @default 'USDC'
*/
token?: SupportedToken
/**
* Accepted credential types, advertised in the challenge. Ordered from most to
* least preferred (draft-evm-charge-00 §3). @default ['permit2','authorization','hash']
Expand Down
Loading