-
Notifications
You must be signed in to change notification settings - Fork 89
Local Facilitation as Facilitator of last resort #631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
9c02d1a
0928790
0ec88d5
9c46ba6
662aad8
fd2027c
8f9cfb0
ad9b203
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,11 @@ | ||
| { | ||
| "title": "Components", | ||
| "pages": [ | ||
| "index", | ||
| "installation", | ||
| "echo-account", | ||
| "ui-components", | ||
| "customization" | ||
| ], | ||
| "icon": "Component" | ||
| } | ||
| "title": "Components", | ||
| "pages": [ | ||
| "index", | ||
| "installation", | ||
| "echo-account", | ||
| "ui-components", | ||
| "customization" | ||
| ], | ||
| "icon": "Component" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| import { | ||
| getAddress, | ||
| Hex, | ||
| parseErc6492Signature, | ||
| Address, | ||
| encodeFunctionData, | ||
| Abi, | ||
| } from 'viem'; | ||
| import { getNetworkId, getERC20Balance } from './evmUtils'; | ||
| import { | ||
| PaymentPayload, | ||
| PaymentRequirements, | ||
| VerifyResponse, | ||
| SettleResponse, | ||
| ExactEvmPayloadSchema, | ||
| } from './x402-types'; | ||
| import { USDC_ADDRESS_BY_NETWORK } from '../../constants'; | ||
| import { ERC3009_ABI } from '../fund-repo/constants'; | ||
| import { getSmartAccount } from '../../utils'; | ||
| import logger from 'logger'; | ||
|
|
||
| const SCHEME = 'exact'; | ||
|
|
||
| export async function verify( | ||
| payload: PaymentPayload, | ||
| paymentRequirements: PaymentRequirements | ||
| ): Promise<VerifyResponse> { | ||
| if (payload.scheme !== SCHEME || paymentRequirements.scheme !== SCHEME) { | ||
| return { | ||
| isValid: false, | ||
| invalidReason: 'unsupported_scheme', | ||
| payer: undefined, | ||
| }; | ||
| } | ||
|
|
||
| const parseResult = ExactEvmPayloadSchema.safeParse(payload.payload); | ||
|
|
||
| if (!parseResult.success) { | ||
| return { | ||
| isValid: false, | ||
| invalidReason: 'invalid_payload', | ||
| payer: undefined, | ||
| }; | ||
| } | ||
|
|
||
| const exactEvmPayload = parseResult.data; | ||
|
|
||
| const network = payload.network; | ||
| const chainId = getNetworkId(network); | ||
| const erc20Address = paymentRequirements.asset as Address; | ||
|
|
||
| if (!chainId) { | ||
| return { | ||
| isValid: false, | ||
| invalidReason: 'invalid_network', | ||
| payer: exactEvmPayload.authorization.from, | ||
| }; | ||
| } | ||
|
|
||
| if (erc20Address !== USDC_ADDRESS_BY_NETWORK[network]) { | ||
| return { | ||
| isValid: false, | ||
| invalidReason: 'invalid_payment', | ||
| payer: exactEvmPayload.authorization.from, | ||
| }; | ||
| } | ||
|
|
||
| // Verify that payment was made to the correct address | ||
| if (getAddress(exactEvmPayload.authorization.to) !== getAddress(paymentRequirements.payTo)) { | ||
| return { | ||
| isValid: false, | ||
| invalidReason: 'invalid_exact_evm_payload_recipient_mismatch', | ||
| payer: exactEvmPayload.authorization.from, | ||
| }; | ||
| } | ||
|
|
||
| // Verify deadline is not yet expired (pad 3 blocks = 6 seconds) | ||
| if ( | ||
| BigInt(exactEvmPayload.authorization.validBefore) < BigInt(Math.floor(Date.now() / 1000) + 6) | ||
| ) { | ||
| return { | ||
| isValid: false, | ||
| invalidReason: 'invalid_exact_evm_payload_authorization_valid_before', | ||
| payer: exactEvmPayload.authorization.from, | ||
| }; | ||
| } | ||
|
|
||
| // Verify deadline is not yet valid | ||
| if (BigInt(exactEvmPayload.authorization.validAfter) > BigInt(Math.floor(Date.now() / 1000))) { | ||
| return { | ||
| isValid: false, | ||
| invalidReason: 'invalid_exact_evm_payload_authorization_valid_after', | ||
| payer: exactEvmPayload.authorization.from, | ||
| }; | ||
| } | ||
|
|
||
| // Verify client has enough funds to cover paymentRequirements.maxAmountRequired | ||
| const balance = await getERC20Balance( | ||
| network, | ||
| erc20Address, | ||
| exactEvmPayload.authorization.from as Address | ||
| ); | ||
|
|
||
| if (balance < BigInt(paymentRequirements.maxAmountRequired)) { | ||
| return { | ||
| isValid: false, | ||
| invalidReason: 'insufficient_funds', | ||
| payer: exactEvmPayload.authorization.from, | ||
| }; | ||
| } | ||
|
|
||
| // Verify value in payload is enough to cover paymentRequirements.maxAmountRequired | ||
| if (BigInt(exactEvmPayload.authorization.value) < BigInt(paymentRequirements.maxAmountRequired)) { | ||
| return { | ||
| isValid: false, | ||
| invalidReason: 'invalid_exact_evm_payload_authorization_value', | ||
| payer: exactEvmPayload.authorization.from, | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| isValid: true, | ||
| invalidReason: undefined, | ||
| payer: exactEvmPayload.authorization.from, | ||
| }; | ||
| } | ||
|
|
||
| export async function settle( | ||
| paymentPayload: PaymentPayload, | ||
| paymentRequirements: PaymentRequirements | ||
| ): Promise<SettleResponse> { | ||
| const valid = await verify(paymentPayload, paymentRequirements); | ||
|
|
||
| if (!valid.isValid) { | ||
| return { | ||
| success: false, | ||
| network: paymentPayload.network, | ||
| transaction: '', | ||
| errorReason: valid.invalidReason ?? 'invalid_scheme', | ||
| payer: valid.payer, | ||
| }; | ||
| } | ||
|
|
||
| const parseResult = ExactEvmPayloadSchema.safeParse(paymentPayload.payload); | ||
|
|
||
| if (!parseResult.success) { | ||
| return { | ||
| success: false, | ||
| network: paymentPayload.network, | ||
| transaction: '', | ||
| errorReason: 'invalid_payload', | ||
| payer: undefined, | ||
| }; | ||
| } | ||
|
|
||
| const payload = parseResult.data; | ||
|
|
||
| const { signature } = parseErc6492Signature(payload.signature as Hex); | ||
|
|
||
| const { smartAccount } = await getSmartAccount(); | ||
|
|
||
| const callData = encodeFunctionData({ | ||
| abi: ERC3009_ABI as Abi, | ||
| functionName: 'transferWithAuthorization', | ||
| args: [ | ||
| payload.authorization.from as Address, | ||
| payload.authorization.to as Address, | ||
| BigInt(payload.authorization.value), | ||
| BigInt(payload.authorization.validAfter), | ||
| BigInt(payload.authorization.validBefore), | ||
| payload.authorization.nonce as Hex, | ||
| signature, | ||
| ], | ||
| }); | ||
|
|
||
| const result = await smartAccount.sendUserOperation({ | ||
| network: paymentPayload.network as 'base' | 'base-sepolia', | ||
| calls: [ | ||
| { | ||
| to: paymentRequirements.asset as `0x${string}`, | ||
| value: 0n, | ||
| data: callData as `0x${string}`, | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| await smartAccount.waitForUserOperation({ | ||
| userOpHash: result.userOpHash, | ||
| }); | ||
|
|
||
| logger.info('Settlement transaction completed', { userOpHash: result.userOpHash }); | ||
|
|
||
| return { | ||
| success: true, | ||
| transaction: result.userOpHash, | ||
| network: paymentPayload.network, | ||
| payer: payload.authorization.from, | ||
| }; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { Network } from './x402-types'; | ||
| import { createPublicClient, http, Address } from 'viem'; | ||
| import { ERC20_CONTRACT_ABI } from '../fund-repo/constants'; | ||
| import { NETWORK_TO_CHAIN_ID, NETWORK_TO_CHAIN } from '../../constants'; | ||
|
|
||
| export function getNetworkId(network: Network): number { | ||
| const chainId = NETWORK_TO_CHAIN_ID[network]; | ||
| if (!chainId) { | ||
| throw new Error(`Unsupported network: ${network}`); | ||
| } | ||
| return chainId; | ||
| } | ||
|
|
||
| export async function getERC20Balance( | ||
| network: Network, | ||
| erc20Address: Address, | ||
| userAddress: Address | ||
| ): Promise<bigint> { | ||
| const chain = NETWORK_TO_CHAIN[network as keyof typeof NETWORK_TO_CHAIN]; | ||
| if (!chain) { | ||
| throw new Error(`Unsupported network for balance check: ${network}`); | ||
| } | ||
|
|
||
| const baseRpcUrl = process.env.BASE_RPC_URL || undefined; | ||
|
|
||
| const client = createPublicClient({ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. public rpc? should we use something more robust ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will swap for a private RPC
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is done now |
||
| chain, | ||
| transport: http(baseRpcUrl), | ||
| }); | ||
|
|
||
| const balance = await client.readContract({ | ||
| address: erc20Address, | ||
| abi: ERC20_CONTRACT_ABI, | ||
| functionName: 'balanceOf', | ||
| args: [userAddress], | ||
| }) as bigint; | ||
|
|
||
| return balance; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The USDC address lookup uses a network key that may not exist in the mapping, causing all non-base networks to fail payment validation with an invalid_payment error.
View Details
📝 Patch Details
Analysis
USDC address lookup fails for non-base networks in evmFacilitator.verify()
What fails:
evmFacilitator.verify()always returnsinvalid_paymentfor avalanche, polygon, base-sepolia, avalanche-fuji, and polygon-amoy networks becauseUSDC_ADDRESS_BY_NETWORK[network]returnsundefinedHow to reproduce:
Result: Line 60 comparison
erc20Address !== USDC_ADDRESS_BY_NETWORK[network]evaluates to"0x3c499c..." !== undefinedwhich is alwaystrue, causing verification to failExpected: Should compare against actual USDC addresses per Circle's official contract addresses for each supported network
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is good. We only support base payments