From 05dc9c8e06b9643bb365db494e67cac843d7b90a Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 26 Jun 2024 11:18:22 -0500 Subject: [PATCH] Permit2 sdk (#1724) * python sdk * js sdk * use viem getContractAddress * Update addresses and address comments * Fix lint issues --------- Co-authored-by: Amin Moghaddam --- express_relay/sdk/js/README.md | 2 + express_relay/sdk/js/package-lock.json | 2 +- express_relay/sdk/js/package.json | 2 +- .../sdk/js/src/examples/simpleSearcher.ts | 7 +- express_relay/sdk/js/src/index.ts | 124 +++++++++--- express_relay/sdk/js/src/serverTypes.d.ts | 70 ++++--- express_relay/sdk/js/src/types.ts | 49 +++-- .../sdk/python/express_relay/client.py | 182 ++++++++++++++---- .../express_relay/express_relay_types.py | 53 +++-- .../searcher/examples/simple_searcher.py | 27 ++- express_relay/sdk/python/pyproject.toml | 2 +- 11 files changed, 376 insertions(+), 144 deletions(-) diff --git a/express_relay/sdk/js/README.md b/express_relay/sdk/js/README.md index 5f86bf40c..e94433bf7 100644 --- a/express_relay/sdk/js/README.md +++ b/express_relay/sdk/js/README.md @@ -78,3 +78,5 @@ npm run simple-searcher -- \ --chain-id op_sepolia \ --private-key ``` + +Note that if you are using a localhost server at `http://127.0.0.1`, you should specify `--endpoint http://127.0.0.1:{PORT}` rather than `http://localhost:{PORT}`, as Typescript maps `localhost` to `::1` in line with IPv6 rather than to `127.0.0.1` as with IPv4. diff --git a/express_relay/sdk/js/package-lock.json b/express_relay/sdk/js/package-lock.json index 30959223e..ff5d3e703 100644 --- a/express_relay/sdk/js/package-lock.json +++ b/express_relay/sdk/js/package-lock.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/express-relay-evm-js", - "version": "0.6.0", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/express_relay/sdk/js/package.json b/express_relay/sdk/js/package.json index 860d00793..f613c6511 100644 --- a/express_relay/sdk/js/package.json +++ b/express_relay/sdk/js/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/express-relay-evm-js", - "version": "0.6.0", + "version": "0.7.0", "description": "Utilities for interacting with the express relay protocol", "homepage": "https://github.com/pyth-network/pyth-crosschain/tree/main/express_relay/sdk/js", "author": "Douro Labs", diff --git a/express_relay/sdk/js/src/examples/simpleSearcher.ts b/express_relay/sdk/js/src/examples/simpleSearcher.ts index e59f6263c..4517fdcde 100644 --- a/express_relay/sdk/js/src/examples/simpleSearcher.ts +++ b/express_relay/sdk/js/src/examples/simpleSearcher.ts @@ -50,9 +50,12 @@ class SimpleSearcher { const bid = BigInt(argv.bid); // Bid info should be generated by evaluating the opportunity // here for simplicity we are using a constant bid and 24 hours of validity + // TODO: generate nonce more intelligently, to reduce gas costs + const nonce = BigInt(Math.floor(Math.random() * 2 ** 50)); const bidParams = { amount: bid, - validUntil: BigInt(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)), + nonce: nonce, + deadline: BigInt(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)), }; const opportunityBid = await this.client.signOpportunityBid( opportunity, @@ -99,7 +102,7 @@ const argv = yargs(hideBin(process.argv)) .option("bid", { description: "Bid amount in wei", type: "string", - default: "100", + default: "20000000000000000", }) .option("private-key", { description: diff --git a/express_relay/sdk/js/src/index.ts b/express_relay/sdk/js/src/index.ts index dae71b9b1..3eb7031d5 100644 --- a/express_relay/sdk/js/src/index.ts +++ b/express_relay/sdk/js/src/index.ts @@ -2,7 +2,7 @@ import type { components, paths } from "./serverTypes"; import createClient, { ClientOptions as FetchClientOptions, } from "openapi-fetch"; -import { Address, Hex, isAddress, isHex } from "viem"; +import { Address, Hex, isAddress, isHex, getContractAddress } from "viem"; import { privateKeyToAccount, signTypedData } from "viem/accounts"; import WebSocket from "isomorphic-ws"; import { @@ -11,11 +11,12 @@ import { BidParams, BidStatusUpdate, Opportunity, - EIP712Domain, + OpportunityAdapterConfig, OpportunityBid, OpportunityParams, TokenAmount, BidsResponse, + TokenPermissions, } from "./types"; export * from "./types"; @@ -59,6 +60,50 @@ export function checkTokenQty(token: { }; } +export const OPPORTUNITY_ADAPTER_CONFIGS: Record< + string, + OpportunityAdapterConfig +> = { + op_sepolia: { + chain_id: 11155420, + opportunity_adapter_factory: "0xfA119693864b2F185742A409c66f04865c787754", + opportunity_adapter_init_bytecode_hash: + "0x3d71516d94b96a8fdca4e3a5825a6b41c9268a8e94610367e69a8462cc543533", + permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + weth: "0x74A4A85C611679B73F402B36c0F84A7D2CcdFDa3", + }, +}; + +/** + * Converts sellTokens, bidAmount, and callValue to permitted tokens + * @param tokens List of sellTokens + * @param bidAmount + * @param callValue + * @param weth + * @returns List of permitted tokens + */ +function getPermittedTokens( + tokens: TokenAmount[], + bidAmount: bigint, + callValue: bigint, + weth: Address +): TokenPermissions[] { + const permitted: TokenPermissions[] = tokens.map(({ token, amount }) => ({ + token, + amount, + })); + const wethIndex = permitted.findIndex(({ token }) => token === weth); + const extraWethNeeded = bidAmount + callValue; + if (wethIndex !== -1) { + permitted[wethIndex].amount += extraWethNeeded; + return permitted; + } + if (extraWethNeeded > 0) { + permitted.push({ token: weth, amount: extraWethNeeded }); + } + return permitted; +} + export class Client { public clientOptions: ClientOptions; public wsOptions: WsOptions; @@ -145,21 +190,11 @@ export class Client { }); } - private convertEIP712Domain( - eip712Domain: components["schemas"]["EIP712Domain"] - ): EIP712Domain { - return { - name: eip712Domain.name, - version: eip712Domain.version, - verifyingContract: checkAddress(eip712Domain.verifying_contract), - chainId: BigInt(eip712Domain.chain_id), - }; - } - /** * Converts an opportunity from the server to the client format * Returns undefined if the opportunity version is not supported * @param opportunity + * @returns Opportunity in the converted client format */ private convertOpportunity( opportunity: components["schemas"]["OpportunityParamsWithMetadata"] @@ -179,7 +214,6 @@ export class Client { targetCallValue: BigInt(opportunity.target_call_value), sellTokens: opportunity.sell_tokens.map(checkTokenQty), buyTokens: opportunity.buy_tokens.map(checkTokenQty), - eip712Domain: this.convertEIP712Domain(opportunity.eip_712_domain), }; } @@ -256,6 +290,7 @@ export class Client { /** * Fetches opportunities * @param chainId Chain id to fetch opportunities for. e.g: sepolia + * @returns List of opportunities */ async getOpportunities(chainId?: string): Promise { const client = createClient(this.clientOptions); @@ -308,6 +343,7 @@ export class Client { * @param opportunity Opportunity to bid on * @param bidParams Bid amount and valid until timestamp * @param privateKey Private key to sign the bid with + * @returns Signed opportunity bid */ async signOpportunityBid( opportunity: Opportunity, @@ -315,40 +351,69 @@ export class Client { privateKey: Hex ): Promise { const types = { - ExecutionParams: [ - { name: "sellTokens", type: "TokenAmount[]" }, + PermitBatchWitnessTransferFrom: [ + { name: "permitted", type: "TokenPermissions[]" }, + { name: "spender", type: "address" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + { name: "witness", type: "OpportunityWitness" }, + ], + OpportunityWitness: [ { name: "buyTokens", type: "TokenAmount[]" }, { name: "executor", type: "address" }, { name: "targetContract", type: "address" }, { name: "targetCalldata", type: "bytes" }, { name: "targetCallValue", type: "uint256" }, - { name: "validUntil", type: "uint256" }, { name: "bidAmount", type: "uint256" }, ], TokenAmount: [ { name: "token", type: "address" }, { name: "amount", type: "uint256" }, ], + TokenPermissions: [ + { name: "token", type: "address" }, + { name: "amount", type: "uint256" }, + ], }; const account = privateKeyToAccount(privateKey); + const opportunityAdapterConfig = + OPPORTUNITY_ADAPTER_CONFIGS[opportunity.chainId]; + const create2Address = getContractAddress({ + bytecodeHash: + opportunityAdapterConfig.opportunity_adapter_init_bytecode_hash, + from: opportunityAdapterConfig.opportunity_adapter_factory, + opcode: "CREATE2", + salt: `0x${account.address.replace("0x", "").padStart(64, "0")}`, + }); + const signature = await signTypedData({ privateKey, domain: { - ...opportunity.eip712Domain, - chainId: Number(opportunity.eip712Domain.chainId), + name: "Permit2", + verifyingContract: checkAddress(opportunityAdapterConfig.permit2), + chainId: opportunityAdapterConfig.chain_id, }, types, - primaryType: "ExecutionParams", + primaryType: "PermitBatchWitnessTransferFrom", message: { - sellTokens: opportunity.sellTokens, - buyTokens: opportunity.buyTokens, - executor: account.address, - targetContract: opportunity.targetContract, - targetCalldata: opportunity.targetCalldata, - targetCallValue: opportunity.targetCallValue, - validUntil: bidParams.validUntil, - bidAmount: bidParams.amount, + permitted: getPermittedTokens( + opportunity.sellTokens, + bidParams.amount, + opportunity.targetCallValue, + checkAddress(opportunityAdapterConfig.weth) + ), + spender: create2Address, + nonce: bidParams.nonce, + deadline: bidParams.deadline, + witness: { + buyTokens: opportunity.buyTokens, + executor: account.address, + targetContract: opportunity.targetContract, + targetCalldata: opportunity.targetCalldata, + targetCallValue: opportunity.targetCallValue, + bidAmount: bidParams.amount, + }, }, }); @@ -369,7 +434,8 @@ export class Client { executor: bid.executor, permission_key: bid.permissionKey, signature: bid.signature, - valid_until: bid.bid.validUntil.toString(), + deadline: bid.bid.deadline.toString(), + nonce: bid.bid.nonce.toString(), }; } diff --git a/express_relay/sdk/js/src/serverTypes.d.ts b/express_relay/sdk/js/src/serverTypes.d.ts index af7dacb6a..de6b922e6 100644 --- a/express_relay/sdk/js/src/serverTypes.d.ts +++ b/express_relay/sdk/js/src/serverTypes.d.ts @@ -22,7 +22,11 @@ export interface paths { get: operations["bid_status"]; }; "/v1/opportunities": { - /** Fetch all opportunities ready to be exectued. */ + /** + * Fetch opportunities ready for execution or historical opportunities + * @description depending on the mode. You need to provide `chain_id` for historical mode. + * Opportunities are sorted by creation time in ascending order in historical mode. + */ get: operations["get_opportunities"]; /** * Submit an opportunity ready to be executed. @@ -159,28 +163,6 @@ export interface components { ClientRequest: components["schemas"]["ClientMessage"] & { id: string; }; - EIP712Domain: { - /** - * @description The network chain id parameter for EIP712 domain. - * @example 31337 - */ - chain_id: string; - /** - * @description The name parameter for the EIP712 domain. - * @example OpportunityAdapter - */ - name: string; - /** - * @description The verifying contract address parameter for the EIP712 domain. - * @example 0xcA11bde05977b3631167028862bE2a173976CA11 - */ - verifying_contract: string; - /** - * @description The version parameter for the EIP712 domain. - * @example 1 - */ - version: string; - }; ErrorBodyResponse: { error: string; }; @@ -190,11 +172,21 @@ export interface components { * @example 1000000000000000000 */ amount: string; + /** + * @description The latest unix timestamp in seconds until which the bid is valid + * @example 1000000000000000000 + */ + deadline: string; /** * @description Executor address * @example 0x5FbDB2315678afecb367f032d93F642f64180aa2 */ executor: string; + /** + * @description The nonce of the bid permit signature + * @example 123 + */ + nonce: string; /** * @description The opportunity permission key * @example 0xdeadbeefcafe @@ -202,12 +194,9 @@ export interface components { permission_key: string; /** @example 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12 */ signature: string; - /** - * @description The latest unix timestamp in seconds until which the bid is valid - * @example 1000000000000000000 - */ - valid_until: string; }; + /** @enum {string} */ + OpportunityMode: "live" | "historical"; OpportunityParams: components["schemas"]["OpportunityParamsV1"] & { /** @enum {string} */ version: "v1"; @@ -257,7 +246,6 @@ export interface components { * @example 1700000000000000 */ creation_time: number; - eip_712_domain: components["schemas"]["EIP712Domain"]; /** * @description The opportunity unique id * @example obo3ee3e-58cc-4372-a567-0e02b2c3d479 @@ -306,6 +294,11 @@ export interface components { * @example op_sepolia */ chain_id: string; + /** + * @description The gas limit for the contract call. + * @example 2000000 + */ + gas_limit: string; /** * @description The unique id for bid. * @example obo3ee3e-58cc-4372-a567-0e02b2c3d479 @@ -388,7 +381,6 @@ export interface components { * @example 1700000000000000 */ creation_time: number; - eip_712_domain: components["schemas"]["EIP712Domain"]; /** * @description The opportunity unique id * @example obo3ee3e-58cc-4372-a567-0e02b2c3d479 @@ -488,12 +480,28 @@ export interface operations { }; }; }; - /** Fetch all opportunities ready to be exectued. */ + /** + * Fetch opportunities ready for execution or historical opportunities + * @description depending on the mode. You need to provide `chain_id` for historical mode. + * Opportunities are sorted by creation time in ascending order in historical mode. + */ get_opportunities: { parameters: { query?: { /** @example op_sepolia */ chain_id?: string | null; + /** @description Get opportunities in live or historical mode */ + mode?: components["schemas"]["OpportunityMode"]; + /** + * @description The permission key to filter the opportunities by. Used only in historical mode. + * @example 0xdeadbeef + */ + permission_key?: string | null; + /** + * @description The time to get the opportunities from. Used only in historical mode. + * @example 2024-05-23T21:26:57.329954Z + */ + from_time?: string | null; }; }; responses: { diff --git a/express_relay/sdk/js/src/types.ts b/express_relay/sdk/js/src/types.ts index 3d5d2e610..261ac3bd6 100644 --- a/express_relay/sdk/js/src/types.ts +++ b/express_relay/sdk/js/src/types.ts @@ -8,6 +8,13 @@ export type TokenAmount = { token: Address; amount: bigint; }; +/** + * TokenPermissions struct for permit2 + */ +export type TokenPermissions = { + token: Address; + amount: bigint; +}; export type BidId = string; export type ChainId = string; /** @@ -18,31 +25,38 @@ export type BidParams = { * Bid amount in wei */ amount: bigint; + /** + * Bid nonce, used to prevent replay of a submitted signature. + * This can be set to a random uint256 when creating a new signature + */ + nonce: bigint; /** * Unix timestamp for when the bid is no longer valid in seconds */ - validUntil: bigint; + deadline: bigint; }; -/** - * Represents the configuration for signing an opportunity - */ -export type EIP712Domain = { + +export type OpportunityAdapterConfig = { /** - * The network chain id for the EIP712 domain. + * The chain id as a u64 */ - chainId: bigint; + chain_id: number; /** - * The verifying contract address for the EIP712 domain. + * The opportunity factory address */ - verifyingContract: Address; + opportunity_adapter_factory: Address; /** - * The name parameter for the EIP712 domain. + * The hash of the bytecode used to initialize the opportunity adapter */ - name: string; + opportunity_adapter_init_bytecode_hash: Hex; /** - * The version parameter for the EIP712 domain. + * The permit2 address */ - version: string; + permit2: Address; + /** + * The weth address + */ + weth: Address; }; /** * Represents a valid opportunity ready to be executed @@ -81,18 +95,11 @@ export type Opportunity = { * Tokens to receive after the opportunity is executed */ buyTokens: TokenAmount[]; - /** - * The data required to sign the opportunity - */ - eip712Domain: EIP712Domain; }; /** * All the parameters necessary to represent an opportunity */ -export type OpportunityParams = Omit< - Opportunity, - "opportunityId" | "eip712Domain" ->; +export type OpportunityParams = Omit; /** * Represents a bid for an opportunity */ diff --git a/express_relay/sdk/python/express_relay/client.py b/express_relay/sdk/python/express_relay/client.py index fa8abe07b..70251a894 100644 --- a/express_relay/sdk/python/express_relay/client.py +++ b/express_relay/sdk/python/express_relay/client.py @@ -3,13 +3,15 @@ from datetime import datetime import json import urllib.parse -from typing import Callable, Any +from typing import Callable, Any, Union, cast from collections.abc import Coroutine from uuid import UUID import httpx import websockets from websockets.client import WebSocketClientProtocol from eth_account.account import Account +from eth_utils import to_checksum_address +import web3 from express_relay.express_relay_types import ( BidResponse, Opportunity, @@ -18,8 +20,65 @@ Bid, OpportunityBid, OpportunityParams, + Address, + Bytes32, + TokenAmount, + OpportunityBidParams, + OpportunityAdapterConfig, ) +OPPORTUNITY_ADAPTER_CONFIGS = { + "op_sepolia": OpportunityAdapterConfig( + chain_id=11155420, + opportunity_adapter_factory="0xfA119693864b2F185742A409c66f04865c787754", + opportunity_adapter_init_bytecode_hash="0x3d71516d94b96a8fdca4e3a5825a6b41c9268a8e94610367e69a8462cc543533", + permit2="0x000000000022D473030F116dDEE9F6B43aC78BA3", + weth="0x74A4A85C611679B73F402B36c0F84A7D2CcdFDa3", + ) +} + + +def _get_permitted_tokens( + sell_tokens: list[TokenAmount], + bid_amount: int, + call_value: int, + weth_address: Address, +) -> list[dict[str, Union[str, int]]]: + """ + Extracts the sell tokens in the permit format. + + Args: + sell_tokens: A list of TokenAmount objects representing the sell tokens. + bid_amount: An integer representing the amount of the bid (in wei). + call_value: An integer representing the call value of the bid (in wei). + weth_address: The address of the WETH token. + Returns: + A list of dictionaries representing the sell tokens in the permit format. + """ + permitted_tokens: list[dict[str, Union[str, int]]] = [ + { + "token": token.token, + "amount": int(token.amount), + } + for token in sell_tokens + ] + + for token in permitted_tokens: + if token["token"] == weth_address: + sell_token_amount = cast(int, token["amount"]) + token["amount"] = sell_token_amount + call_value + bid_amount + return permitted_tokens + + if bid_amount + call_value > 0: + permitted_tokens.append( + { + "token": weth_address, + "amount": bid_amount + call_value, + } + ) + + return permitted_tokens + class ExpressRelayClientException(Exception): pass @@ -145,7 +204,8 @@ def convert_client_msg_to_server(self, client_msg: ClientMessage) -> dict: "executor": msg["params"]["executor"], "permission_key": msg["params"]["permission_key"], "signature": msg["params"]["signature"], - "valid_until": msg["params"]["valid_until"], + "deadline": msg["params"]["deadline"], + "nonce": msg["params"]["nonce"], }, } msg["params"] = params @@ -275,7 +335,8 @@ async def submit_opportunity_bid( "executor": opportunity_bid.executor, "permission_key": opportunity_bid.permission_key, "signature": opportunity_bid.signature, - "valid_until": opportunity_bid.valid_until, + "deadline": opportunity_bid.deadline, + "nonce": opportunity_bid.nonce, } client_msg = ClientMessage.model_validate({"params": params}) result = await self.send_ws_msg(client_msg) @@ -421,10 +482,37 @@ async def get_bids(self, from_time: datetime | None = None) -> list[BidResponse] return bids +def compute_create2_address( + searcher_address: Address, + opportunity_adapter_factory_address: Address, + opportunity_adapter_init_bytecode_hash: Bytes32, +) -> Address: + """ + Computes the CREATE2 address for the opportunity adapter belonging to the searcher. + + Args: + searcher_address: The address of the searcher's wallet. + opportunity_adapter_factory_address: The address of the opportunity adapter factory. + opportunity_adapter_init_bytecode_hash: The hash of the init code for the opportunity adapter. + Returns: + The computed CREATE2 address for the opportunity adapter. + """ + pre = b"\xff" + opportunity_adapter_factory = bytes.fromhex( + opportunity_adapter_factory_address.replace("0x", "") + ) + wallet = bytes.fromhex(searcher_address.replace("0x", "")) + salt = bytes(12) + wallet + init_code_hash = bytes.fromhex( + opportunity_adapter_init_bytecode_hash.replace("0x", "") + ) + result = web3.Web3.keccak(pre + opportunity_adapter_factory + salt + init_code_hash) + return to_checksum_address(result[12:].hex()) + + def sign_bid( opportunity: Opportunity, - bid_amount: int, - valid_until: int, + bid_params: OpportunityBidParams, private_key: str, ) -> OpportunityBid: """ @@ -432,59 +520,76 @@ def sign_bid( Args: opportunity: An object representing the opportunity, of type Opportunity. - bid_amount: An integer representing the amount of the bid (in wei). - valid_until: An integer representing the unix timestamp until which the bid is valid. + bid_params: An object representing the bid parameters, of type OpportunityBidParams. private_key: A 0x-prefixed hex string representing the searcher's private key. Returns: A OpportunityBid object, representing the transaction to submit to the server. This object contains the searcher's signature. """ - - executor = Account.from_key(private_key).address + opportunity_adapter_config = OPPORTUNITY_ADAPTER_CONFIGS[opportunity.chain_id] domain_data = { - "name": opportunity.eip_712_domain.name, - "version": opportunity.eip_712_domain.version, - "chainId": opportunity.eip_712_domain.chain_id, - "verifyingContract": opportunity.eip_712_domain.verifying_contract, + "name": "Permit2", + "chainId": opportunity_adapter_config.chain_id, + "verifyingContract": opportunity_adapter_config.permit2, } + + executor = Account.from_key(private_key).address message_types = { - "ExecutionParams": [ - {"name": "sellTokens", "type": "TokenAmount[]"}, + "PermitBatchWitnessTransferFrom": [ + {"name": "permitted", "type": "TokenPermissions[]"}, + {"name": "spender", "type": "address"}, + {"name": "nonce", "type": "uint256"}, + {"name": "deadline", "type": "uint256"}, + {"name": "witness", "type": "OpportunityWitness"}, + ], + "OpportunityWitness": [ {"name": "buyTokens", "type": "TokenAmount[]"}, {"name": "executor", "type": "address"}, {"name": "targetContract", "type": "address"}, {"name": "targetCalldata", "type": "bytes"}, {"name": "targetCallValue", "type": "uint256"}, - {"name": "validUntil", "type": "uint256"}, {"name": "bidAmount", "type": "uint256"}, ], "TokenAmount": [ {"name": "token", "type": "address"}, {"name": "amount", "type": "uint256"}, ], + "TokenPermissions": [ + {"name": "token", "type": "address"}, + {"name": "amount", "type": "uint256"}, + ], } # the data to be signed message_data = { - "sellTokens": [ - { - "token": token.token, - "amount": int(token.amount), - } - for token in opportunity.sell_tokens - ], - "buyTokens": [ - { - "token": token.token, - "amount": int(token.amount), - } - for token in opportunity.buy_tokens - ], - "executor": executor, - "targetContract": opportunity.target_contract, - "targetCalldata": bytes.fromhex(opportunity.target_calldata.replace("0x", "")), - "targetCallValue": opportunity.target_call_value, - "validUntil": valid_until, - "bidAmount": bid_amount, + "permitted": _get_permitted_tokens( + opportunity.sell_tokens, + bid_params.amount, + opportunity.target_call_value, + opportunity_adapter_config.weth, + ), + "spender": compute_create2_address( + executor, + opportunity_adapter_config.opportunity_adapter_factory, + opportunity_adapter_config.opportunity_adapter_init_bytecode_hash, + ), + "nonce": bid_params.nonce, + "deadline": bid_params.deadline, + "witness": { + "buyTokens": [ + { + "token": token.token, + "amount": int(token.amount), + } + for token in opportunity.buy_tokens + ], + "executor": executor, + "targetContract": opportunity.target_contract, + "targetCalldata": bytes.fromhex( + opportunity.target_calldata.replace("0x", "") + ), + "targetCallValue": opportunity.target_call_value, + "bidAmount": bid_params.amount, + }, } signed_typed_data = Account.sign_typed_data( @@ -494,8 +599,9 @@ def sign_bid( opportunity_bid = OpportunityBid( opportunity_id=opportunity.opportunity_id, permission_key=opportunity.permission_key, - amount=bid_amount, - valid_until=valid_until, + amount=bid_params.amount, + deadline=bid_params.deadline, + nonce=bid_params.nonce, executor=executor, signature=signed_typed_data, ) diff --git a/express_relay/sdk/python/express_relay/express_relay_types.py b/express_relay/sdk/python/express_relay/express_relay_types.py index 8f38e96f4..0db981369 100644 --- a/express_relay/sdk/python/express_relay/express_relay_types.py +++ b/express_relay/sdk/python/express_relay/express_relay_types.py @@ -181,6 +181,7 @@ class BidResponse(BaseModel): status: The latest status for bid. initiation_time: The time server received the bid formatted in rfc3339. profile_id: The profile id for the bid owner. + gas_limit: The gas limit for the bid. """ id: UUIDString @@ -192,6 +193,7 @@ class BidResponse(BaseModel): status: BidStatusUpdate initiation_time: datetime profile_id: str | None = Field(default=None) + gas_limit: IntString @classmethod def process_bid_response_dict(cls, bid_response_dict: dict): @@ -224,6 +226,19 @@ def process_bid_response_dict(cls, bid_response_dict: dict): return None +class OpportunityBidParams(BaseModel): + """ + Attributes: + amount: The amount of the bid in wei. + nonce: The nonce of the bid. + deadline: The unix timestamp after which the bid becomes invalid. + """ + + amount: IntString + nonce: IntString + deadline: IntString + + class OpportunityBid(BaseModel): """ Attributes: @@ -232,7 +247,8 @@ class OpportunityBid(BaseModel): executor: The address of the executor. permission_key: The permission key to bid on. signature: The signature of the bid. - valid_until: The unix timestamp after which the bid becomes invalid. + deadline: The unix timestamp after which the bid becomes invalid. + nonce: The nonce of the bid. """ opportunity_id: UUIDString @@ -240,7 +256,8 @@ class OpportunityBid(BaseModel): executor: Address permission_key: HexString signature: SignedMessageString - valid_until: IntString + deadline: IntString + nonce: IntString model_config = { "arbitrary_types_allowed": True, @@ -279,13 +296,6 @@ class OpportunityParams(BaseModel): params: Union[OpportunityParamsV1] = Field(..., discriminator="version") -class EIP712Domain(BaseModel): - name: str - version: str - chain_id: IntString - verifying_contract: Address - - class Opportunity(BaseModel): """ Attributes: @@ -299,7 +309,6 @@ class Opportunity(BaseModel): version: The version of the opportunity. creation_time: The creation time of the opportunity. opportunity_id: The ID of the opportunity. - eip_712_domain: The EIP712 domain data needed for signing. """ target_calldata: HexString @@ -312,7 +321,6 @@ class Opportunity(BaseModel): version: str creation_time: IntString opportunity_id: UUIDString - eip_712_domain: EIP712Domain supported_versions: ClassVar[list[str]] = ["v1"] @@ -393,7 +401,8 @@ class PostOpportunityBidMessageParams(BaseModel): executor: The address of the executor. permission_key: The permission key to bid on. signature: The signature of the bid. - valid_until: The unix timestamp after which the bid becomes invalid. + deadline: The unix timestamp after which the bid becomes invalid. + nonce: The nonce of the bid. """ method: Literal["post_opportunity_bid"] @@ -402,7 +411,8 @@ class PostOpportunityBidMessageParams(BaseModel): executor: Address permission_key: HexString signature: SignedMessageString - valid_until: IntString + deadline: IntString + nonce: IntString model_config = { "arbitrary_types_allowed": True, @@ -421,3 +431,20 @@ class ClientMessage(BaseModel): PostBidMessageParams, PostOpportunityBidMessageParams, ] = Field(..., discriminator="method") + + +class OpportunityAdapterConfig(BaseModel): + """ + Attributes: + chain_id: The chain ID. + opportunity_adapter_factory: The address of the opportunity adapter factory contract. + opportunity_adapter_init_bytecode_hash: The hash of the init bytecode of the opportunity adapter. + permit2: The address of the permit2 contract. + weth: The address of the WETH contract. + """ + + chain_id: int + opportunity_adapter_factory: Address + opportunity_adapter_init_bytecode_hash: Bytes32 + permit2: Address + weth: Address diff --git a/express_relay/sdk/python/express_relay/searcher/examples/simple_searcher.py b/express_relay/sdk/python/express_relay/searcher/examples/simple_searcher.py index 6d8388093..bf66b9b88 100644 --- a/express_relay/sdk/python/express_relay/searcher/examples/simple_searcher.py +++ b/express_relay/sdk/python/express_relay/searcher/examples/simple_searcher.py @@ -2,10 +2,16 @@ import asyncio import logging from eth_account.account import Account -from express_relay.client import ExpressRelayClient, sign_bid +from secrets import randbits + +from express_relay.client import ( + ExpressRelayClient, + sign_bid, +) from express_relay.express_relay_types import ( Opportunity, OpportunityBid, + OpportunityBidParams, Bytes32, BidStatus, BidStatusUpdate, @@ -13,9 +19,9 @@ logger = logging.getLogger(__name__) -NAIVE_BID = 10 -# Set validity (naively) to max uint256 -VALID_UNTIL_MAX = 2**256 - 1 +NAIVE_BID = int(2e16) +# Set deadline (naively) to max uint256 +DEADLINE_MAX = 2**256 - 1 class SimpleSearcher: @@ -36,18 +42,25 @@ def assess_opportunity( opp: Opportunity, ) -> OpportunityBid | None: """ - Assesses whether an opportunity is worth executing; if so, returns an OpportunityBid object. Otherwise returns None. + Assesses whether an opportunity is worth executing; if so, returns an OpportunityBid object. + Otherwise, returns None. This function determines whether the given opportunity is worthwhile to execute. There are many ways to evaluate this, but the most common way is to check that the value of the tokens the searcher will receive from execution exceeds the value of the tokens spent. Individual searchers will have their own methods to determine market impact and the profitability of executing an opportunity. This function can use external prices to perform this evaluation. - In this simple searcher, the function always (naively) returns an OpportunityBid object with a default bid and valid_until timestamp. + In this simple searcher, the function (naively) returns an OpportunityBid object with a default bid and deadline timestamp. Args: opp: An object representing a single opportunity. Returns: If the opportunity is deemed worthwhile, this function can return an OpportunityBid object, whose contents can be submitted to the auction server. If the opportunity is not deemed worthwhile, this function can return None. """ - opportunity_bid = sign_bid(opp, NAIVE_BID, VALID_UNTIL_MAX, self.private_key) + + # TODO: generate nonce more intelligently, to reduce gas costs + bid_params = OpportunityBidParams( + amount=NAIVE_BID, nonce=randbits(64), deadline=DEADLINE_MAX + ) + + opportunity_bid = sign_bid(opp, bid_params, self.private_key) return opportunity_bid diff --git a/express_relay/sdk/python/pyproject.toml b/express_relay/sdk/python/pyproject.toml index d39444b9b..c4600781a 100644 --- a/express_relay/sdk/python/pyproject.toml +++ b/express_relay/sdk/python/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "express-relay" -version = "0.6.0" +version = "0.7.0" description = "Utilities for searchers and protocols to interact with the Express Relay protocol." authors = ["dourolabs"] license = "Apache-2.0"