Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:Add python support for per in svm #1962

Merged
merged 5 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,11 @@ repos:
name: pyflakes
entry: poetry -C express_relay/sdk/python/express_relay run pyflakes
files: express_relay/sdk/python/express_relay
exclude: express_relay/sdk/python/express_relay/svm/generated/
language: "system"
- id: mypy
name: mypy
entry: poetry -C express_relay/sdk/python/express_relay run mypy
files: express_relay/sdk/python/express_relay
exclude: express_relay/sdk/python/express_relay/svm/generated/
language: "system"
12 changes: 6 additions & 6 deletions express_relay/sdk/js/src/examples/simpleSearcherLimo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ class SimpleSearcherLimo {
const outputMintDecimals = await this.clientLimo.getOrderOutputMintDecimals(
order
);
const inputAmount = new Decimal(
const inputAmountDecimals = new Decimal(
order.state.remainingInputAmount.toNumber()
).div(new Decimal(10).pow(inputMintDecimals));

const outputAmount = new Decimal(
const outputAmountDecimals = new Decimal(
order.state.expectedOutputAmount.toNumber()
).div(new Decimal(10).pow(outputMintDecimals));

Expand All @@ -80,19 +80,19 @@ class SimpleSearcherLimo {
"Sell token",
order.state.inputMint.toBase58(),
"amount:",
inputAmount.toString()
inputAmountDecimals.toString()
);
console.log(
"Buy token",
order.state.outputMint.toBase58(),
"amount:",
outputAmount.toString()
outputAmountDecimals.toString()
);

const ixsTakeOrder = await this.clientLimo.takeOrderIx(
this.searcher.publicKey,
order,
inputAmount,
inputAmountDecimals,
SVM_CONSTANTS[this.chainId].expressRelayProgram,
inputMintDecimals,
outputMintDecimals
Expand All @@ -101,7 +101,7 @@ class SimpleSearcherLimo {

const router = getPdaAuthority(
this.clientLimo.getProgramID(),
this.globalConfig
order.state.globalConfig
);
const bidAmount = new anchor.BN(argv.bid);

Expand Down
2 changes: 1 addition & 1 deletion express_relay/sdk/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ export class Client {

/**
* Constructs an SVM bid, by adding a SubmitBid instruction to a transaction
* @param txRaw The transaction to add a SubmitBid instruction to. This transaction should already check for the appropriate permissions.
* @param tx The transaction to add a SubmitBid instruction to. This transaction should already check for the appropriate permissions.
* @param searcher The address of the searcher that is submitting the bid
* @param router The identifying address of the router that the permission key is for
* @param permissionKey The 32-byte permission key as an SVM PublicKey
Expand Down
2 changes: 1 addition & 1 deletion express_relay/sdk/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ $ poetry add express-relay
To run the simple searcher script, navigate to `python/` and run

```
$ python3 -m express_relay.searcher.examples.simple_searcher --private-key <PRIVATE_KEY_HEX_STRING> --chain-id development --verbose --server-url https://per-staging.dourolabs.app/
$ poetry run python3 -m express_relay.searcher.examples.simple_searcher --private-key <PRIVATE_KEY_HEX_STRING> --chain-id development --verbose --server-url https://per-staging.dourolabs.app/
```

This simple example runs a searcher that queries the Express Relay liquidation server for available liquidation opportunities and naively submits a bid on each available opportunity.
84 changes: 61 additions & 23 deletions express_relay/sdk/python/express_relay/client.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
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 asyncio import Task
from collections.abc import Coroutine
from datetime import datetime
from typing import Callable, Any, Union, cast
from uuid import UUID
from hexbytes import HexBytes

import httpx
import web3
import websockets
from websockets.client import WebSocketClientProtocol
from eth_abi import encode
from eth_account.account import Account
from eth_account.datastructures import SignedMessage
from eth_utils import to_checksum_address
import web3
from hexbytes import HexBytes
from solders.instruction import Instruction
from solders.pubkey import Pubkey
from solders.sysvar import INSTRUCTIONS
from websockets.client import WebSocketClientProtocol

from express_relay.constants import (
OPPORTUNITY_ADAPTER_CONFIGS,
EXECUTION_PARAMS_TYPESTRING,
SVM_CONFIGS,
)
from express_relay.express_relay_types import (
BidResponse,
Opportunity,
Expand All @@ -26,12 +37,14 @@
Bytes32,
TokenAmount,
OpportunityBidParams,
BidEvm,
)
from eth_account.datastructures import SignedMessage
from express_relay.constants import (
OPPORTUNITY_ADAPTER_CONFIGS,
EXECUTION_PARAMS_TYPESTRING,
from express_relay.svm.generated.express_relay.instructions import submit_bid
from express_relay.svm.generated.express_relay.program_id import (
PROGRAM_ID as EXPRESS_RELAY_PROGRAM_ID,
m30m marked this conversation as resolved.
Show resolved Hide resolved
)
from express_relay.svm.generated.express_relay.types import SubmitBidArgs
from express_relay.svm.limo_client import LimoClient


def _get_permitted_tokens(
Expand Down Expand Up @@ -182,16 +195,7 @@ def convert_client_msg_to_server(self, client_msg: ClientMessage) -> dict:
self.ws_msg_counter += 1

if method == "post_bid":
params = {
"bid": {
"amount": msg["params"]["amount"],
"target_contract": msg["params"]["target_contract"],
"chain_id": msg["params"]["chain_id"],
"target_calldata": msg["params"]["target_calldata"],
"permission_key": msg["params"]["permission_key"],
}
}
msg["params"] = params
msg["params"] = {"bid": msg["params"]}

msg["method"] = method

Expand Down Expand Up @@ -419,6 +423,40 @@ async def get_bids(self, from_time: datetime | None = None) -> list[BidResponse]

return bids

@staticmethod
def get_svm_submit_bid_instruction(
searcher: Pubkey,
router: Pubkey,
permission_key: Pubkey,
bid_amount: int,
deadline: int,
chain_id: str,
) -> Instruction:
if chain_id not in SVM_CONFIGS:
raise ValueError(f"Chain ID {chain_id} not supported")
svm_config = SVM_CONFIGS[chain_id]
config_router = LimoClient.get_express_relay_config_router_pda(
EXPRESS_RELAY_PROGRAM_ID, router
)
express_relay_metadata = LimoClient.get_express_relay_metadata_pda(
EXPRESS_RELAY_PROGRAM_ID
)
submit_bid_ix = submit_bid(
{"data": SubmitBidArgs(deadline=deadline, bid_amount=bid_amount)},
{
"searcher": searcher,
"relayer_signer": svm_config["relayer_signer"],
"permission": permission_key,
"router": router,
"config_router": config_router,
"express_relay_metadata": express_relay_metadata,
"fee_receiver_relayer": svm_config["fee_receiver_relayer"],
"sysvar_instructions": INSTRUCTIONS,
},
svm_config["express_relay_program"],
)
return submit_bid_ix


def compute_create2_address(
searcher_address: Address,
Expand Down Expand Up @@ -624,7 +662,7 @@ def sign_opportunity_bid(

def sign_bid(
opportunity: Opportunity, bid_params: OpportunityBidParams, private_key: str
) -> Bid:
) -> BidEvm:
"""
Constructs a signature for a searcher's bid and returns the Bid object to be submitted to the server.

Expand All @@ -649,7 +687,7 @@ def sign_bid(
opportunity, permitted, executor, bid_params, signature
)

return Bid(
return BidEvm(
amount=bid_params.amount,
target_calldata=calldata,
chain_id=opportunity.chain_id,
Expand Down
25 changes: 25 additions & 0 deletions express_relay/sdk/python/express_relay/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from typing import Dict, TypedDict

from solders.pubkey import Pubkey

from express_relay.express_relay_types import OpportunityAdapterConfig

OPPORTUNITY_ADAPTER_CONFIGS = {
Expand All @@ -24,3 +28,24 @@
EXECUTION_PARAMS_TYPESTRING = (
f"({PERMIT_BATCH_TRANSFER_FROM_TYPESTRING},{EXECUTION_WITNESS_TYPESTRING})"
)


class SvmProgramConfig(TypedDict):
express_relay_program: Pubkey
relayer_signer: Pubkey
fee_receiver_relayer: Pubkey


SVM_CONFIGS: Dict[str, SvmProgramConfig] = {
"development-solana": {
"express_relay_program": Pubkey.from_string(
"PytERJFhAKuNNuaiXkApLfWzwNwSNDACpigT3LwQfou"
),
"relayer_signer": Pubkey.from_string(
"GEeEguHhepHtPVo3E9RA1wvnxgxJ61iSc9dJfd433w3K"
),
"fee_receiver_relayer": Pubkey.from_string(
"feesJcX9zwLiEZs9iQGXeBd65b9m2Zc1LjjyHngQF29"
),
}
}
116 changes: 112 additions & 4 deletions express_relay/sdk/python/express_relay/express_relay_types.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import base64
from datetime import datetime
from enum import Enum
from pydantic import BaseModel, model_validator
from pydantic import (
BaseModel,
model_validator,
GetCoreSchemaHandler,
GetJsonSchemaHandler,
Tag,
Discriminator,
)
from pydantic.functional_validators import AfterValidator
from pydantic.functional_serializers import PlainSerializer
from uuid import UUID
import web3
from typing import Union, ClassVar
from typing import Union, ClassVar, Any
from pydantic import Field
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import core_schema
from solders.transaction import Transaction as _SvmTransaction
from typing_extensions import Literal, Annotated
import warnings
import string
Expand Down Expand Up @@ -85,7 +96,7 @@ class TokenAmount(BaseModel):
amount: IntString


class Bid(BaseModel):
class BidEvm(BaseModel):
"""
Attributes:
amount: The amount of the bid in wei.
Expand All @@ -102,6 +113,71 @@ class Bid(BaseModel):
permission_key: HexString


class _TransactionPydanticAnnotation:
@classmethod
def __get_pydantic_core_schema__(
cls,
_source_type: Any,
_handler: GetCoreSchemaHandler,
) -> core_schema.CoreSchema:
"""
We return a pydantic_core.CoreSchema that behaves in the following ways:

* ints will be parsed as `ThirdPartyType` instances with the int as the x attribute
m30m marked this conversation as resolved.
Show resolved Hide resolved
* `ThirdPartyType` instances will be parsed as `ThirdPartyType` instances without any changes
* Nothing else will pass validation
* Serialization will always return just an int
"""

def validate_from_str(value: str) -> _SvmTransaction:
return _SvmTransaction.from_bytes(base64.b64decode(value))

from_str_schema = core_schema.chain_schema(
[
core_schema.str_schema(),
core_schema.no_info_plain_validator_function(validate_from_str),
]
)

return core_schema.json_or_python_schema(
json_schema=from_str_schema,
python_schema=core_schema.union_schema(
[
# check if it's an instance first before doing any further work
core_schema.is_instance_schema(_SvmTransaction),
from_str_schema,
]
),
serialization=core_schema.plain_serializer_function_ser_schema(
lambda instance: base64.b64encode(bytes(instance)).decode("utf-8")
),
)

@classmethod
def __get_pydantic_json_schema__(
cls, _core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
# Use the same schema that would be used for `str`
return handler(core_schema.str_schema())


SvmTransaction = Annotated[_SvmTransaction, _TransactionPydanticAnnotation]


class BidSvm(BaseModel):
"""
Attributes:
transaction: The transaction including the bid
chain_id: The chain ID to bid on.
"""

transaction: SvmTransaction
chain_id: str


Bid = Union[BidEvm, BidSvm]


class BidStatus(Enum):
PENDING = "pending"
SUBMITTED = "submitted"
Expand Down Expand Up @@ -373,7 +449,7 @@ class UnsubscribeMessageParams(BaseModel):
chain_ids: list[str]


class PostBidMessageParams(BaseModel):
class PostBidMessageParamsEvm(BaseModel):
"""
Attributes:
method: A string literal "post_bid".
Expand All @@ -392,6 +468,38 @@ class PostBidMessageParams(BaseModel):
permission_key: HexString


class PostBidMessageParamsSvm(BaseModel):
"""
Attributes:
method: A string literal "post_bid".
chain_id: The chain ID to bid on.
transaction: The transaction including the bid.
"""

method: Literal["post_bid"]
chain_id: str
transaction: SvmTransaction


def get_discriminator_value(v: Any) -> str:
danimhr marked this conversation as resolved.
Show resolved Hide resolved
danimhr marked this conversation as resolved.
Show resolved Hide resolved
if isinstance(v, dict):
if "target_calldata" in v:
return "evm"
return "svm"
if getattr(v, "target_calldata", None):
return "evm"
return "svm"


PostBidMessageParams = Annotated[
Union[
Annotated[PostBidMessageParamsEvm, Tag("evm")],
Annotated[PostBidMessageParamsSvm, Tag("svm")],
],
Discriminator(get_discriminator_value),
]


class PostOpportunityBidMessageParams(BaseModel):
"""
Attributes:
Expand Down
Loading
Loading