mirror of
https://github.com/0xbow-io/privacy-pools-core.git
synced 2026-01-09 01:17:58 -05:00
feat: request quote endpoint (#67)
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@0xbow/privacy-pools-core-sdk": "0.1.17",
|
||||
"@uniswap/v3-sdk": "^3.25.2",
|
||||
"ajv": "8.17.1",
|
||||
"body-parser": "1.20.3",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@@ -12,7 +12,9 @@ export enum ErrorCode {
|
||||
PROCESSOOOR_MISMATCH = "PROCESSOOOR_MISMATCH",
|
||||
FEE_RECEIVER_MISMATCH = "FEE_RECEIVER_MISMATCH",
|
||||
FEE_MISMATCH = "FEE_MISMATCH",
|
||||
FEE_TOO_LOW = "FEE_TOO_LOW",
|
||||
CONTEXT_MISMATCH = "CONTEXT_MISMATCH",
|
||||
RELAYER_COMMITMENT_REJECTED = "RELAYER_COMMITMENT_REJECTED",
|
||||
INSUFFICIENT_WITHDRAWN_VALUE = "INSUFFICIENT_WITHDRAWN_VALUE",
|
||||
ASSET_NOT_SUPPORTED = "ASSET_NOT_SUPPORTED",
|
||||
|
||||
@@ -31,6 +33,9 @@ export enum ErrorCode {
|
||||
|
||||
// SDK error. Wrapper for sdk's native errors
|
||||
SDK_ERROR = "SDK_ERROR",
|
||||
|
||||
// Quote errors
|
||||
QUOTE_ERROR = "QUOTE_ERROR",
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,6 +206,14 @@ export class WithdrawalValidationError extends RelayerError {
|
||||
);
|
||||
}
|
||||
|
||||
public static feeTooLow(details: string) {
|
||||
return new WithdrawalValidationError(
|
||||
"Fee is lower than required by relayer",
|
||||
ErrorCode.FEE_TOO_LOW,
|
||||
details,
|
||||
);
|
||||
}
|
||||
|
||||
public static feeMismatch(details: string) {
|
||||
return new WithdrawalValidationError(
|
||||
"Fee does not match relayer fee",
|
||||
@@ -209,6 +222,14 @@ export class WithdrawalValidationError extends RelayerError {
|
||||
);
|
||||
}
|
||||
|
||||
public static relayerCommitmentRejected(details: string) {
|
||||
return new WithdrawalValidationError(
|
||||
"Relayer commitment is too old or invalid",
|
||||
ErrorCode.RELAYER_COMMITMENT_REJECTED,
|
||||
details,
|
||||
);
|
||||
}
|
||||
|
||||
public static contextMismatch(details: string) {
|
||||
return new WithdrawalValidationError(
|
||||
"Context does not match public signal",
|
||||
@@ -257,3 +278,16 @@ export class BlockchainError extends RelayerError {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class QuoterError extends RelayerError {
|
||||
constructor(message: string, code: ErrorCode = ErrorCode.QUOTE_ERROR, details?: Record<string, unknown> | string) {
|
||||
super(message, code, details);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
|
||||
public static assetNotSupported(
|
||||
details?: Record<string, unknown> | string) {
|
||||
return new QuoterError("Asset is not supported", ErrorCode.ASSET_NOT_SUPPORTED, details);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./relayer/details.js";
|
||||
export * from "./relayer/request.js";
|
||||
export * from "./relayer/quote.js";
|
||||
|
||||
54
packages/relayer/src/handlers/relayer/quote.ts
Normal file
54
packages/relayer/src/handlers/relayer/quote.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { getAddress } from "viem";
|
||||
import { getAssetConfig, getFeeReceiverAddress } from "../../config/index.js";
|
||||
import { QuoterError } from "../../exceptions/base.exception.js";
|
||||
import { web3Provider } from "../../providers/index.js";
|
||||
import { quoteService } from "../../services/index.js";
|
||||
import { QuoteMarshall } from "../../types.js";
|
||||
import { encodeWithdrawalData } from "../../utils.js";
|
||||
|
||||
const TIME_20_SECS = 20 * 1000;
|
||||
|
||||
export async function relayQuoteHandler(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
|
||||
const chainId = Number(req.body.chainId!);
|
||||
const amountIn = BigInt(req.body.amount!.toString());
|
||||
const assetAddress = getAddress(req.body.asset!.toString())
|
||||
|
||||
const config = getAssetConfig(chainId, assetAddress);
|
||||
if (config === undefined)
|
||||
return next(QuoterError.assetNotSupported(`Asset ${assetAddress} for chain ${chainId} is not supported`));
|
||||
|
||||
const feeBPS = await quoteService.quoteFeeBPSNative({
|
||||
chainId, amountIn, assetAddress, baseFeeBPS: config.fee_bps, value: 0n
|
||||
});
|
||||
|
||||
const recipient = req.body.recipient ? getAddress(req.body.recipient.toString()) : undefined
|
||||
|
||||
const quoteResponse = new QuoteMarshall({
|
||||
baseFeeBPS: config.fee_bps,
|
||||
feeBPS,
|
||||
});
|
||||
|
||||
if (recipient) {
|
||||
const feeReceiverAddress = getFeeReceiverAddress(chainId);
|
||||
const withdrawalData = encodeWithdrawalData({
|
||||
feeRecipient: getAddress(feeReceiverAddress),
|
||||
recipient,
|
||||
relayFeeBPS: feeBPS
|
||||
})
|
||||
const expiration = Number(new Date()) + TIME_20_SECS
|
||||
const relayerCommitment = { withdrawalData, expiration };
|
||||
const signedRelayerCommitment = await web3Provider.signRelayerCommitment(chainId, relayerCommitment);
|
||||
quoteResponse.addFeeCommitment({ expiration, withdrawalData, signedRelayerCommitment })
|
||||
}
|
||||
|
||||
res
|
||||
.status(200)
|
||||
.json(res.locals.marshalResponse(quoteResponse));
|
||||
|
||||
}
|
||||
@@ -35,6 +35,7 @@ function relayRequestBodyToWithdrawalPayload(
|
||||
},
|
||||
withdrawal,
|
||||
scope,
|
||||
feeCommitment: body.feeCommitment
|
||||
};
|
||||
return wp;
|
||||
}
|
||||
@@ -108,6 +109,7 @@ export async function relayRequestHandler(
|
||||
if (maxGasPrice !== undefined && currentGasPrice > maxGasPrice) {
|
||||
throw ConfigError.maxGasPrice(`Current gas price ${currentGasPrice} is higher than max price ${maxGasPrice}`)
|
||||
}
|
||||
|
||||
const requestResponse: RelayerResponse =
|
||||
await privacyPoolRelayer.handleRequest(withdrawalPayload, chainId);
|
||||
|
||||
|
||||
10
packages/relayer/src/interfaces/relayer/common.ts
Normal file
10
packages/relayer/src/interfaces/relayer/common.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
/**
|
||||
* Represents the relayer commitment for a pre-built withdrawal.
|
||||
*/
|
||||
export interface FeeCommitment {
|
||||
expiration: number,
|
||||
withdrawalData: `0x${string}`,
|
||||
signedRelayerCommitment: `0x${string}`,
|
||||
}
|
||||
|
||||
18
packages/relayer/src/interfaces/relayer/quote.ts
Normal file
18
packages/relayer/src/interfaces/relayer/quote.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { FeeCommitment } from "./common.js";
|
||||
|
||||
export interface QuotetBody {
|
||||
/** Chain ID to process the request on */
|
||||
chainId: string | number;
|
||||
/** Potential balance to withdraw */
|
||||
amount: string;
|
||||
/** Asset address */
|
||||
asset: string;
|
||||
/** Asset address */
|
||||
recipient?: string;
|
||||
}
|
||||
|
||||
export interface QuoteResponse {
|
||||
baseFeeBPS: bigint,
|
||||
feeBPS: bigint,
|
||||
feeCommitment?: FeeCommitment
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import {
|
||||
Withdrawal,
|
||||
Withdrawal as SdkWithdrawal,
|
||||
WithdrawalProof,
|
||||
} from "@0xbow/privacy-pools-core-sdk";
|
||||
import { FeeCommitment } from "./common.js";
|
||||
|
||||
/**
|
||||
* Represents the proof payload for a relayer request.
|
||||
@@ -34,26 +35,18 @@ export interface WithdrawPublicSignals {
|
||||
context: bigint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the payload for a withdrawal relayer request.
|
||||
*/
|
||||
export interface WithdrawalRelayerPayload {
|
||||
/** Relayer address (0xAdDrEsS) */
|
||||
processooor: string;
|
||||
/** Transaction data (hex encoded) */
|
||||
data: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the request body for a relayer operation.
|
||||
*/
|
||||
export interface RelayRequestBody {
|
||||
/** Withdrawal details */
|
||||
withdrawal: WithdrawalRelayerPayload;
|
||||
withdrawal: SdkWithdrawal;
|
||||
/** Public signals as string array */
|
||||
publicSignals: string[];
|
||||
/** Proof details */
|
||||
proof: ProofRelayerPayload;
|
||||
/** Fee commitment */
|
||||
feeCommitment?: FeeCommitment;
|
||||
/** Pool scope */
|
||||
scope: string;
|
||||
/** Chain ID to process the request on */
|
||||
@@ -65,8 +58,9 @@ export interface RelayRequestBody {
|
||||
*/
|
||||
export interface WithdrawalPayload {
|
||||
readonly proof: WithdrawalProof;
|
||||
readonly withdrawal: Withdrawal;
|
||||
readonly withdrawal: SdkWithdrawal;
|
||||
readonly scope: bigint;
|
||||
readonly feeCommitment?: FeeCommitment;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextFunction, Request, Response } from "express";
|
||||
import { ValidationError } from "../../exceptions/base.exception.js";
|
||||
import { validateDetailsQuerystring } from "../../schemes/relayer/details.scheme.js";
|
||||
import { validateRelayRequestBody } from "../../schemes/relayer/request.scheme.js";
|
||||
import { validateQuoteBody } from "../../schemes/relayer/quote.scheme.js";
|
||||
|
||||
// Middleware to validate the details querying
|
||||
export function validateDetailsMiddleware(
|
||||
@@ -34,3 +35,20 @@ export function validateRelayRequestMiddleware(
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
// Middleware to validate the quote
|
||||
export function validateQuoteMiddleware(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
const isValid = validateQuoteBody(req.body);
|
||||
if (!isValid) {
|
||||
const messages: string[] = [];
|
||||
validateQuoteBody.errors?.forEach(e => e?.message ? messages.push(e.message) : undefined);
|
||||
next(ValidationError.invalidInput({ message: messages.join("\n") }));
|
||||
return;
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Web3Provider } from "./web3.provider.js";
|
||||
import { UniswapProvider } from "./uniswap.provider.js"
|
||||
import { QuoteProvider } from "./quote.provider.js";
|
||||
|
||||
export { db } from "./db.provider.js";
|
||||
export { SdkProvider } from "./sdk.provider.js";
|
||||
export { SqliteDatabase } from "./sqlite.provider.js";
|
||||
export { db } from "./db.provider.js";
|
||||
export { UniswapProvider } from "./uniswap.provider.js"
|
||||
|
||||
export const web3Provider = new Web3Provider();
|
||||
export const uniswapProvider = new UniswapProvider();
|
||||
export const quoteProvider = new QuoteProvider();
|
||||
|
||||
14
packages/relayer/src/providers/quote.provider.ts
Normal file
14
packages/relayer/src/providers/quote.provider.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Address } from "viem";
|
||||
import { uniswapProvider } from "./index.js";
|
||||
|
||||
export class QuoteProvider {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
async quoteNativeTokenInERC20(chainId: number, addressIn: Address, amountIn: bigint): Promise<{ num: bigint, den: bigint }> {
|
||||
const { in: in_, out } = (await uniswapProvider.quoteNativeToken(chainId, addressIn, amountIn))!;
|
||||
return { num: out.amount, den: in_.amount };
|
||||
}
|
||||
|
||||
}
|
||||
90
packages/relayer/src/providers/uniswap.provider.ts
Normal file
90
packages/relayer/src/providers/uniswap.provider.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { Address, getContract } from 'viem'
|
||||
|
||||
import { web3Provider } from '../providers/index.js'
|
||||
import { BlockchainError, RelayerError } from '../exceptions/base.exception.js'
|
||||
import { isViemError } from '../utils.js'
|
||||
import { QUOTER_CONTRACT_ADDRESS, WRAPPED_NATIVE_TOKEN_ADDRESS } from './uniswap/constants.js'
|
||||
import { IERC20MinimalABI } from './uniswap/erc20.abi.js'
|
||||
import { QuoterV2ABI } from './uniswap/quoterV2.abi.js'
|
||||
|
||||
export type UniswapQuote = {
|
||||
chainId: number;
|
||||
addressIn: string;
|
||||
addressOut: string;
|
||||
amountIn: bigint;
|
||||
};
|
||||
|
||||
type QuoteToken = { amount: bigint, decimals: number }
|
||||
export type Quote = {
|
||||
in: QuoteToken
|
||||
out: QuoteToken
|
||||
};
|
||||
|
||||
export class UniswapProvider {
|
||||
|
||||
async getTokenInfo(chainId: number, address: Address): Promise<Token> {
|
||||
const contract = getContract({
|
||||
address,
|
||||
abi: IERC20MinimalABI.abi,
|
||||
client: web3Provider.client(chainId)
|
||||
});
|
||||
const [decimals, symbol] = await Promise.all([
|
||||
contract.read.decimals(),
|
||||
contract.read.symbol(),
|
||||
])
|
||||
return new Token(chainId, address, Number(decimals), symbol);
|
||||
}
|
||||
|
||||
async quoteNativeToken(chainId: number, addressIn: Address, amountIn: bigint): Promise<Quote> {
|
||||
const addressOut = WRAPPED_NATIVE_TOKEN_ADDRESS[chainId.toString()]!
|
||||
return this.quote({
|
||||
chainId,
|
||||
amountIn,
|
||||
addressOut,
|
||||
addressIn
|
||||
});
|
||||
}
|
||||
|
||||
async quote({ chainId, addressIn, addressOut, amountIn }: UniswapQuote) {
|
||||
const tokenIn = await this.getTokenInfo(chainId, addressIn as Address);
|
||||
const tokenOut = await this.getTokenInfo(chainId, addressOut as Address);
|
||||
const quoterContract = getContract({
|
||||
address: QUOTER_CONTRACT_ADDRESS[chainId.toString()]!,
|
||||
abi: QuoterV2ABI.abi,
|
||||
client: web3Provider.client(chainId)
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
const quotedAmountOut = await quoterContract.simulate.quoteExactInputSingle([{
|
||||
tokenIn: tokenIn.address as Address,
|
||||
tokenOut: tokenOut.address as Address,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
amountIn,
|
||||
sqrtPriceLimitX96: 0n,
|
||||
}])
|
||||
|
||||
// amount, sqrtPriceX96After, tickAfter, gasEstimate
|
||||
const [amount, , , ] = quotedAmountOut.result;
|
||||
return {
|
||||
in: {
|
||||
amount: amountIn, decimals: tokenIn.decimals
|
||||
},
|
||||
out: {
|
||||
amount, decimals: tokenOut.decimals
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error && isViemError(error)) {
|
||||
const { metaMessages, shortMessage } = error;
|
||||
throw BlockchainError.txError((metaMessages ? metaMessages[0] : undefined) || shortMessage)
|
||||
} else {
|
||||
throw RelayerError.unknown("Something went wrong while quoting")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
30
packages/relayer/src/providers/uniswap/constants.ts
Normal file
30
packages/relayer/src/providers/uniswap/constants.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Address } from "viem";
|
||||
|
||||
/**
|
||||
* Mainnet, Polygon, Optimism, Arbitrum, Testnets Address
|
||||
* source: https://github.com/Uniswap/v3-periphery/blob/main/deploys.md
|
||||
*/
|
||||
export const QUOTER_CONTRACT_ADDRESS: Record<string, Address> = {
|
||||
"1": "0x61fFE014bA17989E743c5F6cB21bF9697530B21e", // Ethereum
|
||||
"137": "0x61fFE014bA17989E743c5F6cB21bF9697530B21e", // polygon
|
||||
"10": "0x61fFE014bA17989E743c5F6cB21bF9697530B21e", // Optimism
|
||||
"42161": "0x61fFE014bA17989E743c5F6cB21bF9697530B21e", // Arbitrum
|
||||
"11155111": "0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3", // Sepolia
|
||||
};
|
||||
|
||||
export const FACTORY_CONTRACT_ADDRESS: Record<string, Address> = {
|
||||
"1": "0x1F98431c8aD98523631AE4a59f267346ea31F984", // Ethereum
|
||||
"137": "0x1F98431c8aD98523631AE4a59f267346ea31F984", // polygon
|
||||
"10": "0x1F98431c8aD98523631AE4a59f267346ea31F984", // Optimism
|
||||
"42161": "0x1F98431c8aD98523631AE4a59f267346ea31F984", // Arbitrum
|
||||
"11155111": "0x0227628f3f023bb0b980b67d528571c95c6dac1c", // Sepolia
|
||||
}
|
||||
|
||||
export const WRAPPED_NATIVE_TOKEN_ADDRESS: Record<string, Address> = {
|
||||
"1": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // mainnet (WETH)
|
||||
"137": "0x0000000000000000000000000000000000001010", // polygon (POL)
|
||||
// "137": "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", // (WPOL) TODO: compare which token to use
|
||||
"10": "0x4200000000000000000000000000000000000006", // Optimism (WETH)
|
||||
"42161": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", // Arbitrum (WETH)
|
||||
"11155111": "0xfff9976782d46cc05630d1f6ebab18b2324d6b14", // sepolia (WETH)
|
||||
}
|
||||
202
packages/relayer/src/providers/uniswap/erc20.abi.ts
Normal file
202
packages/relayer/src/providers/uniswap/erc20.abi.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
export const IERC20MinimalABI = {
|
||||
"abi": [
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "account",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "sender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
} as const;
|
||||
195
packages/relayer/src/providers/uniswap/quoter.abi.ts
Normal file
195
packages/relayer/src/providers/uniswap/quoter.abi.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
export const QuoterABI = {
|
||||
"abi": [
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_factory",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_WETH9",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "WETH9",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "factory",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "path",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountIn",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "quoteExactInput",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountOut",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "tokenIn",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "tokenOut",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint24",
|
||||
"name": "fee",
|
||||
"type": "uint24"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountIn",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint160",
|
||||
"name": "sqrtPriceLimitX96",
|
||||
"type": "uint160"
|
||||
}
|
||||
],
|
||||
"name": "quoteExactInputSingle",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountOut",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "path",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountOut",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "quoteExactOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountIn",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "tokenIn",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "tokenOut",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint24",
|
||||
"name": "fee",
|
||||
"type": "uint24"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountOut",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint160",
|
||||
"name": "sqrtPriceLimitX96",
|
||||
"type": "uint160"
|
||||
}
|
||||
],
|
||||
"name": "quoteExactOutputSingle",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountIn",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "int256",
|
||||
"name": "amount0Delta",
|
||||
"type": "int256"
|
||||
},
|
||||
{
|
||||
"internalType": "int256",
|
||||
"name": "amount1Delta",
|
||||
"type": "int256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "path",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "uniswapV3SwapCallback",
|
||||
"outputs": [],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
} as const;
|
||||
269
packages/relayer/src/providers/uniswap/quoterV2.abi.ts
Normal file
269
packages/relayer/src/providers/uniswap/quoterV2.abi.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
export const QuoterV2ABI = {
|
||||
"abi": [
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_factory",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_WETH9",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "WETH9",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "factory",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "path",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountIn",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "quoteExactInput",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountOut",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint160[]",
|
||||
"name": "sqrtPriceX96AfterList",
|
||||
"type": "uint160[]"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32[]",
|
||||
"name": "initializedTicksCrossedList",
|
||||
"type": "uint32[]"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "gasEstimate",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "tokenIn",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "tokenOut",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountIn",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint24",
|
||||
"name": "fee",
|
||||
"type": "uint24"
|
||||
},
|
||||
{
|
||||
"internalType": "uint160",
|
||||
"name": "sqrtPriceLimitX96",
|
||||
"type": "uint160"
|
||||
}
|
||||
],
|
||||
"internalType": "struct IQuoterV2.QuoteExactInputSingleParams",
|
||||
"name": "params",
|
||||
"type": "tuple"
|
||||
}
|
||||
],
|
||||
"name": "quoteExactInputSingle",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountOut",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint160",
|
||||
"name": "sqrtPriceX96After",
|
||||
"type": "uint160"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "initializedTicksCrossed",
|
||||
"type": "uint32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "gasEstimate",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "path",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountOut",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "quoteExactOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountIn",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint160[]",
|
||||
"name": "sqrtPriceX96AfterList",
|
||||
"type": "uint160[]"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32[]",
|
||||
"name": "initializedTicksCrossedList",
|
||||
"type": "uint32[]"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "gasEstimate",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "tokenIn",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "tokenOut",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint24",
|
||||
"name": "fee",
|
||||
"type": "uint24"
|
||||
},
|
||||
{
|
||||
"internalType": "uint160",
|
||||
"name": "sqrtPriceLimitX96",
|
||||
"type": "uint160"
|
||||
}
|
||||
],
|
||||
"internalType": "struct IQuoterV2.QuoteExactOutputSingleParams",
|
||||
"name": "params",
|
||||
"type": "tuple"
|
||||
}
|
||||
],
|
||||
"name": "quoteExactOutputSingle",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountIn",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint160",
|
||||
"name": "sqrtPriceX96After",
|
||||
"type": "uint160"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "initializedTicksCrossed",
|
||||
"type": "uint32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "gasEstimate",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "int256",
|
||||
"name": "amount0Delta",
|
||||
"type": "int256"
|
||||
},
|
||||
{
|
||||
"internalType": "int256",
|
||||
"name": "amount1Delta",
|
||||
"type": "int256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "path",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "uniswapV3SwapCallback",
|
||||
"outputs": [],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
} as const;
|
||||
@@ -1,14 +1,30 @@
|
||||
import { Chain, createPublicClient, http, PublicClient } from "viem";
|
||||
import { Chain, createPublicClient, Hex, http, PublicClient, verifyTypedData } from "viem";
|
||||
import {
|
||||
CONFIG
|
||||
CONFIG,
|
||||
getSignerPrivateKey
|
||||
} from "../config/index.js";
|
||||
import { createChainObject } from "../utils.js";
|
||||
import { privateKeyToAccount } from "viem/accounts";
|
||||
import { FeeCommitment } from "../interfaces/relayer/common.js";
|
||||
|
||||
interface IWeb3Provider {
|
||||
client(chainId: number): PublicClient;
|
||||
getGasPrice(chainId: number): Promise<bigint>;
|
||||
}
|
||||
|
||||
const domain = (chainId: number) => ({
|
||||
name: "Privacy Pools Relayer",
|
||||
version: "1",
|
||||
chainId,
|
||||
} as const)
|
||||
|
||||
const RelayerCommitmentTypes = {
|
||||
RelayerCommitment: [
|
||||
{ name: "withdrawalData", type: "bytes" },
|
||||
{ name: "expiration", type: "uint256" },
|
||||
]
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Class representing the provider for interacting with several chains
|
||||
*/
|
||||
@@ -42,4 +58,34 @@ export class Web3Provider implements IWeb3Provider {
|
||||
return await this.client(chainId).getGasPrice()
|
||||
}
|
||||
|
||||
async signRelayerCommitment(chainId: number, commitment: Omit<FeeCommitment, 'signedRelayerCommitment'>) {
|
||||
const signer = privateKeyToAccount(getSignerPrivateKey(chainId) as Hex);
|
||||
const { withdrawalData, expiration } = commitment;
|
||||
return signer.signTypedData({
|
||||
domain: domain(chainId),
|
||||
types: RelayerCommitmentTypes,
|
||||
primaryType: 'RelayerCommitment',
|
||||
message: {
|
||||
withdrawalData,
|
||||
expiration: BigInt(expiration)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async verifyRelayerCommitment(chainId: number, commitment: FeeCommitment): Promise<boolean> {
|
||||
const signer = privateKeyToAccount(getSignerPrivateKey(chainId) as Hex);
|
||||
const { withdrawalData, expiration, signedRelayerCommitment } = commitment;
|
||||
return verifyTypedData({
|
||||
address: signer.address,
|
||||
domain: domain(chainId),
|
||||
types: RelayerCommitmentTypes,
|
||||
primaryType: 'RelayerCommitment',
|
||||
message: {
|
||||
withdrawalData,
|
||||
expiration: BigInt(expiration)
|
||||
},
|
||||
signature: signedRelayerCommitment
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { Router } from "express";
|
||||
import {
|
||||
relayerDetailsHandler,
|
||||
relayQuoteHandler,
|
||||
relayRequestHandler,
|
||||
} from "../handlers/index.js";
|
||||
import { validateDetailsMiddleware, validateRelayRequestMiddleware } from "../middlewares/relayer/request.js";
|
||||
import {
|
||||
validateDetailsMiddleware,
|
||||
validateQuoteMiddleware,
|
||||
validateRelayRequestMiddleware
|
||||
} from "../middlewares/relayer/request.js";
|
||||
|
||||
// Router setup
|
||||
const relayerRouter = Router();
|
||||
|
||||
relayerRouter.get("/details", [
|
||||
validateDetailsMiddleware,
|
||||
relayerDetailsHandler
|
||||
@@ -17,4 +23,10 @@ relayerRouter.post("/request", [
|
||||
relayRequestHandler,
|
||||
]);
|
||||
|
||||
relayerRouter.post("/quote", [
|
||||
validateQuoteMiddleware,
|
||||
relayQuoteHandler
|
||||
]);
|
||||
|
||||
|
||||
export { relayerRouter };
|
||||
|
||||
18
packages/relayer/src/schemes/relayer/quote.scheme.ts
Normal file
18
packages/relayer/src/schemes/relayer/quote.scheme.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Ajv, JSONSchemaType } from "ajv";
|
||||
import { QuotetBody } from "../../interfaces/relayer/quote.js";
|
||||
|
||||
// AJV schema for validation
|
||||
const ajv = new Ajv();
|
||||
|
||||
const quoteSchema: JSONSchemaType<QuotetBody> = {
|
||||
type: "object",
|
||||
properties: {
|
||||
chainId: { type: ["string", "number"] },
|
||||
amount: { type: ["string"] },
|
||||
asset: { type: ["string"] },
|
||||
recipient: { type: ["string"], nullable: true },
|
||||
},
|
||||
required: ["chainId", "amount", "asset"],
|
||||
} as const;
|
||||
|
||||
export const validateQuoteBody = ajv.compile(quoteSchema);
|
||||
@@ -42,6 +42,16 @@ const relayRequestSchema: JSONSchemaType<RelayRequestBody> = {
|
||||
},
|
||||
scope: { type: "string" },
|
||||
chainId: { type: ["string", "number"] },
|
||||
feeCommitment: {
|
||||
type: "object",
|
||||
properties: {
|
||||
expiration: { type: "number" },
|
||||
withdrawalData: { type: "string", pattern: "0x[0-9a-fA-F]+" },
|
||||
signedRelayerCommitment: { type: "string", pattern: "0x[0-9a-fA-F]+" }
|
||||
},
|
||||
nullable: true,
|
||||
required: ["expiration", "signedRelayerCommitment"]
|
||||
}
|
||||
},
|
||||
required: ["withdrawal", "proof", "publicSignals", "scope", "chainId"],
|
||||
} as const;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
export { PrivacyPoolRelayer } from "./privacyPoolRelayer.service.js";
|
||||
import { PrivacyPoolRelayer } from "./privacyPoolRelayer.service.js";
|
||||
import { QuoteService } from "./quote.service.js";
|
||||
|
||||
export const privacyPoolRelayer = new PrivacyPoolRelayer();
|
||||
export const quoteService = new QuoteService();
|
||||
|
||||
@@ -17,10 +17,13 @@ import {
|
||||
RelayerResponse,
|
||||
WithdrawalPayload,
|
||||
} from "../interfaces/relayer/request.js";
|
||||
import { db, SdkProvider } from "../providers/index.js";
|
||||
import { db, SdkProvider, web3Provider } from "../providers/index.js";
|
||||
import { RelayerDatabase } from "../types/db.types.js";
|
||||
import { SdkProviderInterface } from "../types/sdk.types.js";
|
||||
import { decodeWithdrawalData, isViemError, parseSignals } from "../utils.js";
|
||||
import { quoteService } from "./index.js";
|
||||
import { Web3Provider } from "../providers/web3.provider.js";
|
||||
import { FeeCommitment } from "../interfaces/relayer/common.js";
|
||||
|
||||
/**
|
||||
* Class representing the Privacy Pool Relayer, responsible for processing withdrawal requests.
|
||||
@@ -28,8 +31,10 @@ import { decodeWithdrawalData, isViemError, parseSignals } from "../utils.js";
|
||||
export class PrivacyPoolRelayer {
|
||||
/** Database instance for storing and updating request states. */
|
||||
protected db: RelayerDatabase;
|
||||
/** SDK provider for handling blockchain interactions. */
|
||||
/** SDK provider for handling contract interactions. */
|
||||
protected sdkProvider: SdkProviderInterface;
|
||||
/** Web3 provider for handling blockchain interactions. */
|
||||
protected web3Provider: Web3Provider;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the Privacy Pool Relayer.
|
||||
@@ -37,6 +42,7 @@ export class PrivacyPoolRelayer {
|
||||
constructor() {
|
||||
this.db = db;
|
||||
this.sdkProvider = new SdkProvider();
|
||||
this.web3Provider = web3Provider;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,7 +168,6 @@ export class PrivacyPoolRelayer {
|
||||
);
|
||||
const proofSignals = parseSignals(wp.proof.publicSignals);
|
||||
|
||||
|
||||
if (wp.withdrawal.processooor !== entrypointAddress) {
|
||||
throw WithdrawalValidationError.processooorMismatch(
|
||||
`Processooor mismatch: expected "${entrypointAddress}", got "${wp.withdrawal.processooor}".`,
|
||||
@@ -195,10 +200,32 @@ export class PrivacyPoolRelayer {
|
||||
);
|
||||
}
|
||||
|
||||
if (relayFeeBPS !== assetConfig.fee_bps) {
|
||||
throw WithdrawalValidationError.feeMismatch(
|
||||
`Relay fee mismatch: expected "${assetConfig.fee_bps}", got "${relayFeeBPS}".`,
|
||||
);
|
||||
if (wp.feeCommitment) {
|
||||
|
||||
if (commitmentExpired(wp.feeCommitment)) {
|
||||
throw WithdrawalValidationError.relayerCommitmentRejected(
|
||||
`Relay fee commitment expired, please quote again`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!await validFeeCommitment(chainId, wp.feeCommitment)) {
|
||||
throw WithdrawalValidationError.relayerCommitmentRejected(
|
||||
`Invalid relayer commitment`,
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
const currentFeeBPS = await quoteService.quoteFeeBPSNative({
|
||||
chainId, amountIn: proofSignals.withdrawnValue, assetAddress, baseFeeBPS: assetConfig.fee_bps, value: 0n
|
||||
});
|
||||
|
||||
if (relayFeeBPS < currentFeeBPS) {
|
||||
throw WithdrawalValidationError.feeTooLow(
|
||||
`Relay fee too low: expected at least "${currentFeeBPS}", got "${relayFeeBPS}".`,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (proofSignals.withdrawnValue < assetConfig.min_withdraw_amount) {
|
||||
@@ -206,5 +233,15 @@ export class PrivacyPoolRelayer {
|
||||
`Withdrawn value too small: expected minimum "${assetConfig.min_withdraw_amount}", got "${proofSignals.withdrawnValue}".`,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function commitmentExpired(feeCommitment: FeeCommitment): boolean {
|
||||
return feeCommitment.expiration < Number(new Date())
|
||||
}
|
||||
|
||||
async function validFeeCommitment(chainId: number, feeCommitment: FeeCommitment): Promise<boolean> {
|
||||
return web3Provider.verifyRelayerCommitment(chainId, feeCommitment)
|
||||
}
|
||||
|
||||
43
packages/relayer/src/services/quote.service.ts
Normal file
43
packages/relayer/src/services/quote.service.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Address } from "viem";
|
||||
import { quoteProvider, web3Provider } from "../providers/index.js";
|
||||
|
||||
interface QuoteFeeBPSParams {
|
||||
chainId: number,
|
||||
assetAddress: Address,
|
||||
amountIn: bigint,
|
||||
value: bigint,
|
||||
baseFeeBPS: bigint
|
||||
};
|
||||
|
||||
const NativeAddress = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
||||
|
||||
export class QuoteService {
|
||||
|
||||
readonly txCost: bigint;
|
||||
|
||||
constructor() {
|
||||
// a typical withdrawal costs between 450k-650k gas
|
||||
this.txCost = 700_000n;
|
||||
}
|
||||
|
||||
async netFeeBPSNative(baseFee: bigint, balance: bigint, nativeQuote: { num: bigint, den: bigint }, gasPrice: bigint, value: bigint): Promise<bigint> {
|
||||
const nativeCosts = (1n * gasPrice * this.txCost + value)
|
||||
return baseFee + nativeQuote.den * 10_000n * nativeCosts / balance / nativeQuote.num;
|
||||
}
|
||||
|
||||
async quoteFeeBPSNative(quoteParams: QuoteFeeBPSParams): Promise<bigint> {
|
||||
const { chainId, assetAddress, amountIn, baseFeeBPS, value } = quoteParams;
|
||||
const gasPrice = await web3Provider.getGasPrice(chainId);
|
||||
|
||||
let quote: { num: bigint, den: bigint };
|
||||
if (assetAddress.toLowerCase() === NativeAddress.toLowerCase()) {
|
||||
quote = { num: 1n, den: 1n };
|
||||
} else {
|
||||
quote = await quoteProvider.quoteNativeTokenInERC20(chainId, assetAddress, amountIn);
|
||||
}
|
||||
|
||||
const feeBPS = await this.netFeeBPSNative(baseFeeBPS, amountIn, quote, gasPrice, value);
|
||||
return feeBPS
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Address } from "viem/accounts";
|
||||
import { RelayerResponse } from "./interfaces/relayer/request.js";
|
||||
import { QuoteResponse } from "./interfaces/relayer/quote.js";
|
||||
|
||||
export abstract class RelayerMarshall {
|
||||
abstract toJSON(): object;
|
||||
@@ -45,3 +46,25 @@ export class RequestMashall extends RelayerMarshall {
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
|
||||
export class QuoteMarshall extends RelayerMarshall {
|
||||
constructor(readonly response: QuoteResponse) {
|
||||
super();
|
||||
}
|
||||
|
||||
addFeeCommitment(feeCommitment: {
|
||||
expiration: number
|
||||
withdrawalData: `0x${string}`,
|
||||
signedRelayerCommitment: `0x${string}`
|
||||
}) {
|
||||
this.response.feeCommitment = feeCommitment;
|
||||
}
|
||||
|
||||
override toJSON(): object {
|
||||
return {
|
||||
baseFeeBPS: this.response.baseFeeBPS.toString(),
|
||||
feeBPS: this.response.feeBPS.toString(),
|
||||
feeCommitment: this.response.feeCommitment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import {
|
||||
Address,
|
||||
Chain,
|
||||
ContractFunctionExecutionError,
|
||||
ContractFunctionRevertedError,
|
||||
decodeAbiParameters, DecodeAbiParametersErrorType,
|
||||
encodeAbiParameters,
|
||||
EncodeAbiParametersErrorType,
|
||||
BaseError as ViemError
|
||||
} from "viem";
|
||||
import {
|
||||
@@ -15,7 +18,13 @@ import {
|
||||
} from "./interfaces/relayer/request.js";
|
||||
import { FeeDataAbi } from "./types/abi.types.js";
|
||||
|
||||
export function decodeWithdrawalData(data: `0x${string}`) {
|
||||
interface WithdrawalData {
|
||||
recipient: Address,
|
||||
feeRecipient: Address,
|
||||
relayFeeBPS: bigint
|
||||
}
|
||||
|
||||
export function decodeWithdrawalData(data: `0x${string}`): WithdrawalData {
|
||||
try {
|
||||
const [{ recipient, feeRecipient, relayFeeBPS }] = decodeAbiParameters(
|
||||
FeeDataAbi,
|
||||
@@ -31,6 +40,18 @@ export function decodeWithdrawalData(data: `0x${string}`) {
|
||||
}
|
||||
}
|
||||
|
||||
export function encodeWithdrawalData(withdrawalData: WithdrawalData): `0x${string}` {
|
||||
try {
|
||||
return encodeAbiParameters(FeeDataAbi, [withdrawalData])
|
||||
} catch (e) {
|
||||
const error = e as EncodeAbiParametersErrorType;
|
||||
throw WithdrawalValidationError.invalidWithdrawalAbi({
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function parseSignals(
|
||||
signals: RelayRequestBody["publicSignals"],
|
||||
): WithdrawPublicSignals {
|
||||
@@ -61,9 +82,9 @@ export function parseSignals(
|
||||
* @param {object} chainConfig - The chain configuration
|
||||
* @returns {Chain} - The Chain object
|
||||
*/
|
||||
export function createChainObject(chainConfig: {
|
||||
chain_id: number;
|
||||
chain_name: string;
|
||||
export function createChainObject(chainConfig: {
|
||||
chain_id: number;
|
||||
chain_name: string;
|
||||
rpc_url: string;
|
||||
native_currency?: { name: string; symbol: string; decimals: number };
|
||||
}): Chain {
|
||||
|
||||
@@ -29,3 +29,16 @@ export const request = async (requestBody) => {
|
||||
});
|
||||
console.log(JSON.stringify(await r.json(), null, 2));
|
||||
};
|
||||
|
||||
export const quote = async (quoteBody) => {
|
||||
let r = await fetch("http://localhost:3000/relayer/quote", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(quoteBody)
|
||||
})
|
||||
const quoteResponse = await r.json();
|
||||
console.log(JSON.stringify(quoteResponse, null, 2))
|
||||
return quoteResponse;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Address, Hex } from "viem";
|
||||
|
||||
// // mainnet
|
||||
// export const ENTRYPOINT_ADDRESS: Address = "0x6818809EefCe719E480a7526D76bD3e561526b46";
|
||||
// export const ETH_POOL_ADDRESS: Address = "0xF241d57C6DebAe225c0F2e6eA1529373C9A9C9fB";
|
||||
|
||||
// localnet
|
||||
export const ENTRYPOINT_ADDRESS: Address = "0xd6DB18A83F9eE4e2d0FC8D6BEd075A2905A83FDA";
|
||||
export const ETH_POOL_ADDRESS: Address = "0x4Cb503503047b66aA5e64b9BDC8148E769ac52f6";
|
||||
|
||||
export const LOCAL_ANVIL_RPC = "http://127.0.0.1:8545";
|
||||
export const PRIVATE_KEY: Hex = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { Hash, Withdrawal } from "@0xbow/privacy-pools-core-sdk";
|
||||
import { encodeAbiParameters, getAddress, Hex } from "viem";
|
||||
import { request } from "./api-test.js";
|
||||
import { request, quote } from "./api-test.js";
|
||||
import { anvilChain, pool } from "./chain.js";
|
||||
import { ENTRYPOINT_ADDRESS } from "./constants.js";
|
||||
import { deposit, proveWithdrawal } from "./create-withdrawal.js";
|
||||
|
||||
interface QuoteResponse {
|
||||
baseFeeBPS: bigint,
|
||||
feeBPS: bigint,
|
||||
feeCommitment?: {
|
||||
expiration: number,
|
||||
withdrawalData: `0x${string}`,
|
||||
signedRelayerCommitment: `0x${string}`,
|
||||
}
|
||||
}
|
||||
|
||||
const FeeDataAbi = [
|
||||
{
|
||||
name: "FeeData",
|
||||
@@ -25,36 +35,104 @@ async function prove(w: Withdrawal, scope: bigint) {
|
||||
return proveWithdrawal(w, scope);
|
||||
}
|
||||
|
||||
async function depositEth() {
|
||||
async function depositCli() {
|
||||
const r = await deposit();
|
||||
await r.wait();
|
||||
console.log(`Successful deposit, hash := ${r.hash}`);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
async function quoteReq(chainId: number, asset: string, recipient: string, amount: string) {
|
||||
return (await quote({
|
||||
chainId,
|
||||
amount,
|
||||
asset,
|
||||
recipient
|
||||
}) as QuoteResponse);
|
||||
}
|
||||
|
||||
async function quoteCli(chainId: string, asset: string, amount?: string) {
|
||||
const _amount = amount ? Number(amount) : 100_000_000_000_000_000n
|
||||
quoteReq(Number(chainId), asset, recipient, _amount.toString())
|
||||
}
|
||||
|
||||
async function relayCli(chainId: string, asset: string, withQuote: boolean) {
|
||||
|
||||
const scope = await pool.read.SCOPE() as Hash;
|
||||
const data = encodeAbiParameters(FeeDataAbi, [
|
||||
{
|
||||
recipient,
|
||||
feeRecipient: FEE_RECEIVER_ADDRESS,
|
||||
relayFeeBPS: 1_000n,
|
||||
},
|
||||
]) as Hex;
|
||||
|
||||
let data;
|
||||
let feeCommitment = undefined;
|
||||
if (withQuote) {
|
||||
const amount = "100000000000000000"; // 0.1 ETH
|
||||
const quoteRes = await quoteReq(Number(chainId), asset, recipient, amount);
|
||||
data = quoteRes.feeCommitment!.withdrawalData as Hex
|
||||
feeCommitment = {
|
||||
...quoteRes.feeCommitment,
|
||||
};
|
||||
} else {
|
||||
data = encodeAbiParameters(FeeDataAbi, [
|
||||
{
|
||||
recipient,
|
||||
feeRecipient: FEE_RECEIVER_ADDRESS,
|
||||
relayFeeBPS: 100n,
|
||||
},
|
||||
]) as Hex;
|
||||
}
|
||||
|
||||
const withdrawal = { processooor, data };
|
||||
|
||||
await depositEth();
|
||||
|
||||
// prove
|
||||
const { proof, publicSignals } = await prove(withdrawal, scope);
|
||||
|
||||
const requestBody = {
|
||||
scope: scope.toString(),
|
||||
chainId: anvilChain.id,
|
||||
withdrawal,
|
||||
publicSignals,
|
||||
proof,
|
||||
feeCommitment
|
||||
};
|
||||
|
||||
await request(requestBody);
|
||||
}
|
||||
|
||||
async function cli() {
|
||||
const args = process.argv.slice(2)
|
||||
const action = args[0];
|
||||
switch (action) {
|
||||
case "deposit": {
|
||||
console.log(action)
|
||||
await depositCli();
|
||||
break;
|
||||
}
|
||||
case "quote": {
|
||||
console.log(action)
|
||||
if (args.length < 3) {
|
||||
throw Error("Not enough args")
|
||||
}
|
||||
await quoteCli(args[1]!, args[2]!, args[3])
|
||||
break;
|
||||
}
|
||||
case "relay": {
|
||||
console.log(...args)
|
||||
const withQuote = args.includes("--with-quote")
|
||||
const noFlags = args.slice(1).filter(a => a !== "--with-quote")
|
||||
if (noFlags.length < 2) {
|
||||
throw Error("Not enough args")
|
||||
}
|
||||
await relayCli(noFlags[0]!, noFlags[1]!, withQuote);
|
||||
break;
|
||||
}
|
||||
case undefined: {
|
||||
console.log("No action selected")
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
(async () => {
|
||||
|
||||
cli();
|
||||
|
||||
})();
|
||||
|
||||
@@ -469,6 +469,12 @@ describe("PrivacyPoolRelayer", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.skip("throws when feeCommitment has expired", async () => {})
|
||||
|
||||
it.skip("throws when feeCommitment is not verified", async () => {})
|
||||
|
||||
it.skip("throws when there is no feeCommitment and fee is lower than calculated", async () => {})
|
||||
|
||||
it("passes when all checks pass", async () => {
|
||||
const withdrawalPayload: WithdrawalPayload = {
|
||||
withdrawal: {
|
||||
|
||||
Reference in New Issue
Block a user