From 7ef9aa2553a5537665e544d9e7c2afb8568a8b4a Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Thu, 4 Jul 2024 05:36:59 -0500 Subject: [PATCH] Express Relay: directly submit Bids (#1747) * SDKs now directly submitting to bid endpoint * bump versions * address comments * remove development configs * fix lint * Add mode + better error handling * Check chain ids before running simple searcher * Update constants and adjust a bit for mode --------- Co-authored-by: Amin Moghaddam --- express_relay/sdk/js/package.json | 2 +- express_relay/sdk/js/src/abi.ts | 103 +++++++++++ express_relay/sdk/js/src/const.ts | 23 +++ .../sdk/js/src/examples/simpleSearcher.ts | 18 +- express_relay/sdk/js/src/index.ts | 166 ++++++++--------- express_relay/sdk/js/src/types.ts | 23 --- .../sdk/python/express_relay/client.py | 168 ++++++++---------- .../sdk/python/express_relay/constants.py | 26 +++ .../searcher/examples/simple_searcher.py | 33 ++-- express_relay/sdk/python/pyproject.toml | 2 +- pnpm-lock.yaml | 165 ++++++++++++----- 11 files changed, 463 insertions(+), 266 deletions(-) create mode 100644 express_relay/sdk/js/src/abi.ts create mode 100644 express_relay/sdk/js/src/const.ts create mode 100644 express_relay/sdk/python/express_relay/constants.py diff --git a/express_relay/sdk/js/package.json b/express_relay/sdk/js/package.json index 99b4eb8d71..56219e66fe 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.7.1", + "version": "0.8.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/abi.ts b/express_relay/sdk/js/src/abi.ts new file mode 100644 index 0000000000..0b572ac3cd --- /dev/null +++ b/express_relay/sdk/js/src/abi.ts @@ -0,0 +1,103 @@ +export const executeOpportunityAbi = { + type: "function", + name: "executeOpportunity", + inputs: [ + { + name: "params", + type: "tuple", + internalType: "struct ExecutionParams", + components: [ + { + name: "permit", + type: "tuple", + internalType: "struct ISignatureTransfer.PermitBatchTransferFrom", + components: [ + { + name: "permitted", + type: "tuple[]", + internalType: "struct ISignatureTransfer.TokenPermissions[]", + components: [ + { + name: "token", + type: "address", + internalType: "address", + }, + { + name: "amount", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + name: "nonce", + type: "uint256", + internalType: "uint256", + }, + { + name: "deadline", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + name: "witness", + type: "tuple", + internalType: "struct ExecutionWitness", + components: [ + { + name: "buyTokens", + type: "tuple[]", + internalType: "struct TokenAmount[]", + components: [ + { + name: "token", + type: "address", + internalType: "address", + }, + { + name: "amount", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + name: "executor", + type: "address", + internalType: "address", + }, + { + name: "targetContract", + type: "address", + internalType: "address", + }, + { + name: "targetCalldata", + type: "bytes", + internalType: "bytes", + }, + { + name: "targetCallValue", + type: "uint256", + internalType: "uint256", + }, + { + name: "bidAmount", + type: "uint256", + internalType: "uint256", + }, + ], + }, + ], + }, + { + name: "signature", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [], + stateMutability: "payable", +}; diff --git a/express_relay/sdk/js/src/const.ts b/express_relay/sdk/js/src/const.ts new file mode 100644 index 0000000000..6a0f4db4d1 --- /dev/null +++ b/express_relay/sdk/js/src/const.ts @@ -0,0 +1,23 @@ +import { OpportunityAdapterConfig } from "./types"; + +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", + }, + mode: { + chain_id: 34443, + opportunity_adapter_factory: "0x59F78DE21a0b05d96Ae00c547BA951a3B905602f", + opportunity_adapter_init_bytecode_hash: + "0xd53b8e32ab2ecba07c3e3a17c3c5e492c62e2f7051b89e5154f52e6bfeb0e38f", + permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + weth: "0x4200000000000000000000000000000000000006", + }, +}; diff --git a/express_relay/sdk/js/src/examples/simpleSearcher.ts b/express_relay/sdk/js/src/examples/simpleSearcher.ts index 4517fdcdeb..bd45c93b95 100644 --- a/express_relay/sdk/js/src/examples/simpleSearcher.ts +++ b/express_relay/sdk/js/src/examples/simpleSearcher.ts @@ -4,6 +4,7 @@ import { checkHex, Client } from "../index"; import { privateKeyToAccount } from "viem/accounts"; import { isHex } from "viem"; import { BidStatusUpdate, Opportunity } from "../types"; +import { OPPORTUNITY_ADAPTER_CONFIGS } from "../const"; const DAY_IN_SECONDS = 60 * 60 * 24; @@ -47,25 +48,25 @@ class SimpleSearcher { } async opportunityHandler(opportunity: Opportunity) { - const bid = BigInt(argv.bid); + const bidAmount = 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, + amount: bidAmount, nonce: nonce, deadline: BigInt(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)), }; - const opportunityBid = await this.client.signOpportunityBid( + const bid = await this.client.signBid( opportunity, bidParams, checkHex(argv.privateKey) ); try { - const bidId = await this.client.submitOpportunityBid(opportunityBid); + const bidId = await this.client.submitBid(bid); console.log( - `Successful bid. Opportunity id ${opportunityBid.opportunityId} Bid id ${bidId}` + `Successful bid. Opportunity id ${opportunity.opportunityId} Bid id ${bidId}` ); } catch (error) { console.error( @@ -102,7 +103,7 @@ const argv = yargs(hideBin(process.argv)) .option("bid", { description: "Bid amount in wei", type: "string", - default: "20000000000000000", + default: "10000000000000000", }) .option("private-key", { description: @@ -132,6 +133,11 @@ async function run() { argv.privateKey, argv.apiKey ); + if (OPPORTUNITY_ADAPTER_CONFIGS[argv.chainId] === undefined) { + throw new Error( + `Opportunity adapter config not found for chain ${argv.chainId}` + ); + } await searcher.start(); } diff --git a/express_relay/sdk/js/src/index.ts b/express_relay/sdk/js/src/index.ts index 3eb7031d5b..106d01135b 100644 --- a/express_relay/sdk/js/src/index.ts +++ b/express_relay/sdk/js/src/index.ts @@ -2,7 +2,14 @@ import type { components, paths } from "./serverTypes"; import createClient, { ClientOptions as FetchClientOptions, } from "openapi-fetch"; -import { Address, Hex, isAddress, isHex, getContractAddress } from "viem"; +import { + Address, + Hex, + isAddress, + isHex, + getContractAddress, + encodeFunctionData, +} from "viem"; import { privateKeyToAccount, signTypedData } from "viem/accounts"; import WebSocket from "isomorphic-ws"; import { @@ -11,13 +18,13 @@ import { BidParams, BidStatusUpdate, Opportunity, - OpportunityAdapterConfig, - OpportunityBid, OpportunityParams, TokenAmount, BidsResponse, TokenPermissions, } from "./types"; +import { executeOpportunityAbi } from "./abi"; +import { OPPORTUNITY_ADAPTER_CONFIGS } from "./const"; export * from "./types"; @@ -33,7 +40,7 @@ export interface WsOptions { } const DEFAULT_WS_OPTIONS: WsOptions = { - response_timeout: 5000, + response_timeout: 10000, }; export function checkHex(hex: string): Hex { @@ -60,20 +67,6 @@ 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 @@ -338,18 +331,53 @@ export class Client { } } + /** + * Constructs the calldata for the opportunity adapter contract. + * @param opportunity Opportunity to bid on + * @param permitted Permitted tokens + * @param executor Address of the searcher's wallet + * @param bidParams Bid amount, nonce, and deadline timestamp + * @param signature Searcher's signature for opportunity params and bidParams + * @returns Calldata for the opportunity adapter contract + */ + private makeAdapterCalldata( + opportunity: Opportunity, + permitted: TokenPermissions[], + executor: Address, + bidParams: BidParams, + signature: Hex + ): Hex { + return encodeFunctionData({ + abi: [executeOpportunityAbi], + args: [ + [ + [permitted, bidParams.nonce, bidParams.deadline], + [ + opportunity.buyTokens, + executor, + opportunity.targetContract, + opportunity.targetCalldata, + opportunity.targetCallValue, + bidParams.amount, + ], + ], + signature, + ], + }); + } + /** * Creates a signed bid for an opportunity * @param opportunity Opportunity to bid on - * @param bidParams Bid amount and valid until timestamp + * @param bidParams Bid amount, nonce, and deadline timestamp * @param privateKey Private key to sign the bid with - * @returns Signed opportunity bid + * @returns Signed bid */ - async signOpportunityBid( + async signBid( opportunity: Opportunity, bidParams: BidParams, privateKey: Hex - ): Promise { + ): Promise { const types = { PermitBatchWitnessTransferFrom: [ { name: "permitted", type: "TokenPermissions[]" }, @@ -377,14 +405,26 @@ export class Client { }; const account = privateKeyToAccount(privateKey); + const executor = account.address; const opportunityAdapterConfig = OPPORTUNITY_ADAPTER_CONFIGS[opportunity.chainId]; + if (!opportunityAdapterConfig) { + throw new ClientError( + `Opportunity adapter config not found for chain id: ${opportunity.chainId}` + ); + } + const permitted = getPermittedTokens( + opportunity.sellTokens, + bidParams.amount, + opportunity.targetCallValue, + checkAddress(opportunityAdapterConfig.weth) + ); 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")}`, + salt: `0x${executor.replace("0x", "").padStart(64, "0")}`, }); const signature = await signTypedData({ @@ -397,18 +437,13 @@ export class Client { types, primaryType: "PermitBatchWitnessTransferFrom", message: { - permitted: getPermittedTokens( - opportunity.sellTokens, - bidParams.amount, - opportunity.targetCallValue, - checkAddress(opportunityAdapterConfig.weth) - ), + permitted, spender: create2Address, nonce: bidParams.nonce, deadline: bidParams.deadline, witness: { buyTokens: opportunity.buyTokens, - executor: account.address, + executor, targetContract: opportunity.targetContract, targetCalldata: opportunity.targetCalldata, targetCallValue: opportunity.targetCallValue, @@ -417,25 +452,20 @@ export class Client { }, }); - return { - permissionKey: opportunity.permissionKey, - bid: bidParams, - executor: account.address, - signature, - opportunityId: opportunity.opportunityId, - }; - } + const calldata = this.makeAdapterCalldata( + opportunity, + permitted, + executor, + bidParams, + signature + ); - private toServerOpportunityBid( - bid: OpportunityBid - ): components["schemas"]["OpportunityBid"] { return { - amount: bid.bid.amount.toString(), - executor: bid.executor, - permission_key: bid.permissionKey, - signature: bid.signature, - deadline: bid.bid.deadline.toString(), - nonce: bid.bid.nonce.toString(), + amount: bidParams.amount, + targetCalldata: calldata, + chainId: opportunity.chainId, + targetContract: opportunityAdapterConfig.opportunity_adapter_factory, + permissionKey: opportunity.permissionKey, }; } @@ -449,48 +479,6 @@ export class Client { }; } - /** - * Submits a bid for an opportunity - * @param bid - * @param subscribeToUpdates If true, the client will subscribe to bid status updates via websocket and will call the bid status callback if set - * @returns The id of the submitted bid, you can use this id to track the status of the bid - */ - async submitOpportunityBid( - bid: OpportunityBid, - subscribeToUpdates = true - ): Promise { - const serverBid = this.toServerOpportunityBid(bid); - if (subscribeToUpdates) { - const result = await this.requestViaWebsocket({ - method: "post_opportunity_bid", - params: { - opportunity_bid: serverBid, - opportunity_id: bid.opportunityId, - }, - }); - if (result === null) { - throw new ClientError("Empty response in websocket for bid submission"); - } - return result.id; - } else { - const client = createClient(this.clientOptions); - const response = await client.POST( - "/v1/opportunities/{opportunity_id}/bids", - { - body: serverBid, - params: { path: { opportunity_id: bid.opportunityId } }, - } - ); - if (response.error) { - throw new ClientError(response.error.error); - } else if (response.data === undefined) { - throw new ClientError("No data returned"); - } else { - return response.data.id; - } - } - } - /** * Submits a raw bid for a permission key * @param bid diff --git a/express_relay/sdk/js/src/types.ts b/express_relay/sdk/js/src/types.ts index 261ac3bd60..bc1adf26ce 100644 --- a/express_relay/sdk/js/src/types.ts +++ b/express_relay/sdk/js/src/types.ts @@ -100,29 +100,6 @@ export type Opportunity = { * All the parameters necessary to represent an opportunity */ export type OpportunityParams = Omit; -/** - * Represents a bid for an opportunity - */ -export type OpportunityBid = { - /** - * Opportunity unique identifier in uuid format - */ - opportunityId: string; - /** - * The permission key required for successful execution of the opportunity. - */ - permissionKey: Hex; - /** - * Executor address - */ - executor: Address; - /** - * Signature of the executor - */ - signature: Hex; - - bid: BidParams; -}; /** * Represents a raw bid on acquiring a permission key */ diff --git a/express_relay/sdk/python/express_relay/client.py b/express_relay/sdk/python/express_relay/client.py index 70251a8940..29594519fd 100644 --- a/express_relay/sdk/python/express_relay/client.py +++ b/express_relay/sdk/python/express_relay/client.py @@ -1,11 +1,13 @@ import asyncio from asyncio import Task from datetime import datetime +from eth_abi import encode import json import urllib.parse from typing import Callable, Any, Union, cast from collections.abc import Coroutine from uuid import UUID +from hexbytes import HexBytes import httpx import websockets from websockets.client import WebSocketClientProtocol @@ -18,24 +20,16 @@ BidStatusUpdate, ClientMessage, 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", - ) -} +from express_relay.constants import ( + OPPORTUNITY_ADAPTER_CONFIGS, + EXECUTION_PARAMS_TYPESTRING, +) def _get_permitted_tokens( @@ -196,19 +190,6 @@ def convert_client_msg_to_server(self, client_msg: ClientMessage) -> dict: } } msg["params"] = params - elif method == "post_opportunity_bid": - params = { - "opportunity_id": msg["params"]["opportunity_id"], - "opportunity_bid": { - "amount": msg["params"]["amount"], - "executor": msg["params"]["executor"], - "permission_key": msg["params"]["permission_key"], - "signature": msg["params"]["signature"], - "deadline": msg["params"]["deadline"], - "nonce": msg["params"]["nonce"], - }, - } - msg["params"] = params msg["method"] = method @@ -312,51 +293,6 @@ async def submit_bid(self, bid: Bid, subscribe_to_updates: bool = True) -> UUID: return bid_id - async def submit_opportunity_bid( - self, - opportunity_bid: OpportunityBid, - subscribe_to_updates: bool = True, - ) -> UUID: - """ - Submits a bid on an opportunity to the server via websocket. - - Args: - opportunity_bid: An object representing the bid to submit on an opportunity. - subscribe_to_updates: A boolean indicating whether to subscribe to the bid status updates. - Returns: - The ID of the submitted bid. - """ - opportunity_bid_dict = opportunity_bid.model_dump() - if subscribe_to_updates: - params = { - "method": "post_opportunity_bid", - "opportunity_id": opportunity_bid.opportunity_id, - "amount": opportunity_bid.amount, - "executor": opportunity_bid.executor, - "permission_key": opportunity_bid.permission_key, - "signature": opportunity_bid.signature, - "deadline": opportunity_bid.deadline, - "nonce": opportunity_bid.nonce, - } - client_msg = ClientMessage.model_validate({"params": params}) - result = await self.send_ws_msg(client_msg) - bid_id = UUID(result.get("id")) - else: - async with httpx.AsyncClient(**self.http_options) as client: - resp = await client.post( - urllib.parse.urlparse(self.server_url) - ._replace( - path=f"/v1/opportunities/{opportunity_bid.opportunity_id}/bids" - ) - .geturl(), - json=opportunity_bid_dict, - ) - - resp.raise_for_status() - bid_id = UUID(resp.json().get("id")) - - return bid_id - async def ws_handler( self, opportunity_callback: ( @@ -510,22 +446,70 @@ def compute_create2_address( return to_checksum_address(result[12:].hex()) -def sign_bid( +def make_adapter_calldata( opportunity: Opportunity, + permitted: list[dict[str, Union[str, int]]], + executor: Address, bid_params: OpportunityBidParams, - private_key: str, -) -> OpportunityBid: + signature: HexBytes, +): """ - Constructs a signature for a searcher's bid and returns the OpportunityBid object to be submitted to the server. + Constructs the calldata for the opportunity adapter contract. + + Args: + opportunity: An object representing the opportunity, of type Opportunity. + permitted: A list of dictionaries representing the permitted tokens, in the format outputted by _get_permitted_tokens. + executor: The address of the searcher's wallet. + bid_params: An object representing the bid parameters, of type OpportunityBidParams. + signature: The signature of the searcher's bid, as a HexBytes object. + """ + function_selector = web3.Web3.solidity_keccak( + ["string"], [f"executeOpportunity({EXECUTION_PARAMS_TYPESTRING},bytes)"] + )[:4] + function_args = encode( + [EXECUTION_PARAMS_TYPESTRING, "bytes"], + [ + ( + ( + [(token["token"], token["amount"]) for token in permitted], + bid_params.nonce, + bid_params.deadline, + ), + ( + [(token.token, token.amount) for token in opportunity.buy_tokens], + executor, + opportunity.target_contract, + bytes.fromhex(opportunity.target_calldata.replace("0x", "")), + opportunity.target_call_value, + bid_params.amount, + ), + ), + signature, + ], + ) + calldata = f"0x{(function_selector + function_args).hex().replace('0x', '')}" + return calldata + + +def sign_bid( + opportunity: Opportunity, bid_params: OpportunityBidParams, private_key: str +) -> Bid: + """ + Constructs a signature for a searcher's bid and returns the Bid object to be submitted to the server. Args: opportunity: An object representing the opportunity, of type Opportunity. 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. + A Bid object, representing the transaction to submit to the server. This object contains the searcher's signature. """ - opportunity_adapter_config = OPPORTUNITY_ADAPTER_CONFIGS[opportunity.chain_id] + + opportunity_adapter_config = OPPORTUNITY_ADAPTER_CONFIGS.get(opportunity.chain_id) + if not opportunity_adapter_config: + raise ExpressRelayClientException( + f"Opportunity adapter config not found for chain id {opportunity.chain_id}" + ) domain_data = { "name": "Permit2", "chainId": opportunity_adapter_config.chain_id, @@ -559,14 +543,16 @@ def sign_bid( ], } + permitted = _get_permitted_tokens( + opportunity.sell_tokens, + bid_params.amount, + opportunity.target_call_value, + opportunity_adapter_config.weth, + ) + # the data to be signed message_data = { - "permitted": _get_permitted_tokens( - opportunity.sell_tokens, - bid_params.amount, - opportunity.target_call_value, - opportunity_adapter_config.weth, - ), + "permitted": permitted, "spender": compute_create2_address( executor, opportunity_adapter_config.opportunity_adapter_factory, @@ -596,14 +582,14 @@ def sign_bid( private_key, domain_data, message_types, message_data ) - opportunity_bid = OpportunityBid( - opportunity_id=opportunity.opportunity_id, - permission_key=opportunity.permission_key, - amount=bid_params.amount, - deadline=bid_params.deadline, - nonce=bid_params.nonce, - executor=executor, - signature=signed_typed_data, + calldata = make_adapter_calldata( + opportunity, permitted, executor, bid_params, signed_typed_data.signature ) - return opportunity_bid + return Bid( + amount=bid_params.amount, + target_calldata=calldata, + chain_id=opportunity.chain_id, + target_contract=opportunity_adapter_config.opportunity_adapter_factory, + permission_key=opportunity.permission_key, + ) diff --git a/express_relay/sdk/python/express_relay/constants.py b/express_relay/sdk/python/express_relay/constants.py new file mode 100644 index 0000000000..64af8999f5 --- /dev/null +++ b/express_relay/sdk/python/express_relay/constants.py @@ -0,0 +1,26 @@ +from express_relay.express_relay_types import OpportunityAdapterConfig + +OPPORTUNITY_ADAPTER_CONFIGS = { + "op_sepolia": OpportunityAdapterConfig( + chain_id=11155420, + opportunity_adapter_factory="0xfA119693864b2F185742A409c66f04865c787754", + opportunity_adapter_init_bytecode_hash="0x3d71516d94b96a8fdca4e3a5825a6b41c9268a8e94610367e69a8462cc543533", + permit2="0x000000000022D473030F116dDEE9F6B43aC78BA3", + weth="0x74A4A85C611679B73F402B36c0F84A7D2CcdFDa3", + ), + "mode": OpportunityAdapterConfig( + chain_id=34443, + opportunity_adapter_factory="0x59F78DE21a0b05d96Ae00c547BA951a3B905602f", + opportunity_adapter_init_bytecode_hash="0xd53b8e32ab2ecba07c3e3a17c3c5e492c62e2f7051b89e5154f52e6bfeb0e38f", + permit2="0x000000000022D473030F116dDEE9F6B43aC78BA3", + weth="0x4200000000000000000000000000000000000006", + ), +} + +PERMIT_BATCH_TRANSFER_FROM_TYPESTRING = "((address,uint256)[],uint256,uint256)" +EXECUTION_WITNESS_TYPESTRING = ( + "((address,uint256)[],address,address,bytes,uint256,uint256)" +) +EXECUTION_PARAMS_TYPESTRING = ( + f"({PERMIT_BATCH_TRANSFER_FROM_TYPESTRING},{EXECUTION_WITNESS_TYPESTRING})" +) 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 bf66b9b886..77ea610c03 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 @@ -8,9 +8,10 @@ ExpressRelayClient, sign_bid, ) +from express_relay.constants import OPPORTUNITY_ADAPTER_CONFIGS from express_relay.express_relay_types import ( Opportunity, - OpportunityBid, + Bid, OpportunityBidParams, Bytes32, BidStatus, @@ -19,7 +20,7 @@ logger = logging.getLogger(__name__) -NAIVE_BID = int(2e16) +NAIVE_BID = int(1e16) # Set deadline (naively) to max uint256 DEADLINE_MAX = 2**256 - 1 @@ -40,19 +41,19 @@ def __init__( def assess_opportunity( self, opp: Opportunity, - ) -> OpportunityBid | None: + ) -> Bid | None: """ - Assesses whether an opportunity is worth executing; if so, returns an OpportunityBid object. + Assesses whether an opportunity is worth executing; if so, returns a Bid 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 (naively) returns an OpportunityBid object with a default bid and deadline timestamp. + In this simple searcher, the function (naively) returns a Bid 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. + If the opportunity is deemed worthwhile, this function can return a Bid object, whose contents can be submitted to the auction server. If the opportunity is not deemed worthwhile, this function can return None. """ # TODO: generate nonce more intelligently, to reduce gas costs @@ -60,9 +61,9 @@ def assess_opportunity( amount=NAIVE_BID, nonce=randbits(64), deadline=DEADLINE_MAX ) - opportunity_bid = sign_bid(opp, bid_params, self.private_key) + bid = sign_bid(opp, bid_params, self.private_key) - return opportunity_bid + return bid async def opportunity_callback(self, opp: Opportunity): """ @@ -71,16 +72,16 @@ async def opportunity_callback(self, opp: Opportunity): Args: opp: An object representing a single opportunity. """ - opportunity_bid = self.assess_opportunity(opp) - if opportunity_bid: + bid = self.assess_opportunity(opp) + if bid: try: - await self.client.submit_opportunity_bid(opportunity_bid) + await self.client.submit_bid(bid) logger.info( - f"Submitted bid amount {opportunity_bid.amount} for opportunity {str(opportunity_bid.opportunity_id)}" + f"Submitted bid amount {bid.amount} for opportunity {str(opp.opportunity_id)}" ) except Exception as e: logger.error( - f"Error submitting bid amount {opportunity_bid.amount} for opportunity {str(opportunity_bid.opportunity_id)}: {e}" + f"Error submitting bid amount {bid.amount} for opportunity {str(opp.opportunity_id)}: {e}" ) async def bid_status_callback(self, bid_status_update: BidStatusUpdate): @@ -149,7 +150,11 @@ async def main(): simple_searcher = SimpleSearcher(args.server_url, args.private_key, args.api_key) logger.info("Searcher address: %s", simple_searcher.public_key) - + for chain_id in args.chain_ids: + if chain_id not in OPPORTUNITY_ADAPTER_CONFIGS: + raise ValueError( + f"Opportunity adapter config not found for chain {chain_id}" + ) await simple_searcher.client.subscribe_chains(args.chain_ids) task = await simple_searcher.client.get_ws_loop() diff --git a/express_relay/sdk/python/pyproject.toml b/express_relay/sdk/python/pyproject.toml index d95a932ad6..74580e9ed4 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.7.1" +version = "0.8.0" description = "Utilities for searchers and protocols to interact with the Express Relay protocol." authors = ["dourolabs"] license = "Apache-2.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 633036361e..71e5bc7001 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17209,6 +17209,10 @@ packages: resolution: {integrity: sha512-sN1AkhTAFI89anOeCaO0c3GtiGeWtOGVc2tmTdQs2Rd14HuxLyDuLIF3/WwjtkDFRM2189uYy8HJJSWJvW2mYA==} engines: {node: '>=14', npm: '>=6.12.0'} + web3-core@4.5.0: + resolution: {integrity: sha512-Q8LIAqmF7vkRydBPiU+OC7wI44nEU6JEExolFaOakqrjMtQ1CWFHRUQMNJRDsk5bRirjyShuAsuqLeYByvvXhg==} + engines: {node: '>=14', npm: '>=6.12.0'} + web3-errors@1.1.4: resolution: {integrity: sha512-WahtszSqILez+83AxGecVroyZsMuuRT+KmQp4Si5P4Rnqbczno1k748PCrZTS1J4UCPmXMG2/Vt+0Bz2zwXkwQ==} engines: {node: '>=14', npm: '>=6.12.0'} @@ -17333,6 +17337,10 @@ packages: resolution: {integrity: sha512-gqlWq4Xjz+yKL2MdxQ+BgR3F4CRo4AXWDXzftb3LDzvauEfjk/yRyoxkMSK4S9RIG96ylRImS172cV6cYzcukw==} engines: {node: '>=14', npm: '>=6.12.0'} + web3-eth@4.8.0: + resolution: {integrity: sha512-fobkdpwN9SH785/0LSLfxOMH4rZNAD/EvTKIHdpl4ZVz5XdKehX+xPMpSGDGwMlAQ7yXByjZDX3opzoqEQLWxg==} + engines: {node: '>=14', npm: '>=6.12.0'} + web3-net@1.10.0: resolution: {integrity: sha512-NLH/N3IshYWASpxk4/18Ge6n60GEvWBVeM8inx2dmZJVmRI6SJIlUxbL8jySgiTn3MMZlhbdvrGo8fpUW7a1GA==} engines: {node: '>=8.0.0'} @@ -17430,6 +17438,10 @@ packages: resolution: {integrity: sha512-qgOtADqlD5hw+KPKBUGaXAcdNLL0oh6qTeVgXwewCfbL/lG9R+/GrgMQB1gbTJ3cit8hMwtH8KX2Em6OwO0HRw==} engines: {node: '>=14', npm: '>=6.12.0'} + web3-types@1.7.0: + resolution: {integrity: sha512-nhXxDJ7a5FesRw9UG5SZdP/C/3Q2EzHGnB39hkAV+YGXDMgwxBXFWebQLfEzZzuArfHnvC0sQqkIHNwSKcVjdA==} + engines: {node: '>=14', npm: '>=6.12.0'} + web3-utils@1.10.0: resolution: {integrity: sha512-kSaCM0uMcZTNUSmn5vMEhlo02RObGNRRCkdX0V9UTAU0+lrvn0HSaudyCo6CQzuXUsnuY2ERJGCGPfeWmv19Rg==} engines: {node: '>=8.0.0'} @@ -18225,10 +18237,10 @@ snapshots: '@babel/helpers': 7.24.7 '@babel/parser': 7.24.7 '@babel/template': 7.24.7 - '@babel/traverse': 7.24.7(supports-color@5.5.0) + '@babel/traverse': 7.24.7 '@babel/types': 7.24.7 convert-source-map: 2.0.0 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -18447,6 +18459,13 @@ snapshots: dependencies: '@babel/types': 7.24.0 + '@babel/helper-module-imports@7.24.7': + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-imports@7.24.7(supports-color@5.5.0)': dependencies: '@babel/traverse': 7.24.7(supports-color@5.5.0) @@ -18480,7 +18499,7 @@ snapshots: dependencies: '@babel/core': 7.24.7 '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-module-imports': 7.24.7(supports-color@5.5.0) + '@babel/helper-module-imports': 7.24.7 '@babel/helper-simple-access': 7.24.7 '@babel/helper-split-export-declaration': 7.24.7 '@babel/helper-validator-identifier': 7.24.7 @@ -18571,7 +18590,7 @@ snapshots: '@babel/helper-simple-access@7.24.7': dependencies: - '@babel/traverse': 7.24.7(supports-color@5.5.0) + '@babel/traverse': 7.24.7 '@babel/types': 7.24.7 transitivePeerDependencies: - supports-color @@ -20552,6 +20571,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.24.7': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + debug: 4.3.5 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + '@babel/traverse@7.24.7(supports-color@5.5.0)': dependencies: '@babel/code-frame': 7.24.7 @@ -21510,7 +21544,7 @@ snapshots: eth-ens-namehash: 2.0.8 solc: 0.4.26 testrpc: 0.0.1 - web3-utils: 1.8.2 + web3-utils: 1.10.0 '@ensdomains/ensjs@2.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: @@ -21629,7 +21663,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 espree: 9.6.1 globals: 13.20.0 ignore: 5.3.1 @@ -22354,7 +22388,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -26277,7 +26311,7 @@ snapshots: dependencies: '@noble/curves': 1.3.0 '@noble/hashes': 1.3.3 - '@scure/base': 1.1.6 + '@scure/base': 1.1.7 '@scure/bip32@1.4.0': dependencies: @@ -27589,7 +27623,7 @@ snapshots: dependencies: '@babel/runtime': 7.24.7 base64url: 3.0.1 - keccak: 3.0.3 + keccak: 3.0.4 randombytes: 2.1.0 '@toruslabs/openlogin-utils@4.7.0(@babel/runtime@7.24.7)': @@ -28398,7 +28432,7 @@ snapshots: '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.5.2) '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.2) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.1 @@ -28511,7 +28545,7 @@ snapshots: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.2) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 eslint: 8.57.0 optionalDependencies: typescript: 5.5.2 @@ -28627,7 +28661,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.2) '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.2) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.5.2) optionalDependencies: @@ -28718,7 +28752,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -29977,7 +30011,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 transitivePeerDependencies: - supports-color @@ -30533,7 +30567,7 @@ snapshots: axios@1.7.2: dependencies: - follow-redirects: 1.15.6(debug@4.3.4) + follow-redirects: 1.15.6 form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -32238,12 +32272,20 @@ snapshots: optionalDependencies: supports-color: 8.1.1 + debug@4.3.4: + dependencies: + ms: 2.1.2 + debug@4.3.4(supports-color@8.1.1): dependencies: ms: 2.1.2 optionalDependencies: supports-color: 8.1.1 + debug@4.3.5: + dependencies: + ms: 2.1.2 + debug@4.3.5(supports-color@5.5.0): dependencies: ms: 2.1.2 @@ -33398,7 +33440,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -34275,6 +34317,8 @@ snapshots: flow-parser@0.238.2: {} + follow-redirects@1.15.6: {} + follow-redirects@1.15.6(debug@4.3.4): optionalDependencies: debug: 4.3.4(supports-color@8.1.1) @@ -35116,7 +35160,7 @@ snapshots: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 transitivePeerDependencies: - supports-color @@ -35151,7 +35195,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 transitivePeerDependencies: - supports-color @@ -35614,7 +35658,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -36879,7 +36923,7 @@ snapshots: '@babel/core': 7.24.7 '@babel/generator': 7.24.7 '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.24.7) - '@babel/traverse': 7.24.7(supports-color@5.5.0) + '@babel/traverse': 7.24.7 '@babel/types': 7.24.7 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 @@ -42905,7 +42949,7 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 '@types/node': 20.14.9 - acorn: 8.11.3 + acorn: 8.12.0 acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 @@ -43315,7 +43359,7 @@ snapshots: utf-8-validate@5.0.10: dependencies: - node-gyp-build: 4.6.0 + node-gyp-build: 4.8.1 utf-8-validate@5.0.7: dependencies: @@ -43786,13 +43830,30 @@ snapshots: - encoding - utf-8-validate + web3-core@4.5.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10): + dependencies: + web3-errors: 1.2.0 + web3-eth-accounts: 4.1.2 + web3-eth-iban: 4.0.7 + web3-providers-http: 4.1.0(encoding@0.1.13) + web3-providers-ws: 4.0.7(bufferutil@4.0.8)(utf-8-validate@5.0.10) + web3-types: 1.7.0 + web3-utils: 4.3.0 + web3-validator: 2.0.6 + optionalDependencies: + web3-providers-ipc: 4.0.7 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + web3-errors@1.1.4: dependencies: web3-types: 1.6.0 web3-errors@1.2.0: dependencies: - web3-types: 1.6.0 + web3-types: 1.7.0 web3-eth-abi@1.10.0: dependencies: @@ -43824,7 +43885,7 @@ snapshots: dependencies: abitype: 0.7.1(typescript@5.4.5)(zod@3.23.8) web3-errors: 1.2.0 - web3-types: 1.6.0 + web3-types: 1.7.0 web3-utils: 4.3.0 web3-validator: 2.0.6 transitivePeerDependencies: @@ -43885,10 +43946,10 @@ snapshots: '@ethereumjs/rlp': 4.0.1 crc-32: 1.2.2 ethereum-cryptography: 2.1.3 - web3-errors: 1.1.4 - web3-types: 1.6.0 + web3-errors: 1.2.0 + web3-types: 1.7.0 web3-utils: 4.3.0 - web3-validator: 2.0.5 + web3-validator: 2.0.6 web3-eth-contract@1.10.0(encoding@0.1.13): dependencies: @@ -44025,10 +44086,10 @@ snapshots: web3-eth-iban@4.0.7: dependencies: - web3-errors: 1.1.4 - web3-types: 1.6.0 + web3-errors: 1.2.0 + web3-types: 1.7.0 web3-utils: 4.3.0 - web3-validator: 2.0.5 + web3-validator: 2.0.6 web3-eth-personal@1.10.0(encoding@0.1.13): dependencies: @@ -44068,12 +44129,12 @@ snapshots: web3-eth-personal@4.0.8(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8): dependencies: - web3-core: 4.4.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) - web3-eth: 4.7.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) - web3-rpc-methods: 1.2.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) - web3-types: 1.6.0 + web3-core: 4.5.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + web3-eth: 4.8.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) + web3-rpc-methods: 1.3.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + web3-types: 1.7.0 web3-utils: 4.3.0 - web3-validator: 2.0.5 + web3-validator: 2.0.6 transitivePeerDependencies: - bufferutil - encoding @@ -44155,6 +44216,26 @@ snapshots: - utf-8-validate - zod + web3-eth@4.8.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8): + dependencies: + setimmediate: 1.0.5 + web3-core: 4.5.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + web3-errors: 1.2.0 + web3-eth-abi: 4.2.2(typescript@5.4.5)(zod@3.23.8) + web3-eth-accounts: 4.1.2 + web3-net: 4.1.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + web3-providers-ws: 4.0.7(bufferutil@4.0.8)(utf-8-validate@5.0.10) + web3-rpc-methods: 1.3.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + web3-types: 1.7.0 + web3-utils: 4.3.0 + web3-validator: 2.0.6 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + - zod + web3-net@1.10.0(encoding@0.1.13): dependencies: web3-core: 1.10.0(encoding@0.1.13) @@ -44195,9 +44276,9 @@ snapshots: web3-net@4.1.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10): dependencies: - web3-core: 4.4.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + web3-core: 4.5.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) web3-rpc-methods: 1.3.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) - web3-types: 1.6.0 + web3-types: 1.7.0 web3-utils: 4.3.0 transitivePeerDependencies: - bufferutil @@ -44341,8 +44422,8 @@ snapshots: web3-rpc-methods@1.3.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10): dependencies: - web3-core: 4.4.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) - web3-types: 1.6.0 + web3-core: 4.5.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + web3-types: 1.7.0 web3-validator: 2.0.6 transitivePeerDependencies: - bufferutil @@ -44381,6 +44462,8 @@ snapshots: web3-types@1.6.0: {} + web3-types@1.7.0: {} + web3-utils@1.10.0: dependencies: bn.js: 5.2.1 @@ -44424,7 +44507,7 @@ snapshots: ethereum-cryptography: 2.1.3 eventemitter3: 5.0.1 web3-errors: 1.2.0 - web3-types: 1.6.0 + web3-types: 1.7.0 web3-validator: 2.0.6 web3-validator@2.0.5: @@ -44440,7 +44523,7 @@ snapshots: ethereum-cryptography: 2.1.3 util: 0.12.5 web3-errors: 1.2.0 - web3-types: 1.6.0 + web3-types: 1.7.0 zod: 3.23.8 web3@1.10.0(bufferutil@4.0.7)(encoding@0.1.13)(utf-8-validate@5.0.10):