diff --git a/sdk/js/src/abi.ts b/sdk/js/src/abi.ts deleted file mode 100644 index 0b572ac3c..000000000 --- a/sdk/js/src/abi.ts +++ /dev/null @@ -1,103 +0,0 @@ -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/sdk/js/src/const.ts b/sdk/js/src/const.ts index e3c290b63..7469da453 100644 --- a/sdk/js/src/const.ts +++ b/sdk/js/src/const.ts @@ -1,27 +1,5 @@ import { PublicKey } from "@solana/web3.js"; -import { OpportunityAdapterConfig, SvmConstantsConfig } 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", - }, -}; +import { SvmConstantsConfig } from "./types"; export const SVM_CONSTANTS: Record = { "local-solana": { diff --git a/sdk/js/src/index.ts b/sdk/js/src/index.ts index 3b0378757..e4c6e4eea 100644 --- a/sdk/js/src/index.ts +++ b/sdk/js/src/index.ts @@ -12,7 +12,6 @@ import { ExpressRelaySvmConfig, Opportunity, OpportunityCreate, - TokenAmount, SvmChainUpdate, OpportunityDelete, ChainType, @@ -86,16 +85,6 @@ export function checkAddress(address: string): Address { throw new ClientError(`Invalid address: ${address}`); } -export function checkTokenQty(token: { - token: string; - amount: string; -}): TokenAmount { - return { - token: checkAddress(token.token), - amount: BigInt(token.amount), - }; -} - export class Client { public clientOptions: ClientOptions; public wsOptions: WsOptions; diff --git a/sdk/js/src/types.ts b/sdk/js/src/types.ts index fb1c48cf5..d9f00a2fd 100644 --- a/sdk/js/src/types.ts +++ b/sdk/js/src/types.ts @@ -1,17 +1,9 @@ -import { Address, Hex } from "viem"; import type { components } from "./serverTypes"; import { PublicKey, Transaction } from "@solana/web3.js"; import { OrderStateAndAddress } from "@kamino-finance/limo-sdk/dist/utils"; import { VersionedTransaction } from "@solana/web3.js"; import * as anchor from "@coral-xyz/anchor"; -/** - * ERC20 token with contract address and amount - */ -export type TokenAmount = { - token: Address; - amount: bigint; -}; /** * SVM token with contract address and amount */ @@ -20,13 +12,6 @@ export type TokenAmountSvm = { amount: bigint; }; -/** - * TokenPermissions struct for permit2 - */ -export type TokenPermissions = { - token: Address; - amount: bigint; -}; export type BidId = string; export type ChainId = string; /** @@ -48,28 +33,6 @@ export type BidParams = { deadline: bigint; }; -export type OpportunityAdapterConfig = { - /** - * The chain id as a u64 - */ - chain_id: number; - /** - * The opportunity factory address - */ - opportunity_adapter_factory: Address; - /** - * The hash of the bytecode used to initialize the opportunity adapter - */ - opportunity_adapter_init_bytecode_hash: Hex; - /** - * The permit2 address - */ - permit2: Address; - /** - * The weth address - */ - weth: Address; -}; export type OpportunitySvmMetadata = { /** * The chain id where the opportunity will be executed. @@ -137,29 +100,6 @@ export type OpportunitySvm = OpportunitySvmLimo | OpportunitySvmSwap; export type OpportunityCreate = Omit; export type Opportunity = OpportunitySvm; -/** - * 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; -}; /** * All the parameters necessary to represent an opportunity */ diff --git a/sdk/python/express_relay/client.py b/sdk/python/express_relay/client.py index b1ce52c7f..d949c777a 100644 --- a/sdk/python/express_relay/client.py +++ b/sdk/python/express_relay/client.py @@ -5,22 +5,13 @@ from asyncio import Task from collections.abc import Coroutine from datetime import datetime -from typing import Any, Callable, List, TypedDict, Union, cast +from typing import Any, Callable, List, TypedDict from uuid import UUID import express_relay.svm.generated.express_relay.types.fee_token as swap_fee_token import httpx -import web3 import websockets -from eth_abi.abi import encode -from eth_account.account import Account -from eth_account.datastructures import SignedMessage -from eth_utils import to_checksum_address -from express_relay.constants import ( - EXECUTION_PARAMS_TYPESTRING, - OPPORTUNITY_ADAPTER_CONFIGS, - SVM_CONFIGS, -) +from express_relay.constants import SVM_CONFIGS from express_relay.models import ( Bid, BidResponse, @@ -28,21 +19,12 @@ BidStatusUpdate, ClientMessage, Opportunity, - OpportunityBid, - OpportunityBidParams, OpportunityDelete, OpportunityDeleteRoot, OpportunityParams, OpportunityRoot, ) from express_relay.models.base import UnsupportedOpportunityVersionException -from express_relay.models.evm import ( - Address, - BidEvm, - Bytes32, - OpportunityEvm, - TokenAmount, -) from express_relay.models.svm import ( SvmChainUpdate, SwapOpportunitySvm, @@ -66,7 +48,6 @@ unwrap_sol, wrap_sol, ) -from hexbytes import HexBytes from solders.instruction import Instruction from solders.pubkey import Pubkey from solders.sysvar import INSTRUCTIONS @@ -75,48 +56,6 @@ from websockets.client import WebSocketClientProtocol -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 @@ -840,241 +779,3 @@ def get_svm_swap_instructions( ): instructions.append(unwrap_sol(accs["user"])) return instructions - - -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 make_adapter_calldata( - opportunity: OpportunityEvm, - permitted: list[dict[str, Union[str, int]]], - executor: Address, - bid_params: OpportunityBidParams, - signature: HexBytes, -): - """ - 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 get_opportunity_adapter_config(chain_id: str): - opportunity_adapter_config = OPPORTUNITY_ADAPTER_CONFIGS.get(chain_id) - if not opportunity_adapter_config: - raise ExpressRelayClientException( - f"Opportunity adapter config not found for chain id {chain_id}" - ) - return opportunity_adapter_config - - -def get_signature( - opportunity: OpportunityEvm, - bid_params: OpportunityBidParams, - private_key: str, -) -> SignedMessage: - """ - Constructs a signature for a searcher's bid and opportunity. - - 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 SignedMessage object, representing the signature of the searcher's bid. - """ - opportunity_adapter_config = get_opportunity_adapter_config(opportunity.chain_id) - domain_data = { - "name": "Permit2", - "chainId": opportunity_adapter_config.chain_id, - "verifyingContract": opportunity_adapter_config.permit2, - } - - executor = Account.from_key(private_key).address - message_types = { - "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": "bidAmount", "type": "uint256"}, - ], - "TokenAmount": [ - {"name": "token", "type": "address"}, - {"name": "amount", "type": "uint256"}, - ], - "TokenPermissions": [ - {"name": "token", "type": "address"}, - {"name": "amount", "type": "uint256"}, - ], - } - - 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": permitted, - "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( - private_key, domain_data, message_types, message_data - ) - - return signed_typed_data - - -def sign_opportunity_bid( - opportunity: OpportunityEvm, - bid_params: OpportunityBidParams, - private_key: str, -) -> OpportunityBid: - """ - Constructs a signature for a searcher's bid and returns the OpportunityBid 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. - """ - executor = Account.from_key(private_key).address - 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=get_signature(opportunity, bid_params, private_key), - ) - - return opportunity_bid - - -def sign_bid( - opportunity: OpportunityEvm, bid_params: OpportunityBidParams, private_key: str -) -> BidEvm: - """ - 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 Bid object, representing the transaction to submit to the server. This object contains the searcher's signature. - """ - opportunity_adapter_config = get_opportunity_adapter_config(opportunity.chain_id) - permitted = _get_permitted_tokens( - opportunity.sell_tokens, - bid_params.amount, - opportunity.target_call_value, - opportunity_adapter_config.weth, - ) - executor = Account.from_key(private_key).address - - signature = get_signature(opportunity, bid_params, private_key).signature - calldata = make_adapter_calldata( - opportunity, permitted, executor, bid_params, signature - ) - - return BidEvm( - 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/sdk/python/express_relay/constants.py b/sdk/python/express_relay/constants.py index 7cea73d9a..364a91769 100644 --- a/sdk/python/express_relay/constants.py +++ b/sdk/python/express_relay/constants.py @@ -1,33 +1,7 @@ from typing import Dict, TypedDict -from express_relay.models import OpportunityAdapterConfig from solders.pubkey import Pubkey -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})" -) - class SvmProgramConfig(TypedDict): express_relay_program: Pubkey diff --git a/sdk/python/express_relay/models/__init__.py b/sdk/python/express_relay/models/__init__.py index 1e6d67e28..20e778043 100644 --- a/sdk/python/express_relay/models/__init__.py +++ b/sdk/python/express_relay/models/__init__.py @@ -1,18 +1,6 @@ from typing import Any, Union -from express_relay.models.base import IntString, UUIDString -from express_relay.models.evm import ( - Address, - BidEvm, - BidResponseEvm, - BidStatusEvm, - Bytes32, - HexString, - OpportunityDeleteEvm, - OpportunityEvm, - SignedMessageString, - TokenAmount, -) +from express_relay.models.base import UUIDString from express_relay.models.svm import ( BidResponseSvm, BidStatusSvm, @@ -20,11 +8,12 @@ OpportunityDeleteSvm, OpportunitySvm, SvmTransaction, + TokenAmountSvm, ) from pydantic import BaseModel, Discriminator, Field, RootModel, Tag from typing_extensions import Annotated, Literal -Bid = Union[BidEvm, BidSvm] +Bid = BidSvm class BidCancel(BaseModel): @@ -46,51 +35,13 @@ class BidStatusUpdate(BaseModel): """ id: UUIDString - bid_status: Union[BidStatusEvm, BidStatusSvm] + bid_status: BidStatusSvm -BidResponse = Union[BidResponseEvm, BidResponseSvm] +BidResponse = BidResponseSvm BidResponseRoot = RootModel[BidResponse] -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: - opportunity_id: The ID of the opportunity. - amount: The amount of the bid in wei. - executor: The address of the executor. - permission_key: The permission key to bid on. - signature: The signature of the bid. - deadline: The unix timestamp after which the bid becomes invalid. - nonce: The nonce of the bid. - """ - - opportunity_id: UUIDString - amount: IntString - executor: Address - permission_key: HexString - signature: SignedMessageString - deadline: IntString - nonce: IntString - - model_config = { - "arbitrary_types_allowed": True, - } - - class OpportunityParamsV1(BaseModel): """ Attributes: @@ -104,13 +55,9 @@ class OpportunityParamsV1(BaseModel): version: The version of the opportunity. """ - target_calldata: HexString chain_id: str - target_contract: Address - permission_key: HexString - buy_tokens: list[TokenAmount] - sell_tokens: list[TokenAmount] - target_call_value: IntString + buy_tokens: list[TokenAmountSvm] + sell_tokens: list[TokenAmountSvm] version: Literal["v1"] @@ -123,9 +70,9 @@ class OpportunityParams(BaseModel): params: Union[OpportunityParamsV1] = Field(..., discriminator="version") -Opportunity = Union[OpportunityEvm, OpportunitySvm] +Opportunity = OpportunitySvm OpportunityRoot = RootModel[Opportunity] -OpportunityDelete = Union[OpportunityDeleteEvm | OpportunityDeleteSvm] +OpportunityDelete = OpportunityDeleteSvm OpportunityDeleteRoot = RootModel[OpportunityDelete] @@ -151,25 +98,6 @@ class UnsubscribeMessageParams(BaseModel): chain_ids: list[str] -class PostBidMessageParamsEvm(BaseModel): - """ - Attributes: - method: A string literal "post_bid". - amount: The amount of the bid in wei. - target_calldata: The calldata for the contract call. - chain_id: The chain ID to bid on. - target_contract: The contract address to call. - permission_key: The permission key to bid on. - """ - - method: Literal["post_bid"] - amount: IntString - target_calldata: HexString - chain_id: str - target_contract: Address - permission_key: HexString - - class PostOnChainBidMessageParamsSvm(BaseModel): """ Attributes: @@ -203,54 +131,23 @@ class PostSwapBidMessageParamsSvm(BaseModel): def get_discriminator_value(v: Any) -> str: - if isinstance(v, dict): - if "transaction" in v: - return "svm" - return "evm" - if getattr(v, "transaction", None): - return "svm" - return "evm" + if "opportunity_id" in v: + return "swap" + return "on_chain" PostBidMessageParams = Annotated[ Union[ - Annotated[PostBidMessageParamsEvm, Tag("evm")], + Annotated[PostSwapBidMessageParamsSvm, Tag("swap")], Annotated[ - Union[PostOnChainBidMessageParamsSvm, PostSwapBidMessageParamsSvm], - Tag("svm"), + PostOnChainBidMessageParamsSvm, + Tag("on_chain"), ], ], Discriminator(get_discriminator_value), ] -class PostOpportunityBidMessageParams(BaseModel): - """ - Attributes: - method: A string literal "post_opportunity_bid". - opportunity_id: The ID of the opportunity. - amount: The amount of the bid in wei. - executor: The address of the executor. - permission_key: The permission key to bid on. - signature: The signature of the bid. - deadline: The unix timestamp after which the bid becomes invalid. - nonce: The nonce of the bid. - """ - - method: Literal["post_opportunity_bid"] - opportunity_id: UUIDString - amount: IntString - executor: Address - permission_key: HexString - signature: SignedMessageString - deadline: IntString - nonce: IntString - - model_config = { - "arbitrary_types_allowed": True, - } - - class CancelBidMessageParams(BaseModel): """ Attributes: @@ -272,23 +169,5 @@ class ClientMessage(BaseModel): SubscribeMessageParams, UnsubscribeMessageParams, PostBidMessageParams, - PostOpportunityBidMessageParams, CancelBidMessageParams, ] = 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/sdk/python/express_relay/models/base.py b/sdk/python/express_relay/models/base.py index e49c4d05e..c367a61bd 100644 --- a/sdk/python/express_relay/models/base.py +++ b/sdk/python/express_relay/models/base.py @@ -17,13 +17,6 @@ class UnsupportedOpportunityDeleteChainTypeException(Exception): pass -class BidStatusVariantsEvm(Enum): - PENDING = "pending" - SUBMITTED = "submitted" - LOST = "lost" - WON = "won" - - class BidStatusVariantsSvm(Enum): PENDING = "pending" AWAITING_SIGNATURE = "awaiting_signature" diff --git a/sdk/python/express_relay/models/evm.py b/sdk/python/express_relay/models/evm.py deleted file mode 100644 index 5f76bc5ba..000000000 --- a/sdk/python/express_relay/models/evm.py +++ /dev/null @@ -1,225 +0,0 @@ -import string -from datetime import datetime -from typing import ClassVar - -import web3 -from eth_account.datastructures import SignedMessage -from express_relay.models.base import ( - BidStatusVariantsEvm, - IntString, - UnsupportedOpportunityDeleteVersionException, - UnsupportedOpportunityVersionException, - UUIDString, -) -from pydantic import BaseModel, Field, model_validator -from pydantic.functional_serializers import PlainSerializer -from pydantic.functional_validators import AfterValidator -from typing_extensions import Annotated - - -def check_hex_string(s: str): - """ - Validates that a string is a valid hex string. - - Args: - s: The string to validate as a hex string. Can be '0x'-prefixed. - """ - ind = 0 - if s.startswith("0x"): - ind = 2 - - assert all( - c in string.hexdigits for c in s[ind:] - ), "string is not a valid hex string" - - return s - - -def check_bytes32(s: str): - """ - Validates that a string is a valid 32-byte hex string. - - Args: - s: The string to validate as a 32-byte hex string. Can be '0x'-prefixed. - """ - check_hex_string(s) - ind = 0 - if s.startswith("0x"): - ind = 2 - - assert len(s[ind:]) == 64, "hex string is not 32 bytes long" - - return s - - -def check_address(s: str): - """ - Validates that a string is a valid Ethereum address. - - Args: - s: The string to validate as an Ethereum address. Can be '0x'-prefixed. - """ - assert web3.Web3.is_address(s), "string is not a valid Ethereum address" - return s - - -HexString = Annotated[str, AfterValidator(check_hex_string)] -Bytes32 = Annotated[str, AfterValidator(check_bytes32)] -Address = Annotated[str, AfterValidator(check_address)] - -SignedMessageString = Annotated[ - SignedMessage, PlainSerializer(lambda x: bytes(x.signature).hex(), return_type=str) -] - - -class TokenAmount(BaseModel): - """ - Attributes: - token: The address of the token contract. - amount: The amount of the token. - """ - - token: Address - amount: IntString - - -class BidEvm(BaseModel): - """ - Attributes: - amount: The amount of the bid in wei. - target_calldata: The calldata for the contract call. - chain_id: The chain ID to bid on. - target_contract: The contract address to call. - permission_key: The permission key to bid on. - """ - - amount: IntString - target_calldata: HexString - chain_id: str - target_contract: Address - permission_key: HexString - - -class OpportunityEvm(BaseModel): - """ - Attributes: - target_calldata: The calldata for the contract call. - chain_id: The chain ID to bid on. - target_contract: The contract address to call. - permission_key: The permission key to bid on. - buy_tokens: The tokens to receive in the opportunity. - sell_tokens: The tokens to spend in the opportunity. - target_call_value: The value to send with the contract call. - version: The version of the opportunity. - creation_time: The creation time of the opportunity. - opportunity_id: The ID of the opportunity. - """ - - target_calldata: HexString - chain_id: str - target_contract: Address - permission_key: HexString - buy_tokens: list[TokenAmount] - sell_tokens: list[TokenAmount] - target_call_value: IntString - version: str - creation_time: IntString - opportunity_id: UUIDString - - supported_versions: ClassVar[list[str]] = ["v1"] - - @model_validator(mode="before") - @classmethod - def check_version(cls, data): - if data["version"] not in cls.supported_versions: - raise UnsupportedOpportunityVersionException( - f"Cannot handle opportunity version: {data['version']}. Please upgrade your client." - ) - return data - - -class BidStatusEvm(BaseModel): - """ - Attributes: - type: The current status of the bid. - result: The result of the bid: a transaction hash if the status is SUBMITTED or WON. - The LOST status may have a result. - index: The index of the bid in the submitted transaction. - """ - - type: BidStatusVariantsEvm - result: Bytes32 | None = Field(default=None) - index: int | None = Field(default=None) - - @model_validator(mode="after") - def check_result(self): - if self.type == BidStatusVariantsEvm.PENDING: - assert self.result is None, "result must be None" - elif self.type == BidStatusVariantsEvm.LOST: - pass - else: - assert self.result is not None, "result must be a valid 32-byte hash" - return self - - @model_validator(mode="after") - def check_index(self): - if ( - self.type == BidStatusVariantsEvm.SUBMITTED - or self.type == BidStatusVariantsEvm.WON - ): - assert self.index is not None, "index must be a valid integer" - elif self.type == BidStatusVariantsEvm.LOST: - pass - else: - assert self.index is None, "index must be None" - return self - - -class BidResponseEvm(BaseModel): - """ - Attributes: - id: The unique id for bid. - amount: The amount of the bid in wei. - target_calldata: Calldata for the contract call. - chain_id: The chain ID to bid on. - target_contract: The contract address to call. - permission_key: The permission key to bid on. - 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 - bid_amount: IntString - target_calldata: HexString - chain_id: str - target_contract: Address - permission_key: HexString - status: BidStatusEvm - initiation_time: datetime - profile_id: str | None = Field(default=None) - gas_limit: IntString - - -class OpportunityDeleteEvm(BaseModel): - """ - Attributes: - chain_id: The chain ID for opportunities to be removed. - permission_key: The permission key for the opportunities to be removed. - """ - - chain_id: str - permission_key: HexString - - supported_versions: ClassVar[list[str]] = ["v1"] - chain_type: ClassVar[str] = "evm" - - @model_validator(mode="before") - @classmethod - def check_version(cls, data): - if data["version"] not in cls.supported_versions: - raise UnsupportedOpportunityDeleteVersionException( - f"Cannot handle opportunity version: {data['version']}. Please upgrade your client." - ) - return data diff --git a/sdk/python/express_relay/searcher/examples/simple_searcher_evm.py b/sdk/python/express_relay/searcher/examples/simple_searcher_evm.py deleted file mode 100644 index a7e692b31..000000000 --- a/sdk/python/express_relay/searcher/examples/simple_searcher_evm.py +++ /dev/null @@ -1,144 +0,0 @@ -import argparse -import asyncio -import logging -import typing -from secrets import randbits - -from eth_account.account import Account -from express_relay.client import ExpressRelayClient, sign_bid -from express_relay.constants import OPPORTUNITY_ADAPTER_CONFIGS -from express_relay.models import BidStatusUpdate, Opportunity, OpportunityBidParams -from express_relay.models.evm import BidEvm, Bytes32, OpportunityEvm - -logger = logging.getLogger(__name__) - -NAIVE_BID = int(1e16) -# Set deadline (naively) to max uint256 -DEADLINE_MAX = 2**256 - 1 - - -class SimpleSearcher: - def __init__( - self, server_url: str, private_key: Bytes32, api_key: str | None = None - ): - self.client = ExpressRelayClient( - server_url, - api_key, - self.opportunity_callback, - self.bid_status_callback, - ) - self.private_key = private_key - self.public_key = Account.from_key(private_key).address - - def assess_opportunity( - self, - opp: OpportunityEvm, - ) -> BidEvm | None: - """ - 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 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 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 - bid_params = OpportunityBidParams( - amount=NAIVE_BID, nonce=randbits(64), deadline=DEADLINE_MAX - ) - - bid = sign_bid(opp, bid_params, self.private_key) - - return bid - - async def opportunity_callback(self, opp: Opportunity): - """ - Callback function to run when a new opportunity is found. - - Args: - opp: An object representing a single opportunity. - """ - bid = self.assess_opportunity(typing.cast(OpportunityEvm, opp)) - if bid: - try: - await self.client.submit_bid(bid) - logger.info( - f"Submitted bid amount {bid.amount} for opportunity {str(opp.opportunity_id)}" - ) - except Exception as e: - logger.error( - f"Error submitting bid amount {bid.amount} for opportunity {str(opp.opportunity_id)}: {e}" - ) - - async def bid_status_callback(self, bid_status_update: BidStatusUpdate): - """ - Callback function to run when a bid status is updated. - - Args: - bid_status_update: An object representing an update to the status of a bid. - """ - logger.info( - f"Bid status for bid {bid_status_update.id}: {bid_status_update.bid_status}" - ) - - -async def main(): - parser = argparse.ArgumentParser() - parser.add_argument("-v", "--verbose", action="count", default=0) - parser.add_argument( - "--private-key", - type=str, - required=True, - help="Private key of the searcher for signing calldata as a hex string", - ) - parser.add_argument( - "--chain-ids", - type=str, - required=True, - nargs="+", - help="Chain ID(s) of the network(s) to monitor for opportunities", - ) - parser.add_argument( - "--server-url", - type=str, - required=True, - help="Server endpoint to use for fetching opportunities and submitting bids", - ) - parser.add_argument( - "--api-key", - type=str, - required=False, - help="The API key of the searcher to authenticate with the server for fetching and submitting bids", - ) - args = parser.parse_args() - - logger.setLevel(logging.INFO if args.verbose == 0 else logging.DEBUG) - log_handler = logging.StreamHandler() - formatter = logging.Formatter( - "%(asctime)s %(levelname)s:%(name)s:%(module)s %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - log_handler.setFormatter(formatter) - logger.addHandler(log_handler) - - 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() - await task - - -if __name__ == "__main__": - asyncio.run(main())