Преглед изворни кода

feat:Add python support for per in svm (#1962)

* feat: Add python support for per in svm
Amin Moghaddam пре 1 година
родитељ
комит
69cf81d961
57 измењених фајлова са 4866 додато и 316 уклоњено
  1. 2 0
      .pre-commit-config.yaml
  2. 6 6
      express_relay/sdk/js/src/examples/simpleSearcherLimo.ts
  3. 1 1
      express_relay/sdk/js/src/index.ts
  4. 1 1
      express_relay/sdk/python/README.md
  5. 61 23
      express_relay/sdk/python/express_relay/client.py
  6. 25 0
      express_relay/sdk/python/express_relay/constants.py
  7. 103 4
      express_relay/sdk/python/express_relay/express_relay_types.py
  8. 206 0
      express_relay/sdk/python/express_relay/searcher/examples/simple_searcher_svm.py
  9. 0 0
      express_relay/sdk/python/express_relay/svm/__init__.py
  10. 0 0
      express_relay/sdk/python/express_relay/svm/generated/__init__.py
  11. 0 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/__init__.py
  12. 2 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/accounts/__init__.py
  13. 85 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/accounts/config_router.py
  14. 106 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/accounts/express_relay_metadata.py
  15. 29 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/errors/__init__.py
  16. 590 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/errors/anchor.py
  17. 103 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/errors/custom.py
  18. 12 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/__init__.py
  19. 41 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/check_permission.py
  20. 55 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/initialize.py
  21. 31 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/set_admin.py
  22. 37 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/set_relayer.py
  23. 53 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/set_router_split.py
  24. 43 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/set_splits.py
  25. 65 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/submit_bid.py
  26. 33 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/withdraw_fees.py
  27. 3 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/program_id.py
  28. 9 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/types/__init__.py
  29. 45 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/types/initialize_args.py
  30. 29 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/types/set_router_split_args.py
  31. 45 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/types/set_splits_args.py
  32. 33 0
      express_relay/sdk/python/express_relay/svm/generated/express_relay/types/submit_bid_args.py
  33. 0 0
      express_relay/sdk/python/express_relay/svm/generated/limo/__init__.py
  34. 2 0
      express_relay/sdk/python/express_relay/svm/generated/limo/accounts/__init__.py
  35. 162 0
      express_relay/sdk/python/express_relay/svm/generated/limo/accounts/global_config.py
  36. 178 0
      express_relay/sdk/python/express_relay/svm/generated/limo/accounts/order.py
  37. 29 0
      express_relay/sdk/python/express_relay/svm/generated/limo/errors/__init__.py
  38. 590 0
      express_relay/sdk/python/express_relay/svm/generated/limo/errors/anchor.py
  39. 191 0
      express_relay/sdk/python/express_relay/svm/generated/limo/errors/custom.py
  40. 17 0
      express_relay/sdk/python/express_relay/svm/generated/limo/instructions/__init__.py
  41. 49 0
      express_relay/sdk/python/express_relay/svm/generated/limo/instructions/close_order_and_claim_tip.py
  42. 70 0
      express_relay/sdk/python/express_relay/svm/generated/limo/instructions/create_order.py
  43. 35 0
      express_relay/sdk/python/express_relay/svm/generated/limo/instructions/initialize_global_config.py
  44. 45 0
      express_relay/sdk/python/express_relay/svm/generated/limo/instructions/initialize_vault.py
  45. 96 0
      express_relay/sdk/python/express_relay/svm/generated/limo/instructions/take_order.py
  46. 48 0
      express_relay/sdk/python/express_relay/svm/generated/limo/instructions/update_global_config.py
  47. 37 0
      express_relay/sdk/python/express_relay/svm/generated/limo/instructions/withdraw_host_tip.py
  48. 3 0
      express_relay/sdk/python/express_relay/svm/generated/limo/program_id.py
  49. 15 0
      express_relay/sdk/python/express_relay/svm/generated/limo/types/__init__.py
  50. 105 0
      express_relay/sdk/python/express_relay/svm/generated/limo/types/order_status.py
  51. 75 0
      express_relay/sdk/python/express_relay/svm/generated/limo/types/order_type.py
  52. 200 0
      express_relay/sdk/python/express_relay/svm/generated/limo/types/update_global_config_mode.py
  53. 128 0
      express_relay/sdk/python/express_relay/svm/generated/limo/types/update_global_config_value.py
  54. 342 0
      express_relay/sdk/python/express_relay/svm/limo_client.py
  55. 1 0
      express_relay/sdk/python/mypy.ini
  56. 588 278
      express_relay/sdk/python/poetry.lock
  57. 6 3
      express_relay/sdk/python/pyproject.toml

+ 2 - 0
.pre-commit-config.yaml

@@ -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"

+ 6 - 6
express_relay/sdk/js/src/examples/simpleSearcherLimo.ts

@@ -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));
 
@@ -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
@@ -101,7 +101,7 @@ class SimpleSearcherLimo {
 
     const router = getPdaAuthority(
       this.clientLimo.getProgramID(),
-      this.globalConfig
+      order.state.globalConfig
     );
     const bidAmount = new anchor.BN(argv.bid);
 

+ 1 - 1
express_relay/sdk/js/src/index.ts

@@ -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

+ 1 - 1
express_relay/sdk/python/README.md

@@ -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.

+ 61 - 23
express_relay/sdk/python/express_relay/client.py

@@ -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,
@@ -26,12 +37,14 @@ from express_relay.express_relay_types import (
     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 SVM_EXPRESS_RELAY_PROGRAM_ID,
 )
+from express_relay.svm.generated.express_relay.types import SubmitBidArgs
+from express_relay.svm.limo_client import LimoClient
 
 
 def _get_permitted_tokens(
@@ -182,16 +195,7 @@ class ExpressRelayClient:
         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
 
@@ -419,6 +423,40 @@ class ExpressRelayClient:
 
         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(
+            SVM_EXPRESS_RELAY_PROGRAM_ID, router
+        )
+        express_relay_metadata = LimoClient.get_express_relay_metadata_pda(
+            SVM_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,
@@ -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.
 
@@ -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,

+ 25 - 0
express_relay/sdk/python/express_relay/constants.py

@@ -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 = {
@@ -24,3 +28,24 @@ EXECUTION_WITNESS_TYPESTRING = (
 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"
+        ),
+    }
+}

+ 103 - 4
express_relay/sdk/python/express_relay/express_relay_types.py

@@ -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
@@ -85,7 +96,7 @@ class TokenAmount(BaseModel):
     amount: IntString
 
 
-class Bid(BaseModel):
+class BidEvm(BaseModel):
     """
     Attributes:
         amount: The amount of the bid in wei.
@@ -102,6 +113,62 @@ class Bid(BaseModel):
     permission_key: HexString
 
 
+class _TransactionPydanticAnnotation:
+    @classmethod
+    def __get_pydantic_core_schema__(
+        cls,
+        _source_type: Any,
+        _handler: GetCoreSchemaHandler,
+    ) -> core_schema.CoreSchema:
+        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"
@@ -373,7 +440,7 @@ class UnsubscribeMessageParams(BaseModel):
     chain_ids: list[str]
 
 
-class PostBidMessageParams(BaseModel):
+class PostBidMessageParamsEvm(BaseModel):
     """
     Attributes:
         method: A string literal "post_bid".
@@ -392,6 +459,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:
+    if isinstance(v, dict):
+        if "transaction" in v:
+            return "svm"
+        return "evm"
+    if getattr(v, "transaction", None):
+        return "svm"
+    return "evm"
+
+
+PostBidMessageParams = Annotated[
+    Union[
+        Annotated[PostBidMessageParamsEvm, Tag("evm")],
+        Annotated[PostBidMessageParamsSvm, Tag("svm")],
+    ],
+    Discriminator(get_discriminator_value),
+]
+
+
 class PostOpportunityBidMessageParams(BaseModel):
     """
     Attributes:

+ 206 - 0
express_relay/sdk/python/express_relay/searcher/examples/simple_searcher_svm.py

@@ -0,0 +1,206 @@
+import argparse
+import asyncio
+import logging
+from decimal import Decimal
+
+from solana.rpc.async_api import AsyncClient
+from solders.keypair import Keypair
+from solders.pubkey import Pubkey
+from solders.transaction import Transaction
+
+from express_relay.client import (
+    ExpressRelayClient,
+)
+from express_relay.constants import SVM_CONFIGS
+from express_relay.express_relay_types import (
+    BidStatus,
+    BidStatusUpdate,
+    BidSvm,
+)
+from express_relay.svm.limo_client import LimoClient, OrderStateAndAddress
+
+DEADLINE = 2**62
+logger = logging.getLogger(__name__)
+
+
+class SimpleSearcherSvm:
+    def __init__(
+        self,
+        server_url: str,
+        private_key: Keypair,
+        bid_amount: int,
+        chain_id: str,
+        svm_rpc_endpoint: str,
+        limo_global_config: str,
+        api_key: str | None = None,
+    ):
+        self.client = ExpressRelayClient(
+            server_url,
+            api_key,
+            None,
+            self.bid_status_callback,
+        )
+        self.private_key = private_key
+        self.bid_amount = bid_amount
+        self.chain_id = chain_id
+        if self.chain_id not in SVM_CONFIGS:
+            raise ValueError(f"Chain ID {self.chain_id} not supported")
+        self.svm_config = SVM_CONFIGS[self.chain_id]
+        self.rpc_client = AsyncClient(svm_rpc_endpoint)
+        self.limo_client = LimoClient(
+            self.rpc_client, global_config=Pubkey.from_string(limo_global_config)
+        )
+
+    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.
+        """
+        id = bid_status_update.id
+        bid_status = bid_status_update.bid_status
+        result = bid_status_update.result
+
+        result_details = ""
+        if bid_status == BidStatus("submitted") or bid_status == BidStatus("won"):
+            result_details = f", transaction {result}"
+        elif bid_status == BidStatus("lost"):
+            if result:
+                result_details = f", transaction {result}"
+        logger.info(f"Bid status for bid {id}: {bid_status.value}{result_details}")
+
+    async def bid_on_new_orders(self):
+        orders = await self.limo_client.get_all_orders_state_and_address_with_filters(
+            []
+        )
+        orders = [
+            order for order in orders if order["state"].remaining_input_amount > 0
+        ]
+        if len(orders) == 0:
+            logger.info("No orders to bid on")
+            return
+        for order in orders:
+            await self.evaluate_order(order)
+
+    async def evaluate_order(self, order: OrderStateAndAddress):
+        input_mint_decimals = await self.limo_client.get_mint_decimals(
+            order["state"].input_mint
+        )
+        output_mint_decimals = await self.limo_client.get_mint_decimals(
+            order["state"].output_mint
+        )
+        input_amount_decimals = Decimal(
+            order["state"].remaining_input_amount
+        ) / Decimal(10**input_mint_decimals)
+        output_amount_decimals = Decimal(
+            order["state"].expected_output_amount
+        ) / Decimal(10**output_mint_decimals)
+        logger.info(
+            f"Order address {order['address']}\n"
+            f"Sell token {order['state'].input_mint} amount: {input_amount_decimals}\n"
+            f"Buy token {order['state'].output_mint} amount: {output_amount_decimals}"
+        )
+        ixs_take_order = await self.limo_client.take_order_ix(
+            self.private_key.pubkey(),
+            order,
+            input_amount_decimals,
+            input_mint_decimals,
+            self.svm_config["express_relay_program"],
+        )
+        router = self.limo_client.get_pda_authority(
+            self.limo_client.get_program_id(), order["state"].global_config
+        )
+        submit_bid_ix = self.client.get_svm_submit_bid_instruction(
+            searcher=self.private_key.pubkey(),
+            router=router,
+            permission_key=order["address"],
+            bid_amount=self.bid_amount,
+            deadline=DEADLINE,
+            chain_id=self.chain_id,
+        )
+        transaction = Transaction.new_with_payer(
+            [submit_bid_ix] + ixs_take_order, self.private_key.pubkey()
+        )
+
+        blockhash = (await self.rpc_client.get_latest_blockhash()).value
+        transaction.partial_sign(
+            [self.private_key], recent_blockhash=blockhash.blockhash
+        )
+        bid = BidSvm(transaction=transaction, chain_id=self.chain_id)
+        bid_id = await self.client.submit_bid(bid, False)
+        print(f"Submitted bid {bid_id} for order {order['address']}")
+
+
+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 in base58 format",
+    )
+    parser.add_argument(
+        "--chain-id",
+        type=str,
+        required=True,
+        help="Chain ID of the SVM network to submit bids",
+    )
+    parser.add_argument(
+        "--endpoint-express-relay",
+        type=str,
+        required=True,
+        help="Server endpoint to use for submitting bids",
+    )
+    parser.add_argument(
+        "--endpoint-svm",
+        type=str,
+        required=True,
+        help="Server endpoint to use for 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",
+    )
+    parser.add_argument(
+        "--global-config",
+        type=str,
+        required=True,
+        help="Limo program global config to use",
+    )
+    parser.add_argument(
+        "--bid",
+        type=int,
+        default=100,
+        required=True,
+        help="The amount of bid to submit for each opportunity",
+    )
+    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)
+
+    searcher = SimpleSearcherSvm(
+        args.endpoint_express_relay,
+        Keypair.from_base58_string(args.private_key),
+        args.bid,
+        args.chain_id,
+        args.endpoint_svm,
+        args.global_config,
+        args.api_key,
+    )
+
+    await searcher.bid_on_new_orders()
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 0 - 0
express_relay/sdk/python/express_relay/svm/__init__.py


+ 0 - 0
express_relay/sdk/python/express_relay/svm/generated/__init__.py


+ 0 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/__init__.py


+ 2 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/accounts/__init__.py

@@ -0,0 +1,2 @@
+from .config_router import ConfigRouter, ConfigRouterJSON
+from .express_relay_metadata import ExpressRelayMetadata, ExpressRelayMetadataJSON

+ 85 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/accounts/config_router.py

@@ -0,0 +1,85 @@
+import typing
+from dataclasses import dataclass
+from solders.pubkey import Pubkey
+from solana.rpc.async_api import AsyncClient
+from solana.rpc.commitment import Commitment
+import borsh_construct as borsh
+from anchorpy.coder.accounts import ACCOUNT_DISCRIMINATOR_SIZE
+from anchorpy.error import AccountInvalidDiscriminator
+from anchorpy.utils.rpc import get_multiple_accounts
+from anchorpy.borsh_extension import BorshPubkey
+from ..program_id import PROGRAM_ID
+
+
+class ConfigRouterJSON(typing.TypedDict):
+    router: str
+    split: int
+
+
+@dataclass
+class ConfigRouter:
+    discriminator: typing.ClassVar = b"\x87B\xf0\xa6^\xc6\xbb$"
+    layout: typing.ClassVar = borsh.CStruct("router" / BorshPubkey, "split" / borsh.U64)
+    router: Pubkey
+    split: int
+
+    @classmethod
+    async def fetch(
+        cls,
+        conn: AsyncClient,
+        address: Pubkey,
+        commitment: typing.Optional[Commitment] = None,
+        program_id: Pubkey = PROGRAM_ID,
+    ) -> typing.Optional["ConfigRouter"]:
+        resp = await conn.get_account_info(address, commitment=commitment)
+        info = resp.value
+        if info is None:
+            return None
+        if info.owner != program_id:
+            raise ValueError("Account does not belong to this program")
+        bytes_data = info.data
+        return cls.decode(bytes_data)
+
+    @classmethod
+    async def fetch_multiple(
+        cls,
+        conn: AsyncClient,
+        addresses: list[Pubkey],
+        commitment: typing.Optional[Commitment] = None,
+        program_id: Pubkey = PROGRAM_ID,
+    ) -> typing.List[typing.Optional["ConfigRouter"]]:
+        infos = await get_multiple_accounts(conn, addresses, commitment=commitment)
+        res: typing.List[typing.Optional["ConfigRouter"]] = []
+        for info in infos:
+            if info is None:
+                res.append(None)
+                continue
+            if info.account.owner != program_id:
+                raise ValueError("Account does not belong to this program")
+            res.append(cls.decode(info.account.data))
+        return res
+
+    @classmethod
+    def decode(cls, data: bytes) -> "ConfigRouter":
+        if data[:ACCOUNT_DISCRIMINATOR_SIZE] != cls.discriminator:
+            raise AccountInvalidDiscriminator(
+                "The discriminator for this account is invalid"
+            )
+        dec = ConfigRouter.layout.parse(data[ACCOUNT_DISCRIMINATOR_SIZE:])
+        return cls(
+            router=dec.router,
+            split=dec.split,
+        )
+
+    def to_json(self) -> ConfigRouterJSON:
+        return {
+            "router": str(self.router),
+            "split": self.split,
+        }
+
+    @classmethod
+    def from_json(cls, obj: ConfigRouterJSON) -> "ConfigRouter":
+        return cls(
+            router=Pubkey.from_string(obj["router"]),
+            split=obj["split"],
+        )

+ 106 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/accounts/express_relay_metadata.py

@@ -0,0 +1,106 @@
+import typing
+from dataclasses import dataclass
+from solders.pubkey import Pubkey
+from solana.rpc.async_api import AsyncClient
+from solana.rpc.commitment import Commitment
+import borsh_construct as borsh
+from anchorpy.coder.accounts import ACCOUNT_DISCRIMINATOR_SIZE
+from anchorpy.error import AccountInvalidDiscriminator
+from anchorpy.utils.rpc import get_multiple_accounts
+from anchorpy.borsh_extension import BorshPubkey
+from ..program_id import PROGRAM_ID
+
+
+class ExpressRelayMetadataJSON(typing.TypedDict):
+    admin: str
+    relayer_signer: str
+    fee_receiver_relayer: str
+    split_router_default: int
+    split_relayer: int
+
+
+@dataclass
+class ExpressRelayMetadata:
+    discriminator: typing.ClassVar = b"\xccK\x85\x07\xaf\xf1\x82\x0b"
+    layout: typing.ClassVar = borsh.CStruct(
+        "admin" / BorshPubkey,
+        "relayer_signer" / BorshPubkey,
+        "fee_receiver_relayer" / BorshPubkey,
+        "split_router_default" / borsh.U64,
+        "split_relayer" / borsh.U64,
+    )
+    admin: Pubkey
+    relayer_signer: Pubkey
+    fee_receiver_relayer: Pubkey
+    split_router_default: int
+    split_relayer: int
+
+    @classmethod
+    async def fetch(
+        cls,
+        conn: AsyncClient,
+        address: Pubkey,
+        commitment: typing.Optional[Commitment] = None,
+        program_id: Pubkey = PROGRAM_ID,
+    ) -> typing.Optional["ExpressRelayMetadata"]:
+        resp = await conn.get_account_info(address, commitment=commitment)
+        info = resp.value
+        if info is None:
+            return None
+        if info.owner != program_id:
+            raise ValueError("Account does not belong to this program")
+        bytes_data = info.data
+        return cls.decode(bytes_data)
+
+    @classmethod
+    async def fetch_multiple(
+        cls,
+        conn: AsyncClient,
+        addresses: list[Pubkey],
+        commitment: typing.Optional[Commitment] = None,
+        program_id: Pubkey = PROGRAM_ID,
+    ) -> typing.List[typing.Optional["ExpressRelayMetadata"]]:
+        infos = await get_multiple_accounts(conn, addresses, commitment=commitment)
+        res: typing.List[typing.Optional["ExpressRelayMetadata"]] = []
+        for info in infos:
+            if info is None:
+                res.append(None)
+                continue
+            if info.account.owner != program_id:
+                raise ValueError("Account does not belong to this program")
+            res.append(cls.decode(info.account.data))
+        return res
+
+    @classmethod
+    def decode(cls, data: bytes) -> "ExpressRelayMetadata":
+        if data[:ACCOUNT_DISCRIMINATOR_SIZE] != cls.discriminator:
+            raise AccountInvalidDiscriminator(
+                "The discriminator for this account is invalid"
+            )
+        dec = ExpressRelayMetadata.layout.parse(data[ACCOUNT_DISCRIMINATOR_SIZE:])
+        return cls(
+            admin=dec.admin,
+            relayer_signer=dec.relayer_signer,
+            fee_receiver_relayer=dec.fee_receiver_relayer,
+            split_router_default=dec.split_router_default,
+            split_relayer=dec.split_relayer,
+        )
+
+    def to_json(self) -> ExpressRelayMetadataJSON:
+        return {
+            "admin": str(self.admin),
+            "relayer_signer": str(self.relayer_signer),
+            "fee_receiver_relayer": str(self.fee_receiver_relayer),
+            "split_router_default": self.split_router_default,
+            "split_relayer": self.split_relayer,
+        }
+
+    @classmethod
+    def from_json(cls, obj: ExpressRelayMetadataJSON) -> "ExpressRelayMetadata":
+        return cls(
+            admin=Pubkey.from_string(obj["admin"]),
+            relayer_signer=Pubkey.from_string(obj["relayer_signer"]),
+            fee_receiver_relayer=Pubkey.from_string(obj["fee_receiver_relayer"]),
+            split_router_default=obj["split_router_default"],
+            split_relayer=obj["split_relayer"],
+        )

+ 29 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/errors/__init__.py

@@ -0,0 +1,29 @@
+import typing
+import re
+from solders.transaction_status import (
+    InstructionErrorCustom,
+    TransactionErrorInstructionError,
+)
+from solana.rpc.core import RPCException
+from solders.rpc.errors import SendTransactionPreflightFailureMessage
+from anchorpy.error import extract_code_and_logs
+from ..program_id import PROGRAM_ID
+from . import anchor
+from . import custom
+
+
+def from_code(code: int) -> typing.Union[custom.CustomError, anchor.AnchorError, None]:
+    return custom.from_code(code) if code >= 6000 else anchor.from_code(code)
+
+
+error_re = re.compile(r"Program (\w+) failed: custom program error: (\w+)")
+
+
+def from_tx_error(
+    error: RPCException,
+) -> typing.Union[anchor.AnchorError, custom.CustomError, None]:
+    err_info = error.args[0]
+    extracted = extract_code_and_logs(err_info, PROGRAM_ID)
+    if extracted is None:
+        return None
+    return from_code(extracted[0])

+ 590 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/errors/anchor.py

@@ -0,0 +1,590 @@
+import typing
+from anchorpy.error import ProgramError
+
+
+class InstructionMissing(ProgramError):
+    def __init__(self):
+        super().__init__(100, "8 byte instruction identifier not provided")
+
+    code = 100
+    name = "InstructionMissing"
+    msg = "8 byte instruction identifier not provided"
+
+
+class InstructionFallbackNotFound(ProgramError):
+    def __init__(self):
+        super().__init__(101, "Fallback functions are not supported")
+
+    code = 101
+    name = "InstructionFallbackNotFound"
+    msg = "Fallback functions are not supported"
+
+
+class InstructionDidNotDeserialize(ProgramError):
+    def __init__(self):
+        super().__init__(102, "The program could not deserialize the given instruction")
+
+    code = 102
+    name = "InstructionDidNotDeserialize"
+    msg = "The program could not deserialize the given instruction"
+
+
+class InstructionDidNotSerialize(ProgramError):
+    def __init__(self):
+        super().__init__(103, "The program could not serialize the given instruction")
+
+    code = 103
+    name = "InstructionDidNotSerialize"
+    msg = "The program could not serialize the given instruction"
+
+
+class IdlInstructionStub(ProgramError):
+    def __init__(self):
+        super().__init__(1000, "The program was compiled without idl instructions")
+
+    code = 1000
+    name = "IdlInstructionStub"
+    msg = "The program was compiled without idl instructions"
+
+
+class IdlInstructionInvalidProgram(ProgramError):
+    def __init__(self):
+        super().__init__(
+            1001, "The transaction was given an invalid program for the IDL instruction"
+        )
+
+    code = 1001
+    name = "IdlInstructionInvalidProgram"
+    msg = "The transaction was given an invalid program for the IDL instruction"
+
+
+class ConstraintMut(ProgramError):
+    def __init__(self):
+        super().__init__(2000, "A mut constraint was violated")
+
+    code = 2000
+    name = "ConstraintMut"
+    msg = "A mut constraint was violated"
+
+
+class ConstraintHasOne(ProgramError):
+    def __init__(self):
+        super().__init__(2001, "A has_one constraint was violated")
+
+    code = 2001
+    name = "ConstraintHasOne"
+    msg = "A has_one constraint was violated"
+
+
+class ConstraintSigner(ProgramError):
+    def __init__(self):
+        super().__init__(2002, "A signer constraint was violated")
+
+    code = 2002
+    name = "ConstraintSigner"
+    msg = "A signer constraint was violated"
+
+
+class ConstraintRaw(ProgramError):
+    def __init__(self):
+        super().__init__(2003, "A raw constraint was violated")
+
+    code = 2003
+    name = "ConstraintRaw"
+    msg = "A raw constraint was violated"
+
+
+class ConstraintOwner(ProgramError):
+    def __init__(self):
+        super().__init__(2004, "An owner constraint was violated")
+
+    code = 2004
+    name = "ConstraintOwner"
+    msg = "An owner constraint was violated"
+
+
+class ConstraintRentExempt(ProgramError):
+    def __init__(self):
+        super().__init__(2005, "A rent exempt constraint was violated")
+
+    code = 2005
+    name = "ConstraintRentExempt"
+    msg = "A rent exempt constraint was violated"
+
+
+class ConstraintSeeds(ProgramError):
+    def __init__(self):
+        super().__init__(2006, "A seeds constraint was violated")
+
+    code = 2006
+    name = "ConstraintSeeds"
+    msg = "A seeds constraint was violated"
+
+
+class ConstraintExecutable(ProgramError):
+    def __init__(self):
+        super().__init__(2007, "An executable constraint was violated")
+
+    code = 2007
+    name = "ConstraintExecutable"
+    msg = "An executable constraint was violated"
+
+
+class ConstraintState(ProgramError):
+    def __init__(self):
+        super().__init__(2008, "A state constraint was violated")
+
+    code = 2008
+    name = "ConstraintState"
+    msg = "A state constraint was violated"
+
+
+class ConstraintAssociated(ProgramError):
+    def __init__(self):
+        super().__init__(2009, "An associated constraint was violated")
+
+    code = 2009
+    name = "ConstraintAssociated"
+    msg = "An associated constraint was violated"
+
+
+class ConstraintAssociatedInit(ProgramError):
+    def __init__(self):
+        super().__init__(2010, "An associated init constraint was violated")
+
+    code = 2010
+    name = "ConstraintAssociatedInit"
+    msg = "An associated init constraint was violated"
+
+
+class ConstraintClose(ProgramError):
+    def __init__(self):
+        super().__init__(2011, "A close constraint was violated")
+
+    code = 2011
+    name = "ConstraintClose"
+    msg = "A close constraint was violated"
+
+
+class ConstraintAddress(ProgramError):
+    def __init__(self):
+        super().__init__(2012, "An address constraint was violated")
+
+    code = 2012
+    name = "ConstraintAddress"
+    msg = "An address constraint was violated"
+
+
+class ConstraintZero(ProgramError):
+    def __init__(self):
+        super().__init__(2013, "Expected zero account discriminant")
+
+    code = 2013
+    name = "ConstraintZero"
+    msg = "Expected zero account discriminant"
+
+
+class ConstraintTokenMint(ProgramError):
+    def __init__(self):
+        super().__init__(2014, "A token mint constraint was violated")
+
+    code = 2014
+    name = "ConstraintTokenMint"
+    msg = "A token mint constraint was violated"
+
+
+class ConstraintTokenOwner(ProgramError):
+    def __init__(self):
+        super().__init__(2015, "A token owner constraint was violated")
+
+    code = 2015
+    name = "ConstraintTokenOwner"
+    msg = "A token owner constraint was violated"
+
+
+class ConstraintMintMintAuthority(ProgramError):
+    def __init__(self):
+        super().__init__(2016, "A mint mint authority constraint was violated")
+
+    code = 2016
+    name = "ConstraintMintMintAuthority"
+    msg = "A mint mint authority constraint was violated"
+
+
+class ConstraintMintFreezeAuthority(ProgramError):
+    def __init__(self):
+        super().__init__(2017, "A mint freeze authority constraint was violated")
+
+    code = 2017
+    name = "ConstraintMintFreezeAuthority"
+    msg = "A mint freeze authority constraint was violated"
+
+
+class ConstraintMintDecimals(ProgramError):
+    def __init__(self):
+        super().__init__(2018, "A mint decimals constraint was violated")
+
+    code = 2018
+    name = "ConstraintMintDecimals"
+    msg = "A mint decimals constraint was violated"
+
+
+class ConstraintSpace(ProgramError):
+    def __init__(self):
+        super().__init__(2019, "A space constraint was violated")
+
+    code = 2019
+    name = "ConstraintSpace"
+    msg = "A space constraint was violated"
+
+
+class RequireViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2500, "A require expression was violated")
+
+    code = 2500
+    name = "RequireViolated"
+    msg = "A require expression was violated"
+
+
+class RequireEqViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2501, "A require_eq expression was violated")
+
+    code = 2501
+    name = "RequireEqViolated"
+    msg = "A require_eq expression was violated"
+
+
+class RequireKeysEqViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2502, "A require_keys_eq expression was violated")
+
+    code = 2502
+    name = "RequireKeysEqViolated"
+    msg = "A require_keys_eq expression was violated"
+
+
+class RequireNeqViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2503, "A require_neq expression was violated")
+
+    code = 2503
+    name = "RequireNeqViolated"
+    msg = "A require_neq expression was violated"
+
+
+class RequireKeysNeqViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2504, "A require_keys_neq expression was violated")
+
+    code = 2504
+    name = "RequireKeysNeqViolated"
+    msg = "A require_keys_neq expression was violated"
+
+
+class RequireGtViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2505, "A require_gt expression was violated")
+
+    code = 2505
+    name = "RequireGtViolated"
+    msg = "A require_gt expression was violated"
+
+
+class RequireGteViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2506, "A require_gte expression was violated")
+
+    code = 2506
+    name = "RequireGteViolated"
+    msg = "A require_gte expression was violated"
+
+
+class AccountDiscriminatorAlreadySet(ProgramError):
+    def __init__(self):
+        super().__init__(
+            3000, "The account discriminator was already set on this account"
+        )
+
+    code = 3000
+    name = "AccountDiscriminatorAlreadySet"
+    msg = "The account discriminator was already set on this account"
+
+
+class AccountDiscriminatorNotFound(ProgramError):
+    def __init__(self):
+        super().__init__(3001, "No 8 byte discriminator was found on the account")
+
+    code = 3001
+    name = "AccountDiscriminatorNotFound"
+    msg = "No 8 byte discriminator was found on the account"
+
+
+class AccountDiscriminatorMismatch(ProgramError):
+    def __init__(self):
+        super().__init__(3002, "8 byte discriminator did not match what was expected")
+
+    code = 3002
+    name = "AccountDiscriminatorMismatch"
+    msg = "8 byte discriminator did not match what was expected"
+
+
+class AccountDidNotDeserialize(ProgramError):
+    def __init__(self):
+        super().__init__(3003, "Failed to deserialize the account")
+
+    code = 3003
+    name = "AccountDidNotDeserialize"
+    msg = "Failed to deserialize the account"
+
+
+class AccountDidNotSerialize(ProgramError):
+    def __init__(self):
+        super().__init__(3004, "Failed to serialize the account")
+
+    code = 3004
+    name = "AccountDidNotSerialize"
+    msg = "Failed to serialize the account"
+
+
+class AccountNotEnoughKeys(ProgramError):
+    def __init__(self):
+        super().__init__(3005, "Not enough account keys given to the instruction")
+
+    code = 3005
+    name = "AccountNotEnoughKeys"
+    msg = "Not enough account keys given to the instruction"
+
+
+class AccountNotMutable(ProgramError):
+    def __init__(self):
+        super().__init__(3006, "The given account is not mutable")
+
+    code = 3006
+    name = "AccountNotMutable"
+    msg = "The given account is not mutable"
+
+
+class AccountOwnedByWrongProgram(ProgramError):
+    def __init__(self):
+        super().__init__(
+            3007, "The given account is owned by a different program than expected"
+        )
+
+    code = 3007
+    name = "AccountOwnedByWrongProgram"
+    msg = "The given account is owned by a different program than expected"
+
+
+class InvalidProgramId(ProgramError):
+    def __init__(self):
+        super().__init__(3008, "Program ID was not as expected")
+
+    code = 3008
+    name = "InvalidProgramId"
+    msg = "Program ID was not as expected"
+
+
+class InvalidProgramExecutable(ProgramError):
+    def __init__(self):
+        super().__init__(3009, "Program account is not executable")
+
+    code = 3009
+    name = "InvalidProgramExecutable"
+    msg = "Program account is not executable"
+
+
+class AccountNotSigner(ProgramError):
+    def __init__(self):
+        super().__init__(3010, "The given account did not sign")
+
+    code = 3010
+    name = "AccountNotSigner"
+    msg = "The given account did not sign"
+
+
+class AccountNotSystemOwned(ProgramError):
+    def __init__(self):
+        super().__init__(3011, "The given account is not owned by the system program")
+
+    code = 3011
+    name = "AccountNotSystemOwned"
+    msg = "The given account is not owned by the system program"
+
+
+class AccountNotInitialized(ProgramError):
+    def __init__(self):
+        super().__init__(
+            3012, "The program expected this account to be already initialized"
+        )
+
+    code = 3012
+    name = "AccountNotInitialized"
+    msg = "The program expected this account to be already initialized"
+
+
+class AccountNotProgramData(ProgramError):
+    def __init__(self):
+        super().__init__(3013, "The given account is not a program data account")
+
+    code = 3013
+    name = "AccountNotProgramData"
+    msg = "The given account is not a program data account"
+
+
+class AccountNotAssociatedTokenAccount(ProgramError):
+    def __init__(self):
+        super().__init__(3014, "The given account is not the associated token account")
+
+    code = 3014
+    name = "AccountNotAssociatedTokenAccount"
+    msg = "The given account is not the associated token account"
+
+
+class AccountSysvarMismatch(ProgramError):
+    def __init__(self):
+        super().__init__(
+            3015, "The given public key does not match the required sysvar"
+        )
+
+    code = 3015
+    name = "AccountSysvarMismatch"
+    msg = "The given public key does not match the required sysvar"
+
+
+class StateInvalidAddress(ProgramError):
+    def __init__(self):
+        super().__init__(
+            4000, "The given state account does not have the correct address"
+        )
+
+    code = 4000
+    name = "StateInvalidAddress"
+    msg = "The given state account does not have the correct address"
+
+
+class Deprecated(ProgramError):
+    def __init__(self):
+        super().__init__(
+            5000, "The API being used is deprecated and should no longer be used"
+        )
+
+    code = 5000
+    name = "Deprecated"
+    msg = "The API being used is deprecated and should no longer be used"
+
+
+AnchorError = typing.Union[
+    InstructionMissing,
+    InstructionFallbackNotFound,
+    InstructionDidNotDeserialize,
+    InstructionDidNotSerialize,
+    IdlInstructionStub,
+    IdlInstructionInvalidProgram,
+    ConstraintMut,
+    ConstraintHasOne,
+    ConstraintSigner,
+    ConstraintRaw,
+    ConstraintOwner,
+    ConstraintRentExempt,
+    ConstraintSeeds,
+    ConstraintExecutable,
+    ConstraintState,
+    ConstraintAssociated,
+    ConstraintAssociatedInit,
+    ConstraintClose,
+    ConstraintAddress,
+    ConstraintZero,
+    ConstraintTokenMint,
+    ConstraintTokenOwner,
+    ConstraintMintMintAuthority,
+    ConstraintMintFreezeAuthority,
+    ConstraintMintDecimals,
+    ConstraintSpace,
+    RequireViolated,
+    RequireEqViolated,
+    RequireKeysEqViolated,
+    RequireNeqViolated,
+    RequireKeysNeqViolated,
+    RequireGtViolated,
+    RequireGteViolated,
+    AccountDiscriminatorAlreadySet,
+    AccountDiscriminatorNotFound,
+    AccountDiscriminatorMismatch,
+    AccountDidNotDeserialize,
+    AccountDidNotSerialize,
+    AccountNotEnoughKeys,
+    AccountNotMutable,
+    AccountOwnedByWrongProgram,
+    InvalidProgramId,
+    InvalidProgramExecutable,
+    AccountNotSigner,
+    AccountNotSystemOwned,
+    AccountNotInitialized,
+    AccountNotProgramData,
+    AccountNotAssociatedTokenAccount,
+    AccountSysvarMismatch,
+    StateInvalidAddress,
+    Deprecated,
+]
+ANCHOR_ERROR_MAP: dict[int, AnchorError] = {
+    100: InstructionMissing(),
+    101: InstructionFallbackNotFound(),
+    102: InstructionDidNotDeserialize(),
+    103: InstructionDidNotSerialize(),
+    1000: IdlInstructionStub(),
+    1001: IdlInstructionInvalidProgram(),
+    2000: ConstraintMut(),
+    2001: ConstraintHasOne(),
+    2002: ConstraintSigner(),
+    2003: ConstraintRaw(),
+    2004: ConstraintOwner(),
+    2005: ConstraintRentExempt(),
+    2006: ConstraintSeeds(),
+    2007: ConstraintExecutable(),
+    2008: ConstraintState(),
+    2009: ConstraintAssociated(),
+    2010: ConstraintAssociatedInit(),
+    2011: ConstraintClose(),
+    2012: ConstraintAddress(),
+    2013: ConstraintZero(),
+    2014: ConstraintTokenMint(),
+    2015: ConstraintTokenOwner(),
+    2016: ConstraintMintMintAuthority(),
+    2017: ConstraintMintFreezeAuthority(),
+    2018: ConstraintMintDecimals(),
+    2019: ConstraintSpace(),
+    2500: RequireViolated(),
+    2501: RequireEqViolated(),
+    2502: RequireKeysEqViolated(),
+    2503: RequireNeqViolated(),
+    2504: RequireKeysNeqViolated(),
+    2505: RequireGtViolated(),
+    2506: RequireGteViolated(),
+    3000: AccountDiscriminatorAlreadySet(),
+    3001: AccountDiscriminatorNotFound(),
+    3002: AccountDiscriminatorMismatch(),
+    3003: AccountDidNotDeserialize(),
+    3004: AccountDidNotSerialize(),
+    3005: AccountNotEnoughKeys(),
+    3006: AccountNotMutable(),
+    3007: AccountOwnedByWrongProgram(),
+    3008: InvalidProgramId(),
+    3009: InvalidProgramExecutable(),
+    3010: AccountNotSigner(),
+    3011: AccountNotSystemOwned(),
+    3012: AccountNotInitialized(),
+    3013: AccountNotProgramData(),
+    3014: AccountNotAssociatedTokenAccount(),
+    3015: AccountSysvarMismatch(),
+    4000: StateInvalidAddress(),
+    5000: Deprecated(),
+}
+
+
+def from_code(code: int) -> typing.Optional[AnchorError]:
+    maybe_err = ANCHOR_ERROR_MAP.get(code)
+    if maybe_err is None:
+        return None
+    return maybe_err

+ 103 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/errors/custom.py

@@ -0,0 +1,103 @@
+import typing
+from anchorpy.error import ProgramError
+
+
+class FeeSplitLargerThanPrecision(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6000, "Fee split(s) larger than fee precision")
+
+    code = 6000
+    name = "FeeSplitLargerThanPrecision"
+    msg = "Fee split(s) larger than fee precision"
+
+
+class FeesHigherThanBid(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6001, "Fees higher than bid")
+
+    code = 6001
+    name = "FeesHigherThanBid"
+    msg = "Fees higher than bid"
+
+
+class DeadlinePassed(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6002, "Deadline passed")
+
+    code = 6002
+    name = "DeadlinePassed"
+    msg = "Deadline passed"
+
+
+class InvalidCPISubmitBid(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6003, "Invalid CPI into submit bid instruction")
+
+    code = 6003
+    name = "InvalidCPISubmitBid"
+    msg = "Invalid CPI into submit bid instruction"
+
+
+class MissingPermission(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6004, "Missing permission")
+
+    code = 6004
+    name = "MissingPermission"
+    msg = "Missing permission"
+
+
+class MultiplePermissions(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6005, "Multiple permissions")
+
+    code = 6005
+    name = "MultiplePermissions"
+    msg = "Multiple permissions"
+
+
+class InsufficientSearcherFunds(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6006, "Insufficient Searcher Funds")
+
+    code = 6006
+    name = "InsufficientSearcherFunds"
+    msg = "Insufficient Searcher Funds"
+
+
+class InsufficientRent(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6007, "Insufficient funds for rent")
+
+    code = 6007
+    name = "InsufficientRent"
+    msg = "Insufficient funds for rent"
+
+
+CustomError = typing.Union[
+    FeeSplitLargerThanPrecision,
+    FeesHigherThanBid,
+    DeadlinePassed,
+    InvalidCPISubmitBid,
+    MissingPermission,
+    MultiplePermissions,
+    InsufficientSearcherFunds,
+    InsufficientRent,
+]
+CUSTOM_ERROR_MAP: dict[int, CustomError] = {
+    6000: FeeSplitLargerThanPrecision(),
+    6001: FeesHigherThanBid(),
+    6002: DeadlinePassed(),
+    6003: InvalidCPISubmitBid(),
+    6004: MissingPermission(),
+    6005: MultiplePermissions(),
+    6006: InsufficientSearcherFunds(),
+    6007: InsufficientRent(),
+}
+
+
+def from_code(code: int) -> typing.Optional[CustomError]:
+    maybe_err = CUSTOM_ERROR_MAP.get(code)
+    if maybe_err is None:
+        return None
+    return maybe_err

+ 12 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/__init__.py

@@ -0,0 +1,12 @@
+from .initialize import initialize, InitializeArgs, InitializeAccounts
+from .set_admin import set_admin, SetAdminAccounts
+from .set_relayer import set_relayer, SetRelayerAccounts
+from .set_splits import set_splits, SetSplitsArgs, SetSplitsAccounts
+from .set_router_split import (
+    set_router_split,
+    SetRouterSplitArgs,
+    SetRouterSplitAccounts,
+)
+from .submit_bid import submit_bid, SubmitBidArgs, SubmitBidAccounts
+from .check_permission import check_permission, CheckPermissionAccounts
+from .withdraw_fees import withdraw_fees, WithdrawFeesAccounts

+ 41 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/check_permission.py

@@ -0,0 +1,41 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.instruction import Instruction, AccountMeta
+from ..program_id import PROGRAM_ID
+
+
+class CheckPermissionAccounts(typing.TypedDict):
+    sysvar_instructions: Pubkey
+    permission: Pubkey
+    router: Pubkey
+    config_router: Pubkey
+    express_relay_metadata: Pubkey
+
+
+def check_permission(
+    accounts: CheckPermissionAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(
+            pubkey=accounts["sysvar_instructions"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(pubkey=accounts["permission"], is_signer=False, is_writable=False),
+        AccountMeta(pubkey=accounts["router"], is_signer=False, is_writable=False),
+        AccountMeta(
+            pubkey=accounts["config_router"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(
+            pubkey=accounts["express_relay_metadata"],
+            is_signer=False,
+            is_writable=False,
+        ),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"\x9a\xc7\xe8\xf2`H\xc5\xec"
+    encoded_args = b""
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 55 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/initialize.py

@@ -0,0 +1,55 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.system_program import ID as SYS_PROGRAM_ID
+from solders.instruction import Instruction, AccountMeta
+import borsh_construct as borsh
+from .. import types
+from ..program_id import PROGRAM_ID
+
+
+class InitializeArgs(typing.TypedDict):
+    data: types.initialize_args.InitializeArgs
+
+
+layout = borsh.CStruct("data" / types.initialize_args.InitializeArgs.layout)
+
+
+class InitializeAccounts(typing.TypedDict):
+    payer: Pubkey
+    express_relay_metadata: Pubkey
+    admin: Pubkey
+    relayer_signer: Pubkey
+    fee_receiver_relayer: Pubkey
+
+
+def initialize(
+    args: InitializeArgs,
+    accounts: InitializeAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(pubkey=accounts["payer"], is_signer=True, is_writable=True),
+        AccountMeta(
+            pubkey=accounts["express_relay_metadata"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(pubkey=accounts["admin"], is_signer=False, is_writable=False),
+        AccountMeta(
+            pubkey=accounts["relayer_signer"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(
+            pubkey=accounts["fee_receiver_relayer"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"\xaf\xafm\x1f\r\x98\x9b\xed"
+    encoded_args = layout.build(
+        {
+            "data": args["data"].to_encodable(),
+        }
+    )
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 31 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/set_admin.py

@@ -0,0 +1,31 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.instruction import Instruction, AccountMeta
+from ..program_id import PROGRAM_ID
+
+
+class SetAdminAccounts(typing.TypedDict):
+    admin: Pubkey
+    express_relay_metadata: Pubkey
+    admin_new: Pubkey
+
+
+def set_admin(
+    accounts: SetAdminAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(pubkey=accounts["admin"], is_signer=True, is_writable=True),
+        AccountMeta(
+            pubkey=accounts["express_relay_metadata"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(pubkey=accounts["admin_new"], is_signer=False, is_writable=False),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"\xfb\xa3\x004[\xc2\xbb\\"
+    encoded_args = b""
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 37 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/set_relayer.py

@@ -0,0 +1,37 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.instruction import Instruction, AccountMeta
+from ..program_id import PROGRAM_ID
+
+
+class SetRelayerAccounts(typing.TypedDict):
+    admin: Pubkey
+    express_relay_metadata: Pubkey
+    relayer_signer: Pubkey
+    fee_receiver_relayer: Pubkey
+
+
+def set_relayer(
+    accounts: SetRelayerAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(pubkey=accounts["admin"], is_signer=True, is_writable=True),
+        AccountMeta(
+            pubkey=accounts["express_relay_metadata"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["relayer_signer"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(
+            pubkey=accounts["fee_receiver_relayer"], is_signer=False, is_writable=False
+        ),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"\x17\xf3!XnT\xc4%"
+    encoded_args = b""
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 53 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/set_router_split.py

@@ -0,0 +1,53 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.system_program import ID as SYS_PROGRAM_ID
+from solders.instruction import Instruction, AccountMeta
+import borsh_construct as borsh
+from .. import types
+from ..program_id import PROGRAM_ID
+
+
+class SetRouterSplitArgs(typing.TypedDict):
+    data: types.set_router_split_args.SetRouterSplitArgs
+
+
+layout = borsh.CStruct("data" / types.set_router_split_args.SetRouterSplitArgs.layout)
+
+
+class SetRouterSplitAccounts(typing.TypedDict):
+    admin: Pubkey
+    config_router: Pubkey
+    express_relay_metadata: Pubkey
+    router: Pubkey
+
+
+def set_router_split(
+    args: SetRouterSplitArgs,
+    accounts: SetRouterSplitAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(pubkey=accounts["admin"], is_signer=True, is_writable=True),
+        AccountMeta(
+            pubkey=accounts["config_router"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["express_relay_metadata"],
+            is_signer=False,
+            is_writable=False,
+        ),
+        AccountMeta(pubkey=accounts["router"], is_signer=False, is_writable=False),
+        AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"\x10\x96j\r\x1b\xbfh\x08"
+    encoded_args = layout.build(
+        {
+            "data": args["data"].to_encodable(),
+        }
+    )
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 43 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/set_splits.py

@@ -0,0 +1,43 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.instruction import Instruction, AccountMeta
+import borsh_construct as borsh
+from .. import types
+from ..program_id import PROGRAM_ID
+
+
+class SetSplitsArgs(typing.TypedDict):
+    data: types.set_splits_args.SetSplitsArgs
+
+
+layout = borsh.CStruct("data" / types.set_splits_args.SetSplitsArgs.layout)
+
+
+class SetSplitsAccounts(typing.TypedDict):
+    admin: Pubkey
+    express_relay_metadata: Pubkey
+
+
+def set_splits(
+    args: SetSplitsArgs,
+    accounts: SetSplitsAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(pubkey=accounts["admin"], is_signer=True, is_writable=True),
+        AccountMeta(
+            pubkey=accounts["express_relay_metadata"], is_signer=False, is_writable=True
+        ),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"\xaf\x02V1\xe1\xca\xe8\xbd"
+    encoded_args = layout.build(
+        {
+            "data": args["data"].to_encodable(),
+        }
+    )
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 65 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/submit_bid.py

@@ -0,0 +1,65 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.system_program import ID as SYS_PROGRAM_ID
+from solders.instruction import Instruction, AccountMeta
+import borsh_construct as borsh
+from .. import types
+from ..program_id import PROGRAM_ID
+
+
+class SubmitBidArgs(typing.TypedDict):
+    data: types.submit_bid_args.SubmitBidArgs
+
+
+layout = borsh.CStruct("data" / types.submit_bid_args.SubmitBidArgs.layout)
+
+
+class SubmitBidAccounts(typing.TypedDict):
+    searcher: Pubkey
+    relayer_signer: Pubkey
+    permission: Pubkey
+    router: Pubkey
+    config_router: Pubkey
+    express_relay_metadata: Pubkey
+    fee_receiver_relayer: Pubkey
+    sysvar_instructions: Pubkey
+
+
+def submit_bid(
+    args: SubmitBidArgs,
+    accounts: SubmitBidAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(pubkey=accounts["searcher"], is_signer=True, is_writable=True),
+        AccountMeta(
+            pubkey=accounts["relayer_signer"], is_signer=True, is_writable=False
+        ),
+        AccountMeta(pubkey=accounts["permission"], is_signer=False, is_writable=False),
+        AccountMeta(pubkey=accounts["router"], is_signer=False, is_writable=True),
+        AccountMeta(
+            pubkey=accounts["config_router"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(
+            pubkey=accounts["express_relay_metadata"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["fee_receiver_relayer"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
+        AccountMeta(
+            pubkey=accounts["sysvar_instructions"], is_signer=False, is_writable=False
+        ),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"\x13\xa4\xed\xfe@\x8b\xed]"
+    encoded_args = layout.build(
+        {
+            "data": args["data"].to_encodable(),
+        }
+    )
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 33 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/instructions/withdraw_fees.py

@@ -0,0 +1,33 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.instruction import Instruction, AccountMeta
+from ..program_id import PROGRAM_ID
+
+
+class WithdrawFeesAccounts(typing.TypedDict):
+    admin: Pubkey
+    fee_receiver_admin: Pubkey
+    express_relay_metadata: Pubkey
+
+
+def withdraw_fees(
+    accounts: WithdrawFeesAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(pubkey=accounts["admin"], is_signer=True, is_writable=True),
+        AccountMeta(
+            pubkey=accounts["fee_receiver_admin"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["express_relay_metadata"], is_signer=False, is_writable=True
+        ),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"\xc6\xd4\xabm\x90\xd7\xaeY"
+    encoded_args = b""
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 3 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/program_id.py

@@ -0,0 +1,3 @@
+from solders.pubkey import Pubkey
+
+PROGRAM_ID = Pubkey.from_string("PytERJFhAKuNNuaiXkApLfWzwNwSNDACpigT3LwQfou")

+ 9 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/types/__init__.py

@@ -0,0 +1,9 @@
+import typing
+from . import initialize_args
+from .initialize_args import InitializeArgs, InitializeArgsJSON
+from . import set_router_split_args
+from .set_router_split_args import SetRouterSplitArgs, SetRouterSplitArgsJSON
+from . import set_splits_args
+from .set_splits_args import SetSplitsArgs, SetSplitsArgsJSON
+from . import submit_bid_args
+from .submit_bid_args import SubmitBidArgs, SubmitBidArgsJSON

+ 45 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/types/initialize_args.py

@@ -0,0 +1,45 @@
+from __future__ import annotations
+import typing
+from dataclasses import dataclass
+from construct import Container
+import borsh_construct as borsh
+
+
+class InitializeArgsJSON(typing.TypedDict):
+    split_router_default: int
+    split_relayer: int
+
+
+@dataclass
+class InitializeArgs:
+    layout: typing.ClassVar = borsh.CStruct(
+        "split_router_default" / borsh.U64, "split_relayer" / borsh.U64
+    )
+    split_router_default: int
+    split_relayer: int
+
+    @classmethod
+    def from_decoded(cls, obj: Container) -> "InitializeArgs":
+        return cls(
+            split_router_default=obj.split_router_default,
+            split_relayer=obj.split_relayer,
+        )
+
+    def to_encodable(self) -> dict[str, typing.Any]:
+        return {
+            "split_router_default": self.split_router_default,
+            "split_relayer": self.split_relayer,
+        }
+
+    def to_json(self) -> InitializeArgsJSON:
+        return {
+            "split_router_default": self.split_router_default,
+            "split_relayer": self.split_relayer,
+        }
+
+    @classmethod
+    def from_json(cls, obj: InitializeArgsJSON) -> "InitializeArgs":
+        return cls(
+            split_router_default=obj["split_router_default"],
+            split_relayer=obj["split_relayer"],
+        )

+ 29 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/types/set_router_split_args.py

@@ -0,0 +1,29 @@
+from __future__ import annotations
+import typing
+from dataclasses import dataclass
+from construct import Container
+import borsh_construct as borsh
+
+
+class SetRouterSplitArgsJSON(typing.TypedDict):
+    split_router: int
+
+
+@dataclass
+class SetRouterSplitArgs:
+    layout: typing.ClassVar = borsh.CStruct("split_router" / borsh.U64)
+    split_router: int
+
+    @classmethod
+    def from_decoded(cls, obj: Container) -> "SetRouterSplitArgs":
+        return cls(split_router=obj.split_router)
+
+    def to_encodable(self) -> dict[str, typing.Any]:
+        return {"split_router": self.split_router}
+
+    def to_json(self) -> SetRouterSplitArgsJSON:
+        return {"split_router": self.split_router}
+
+    @classmethod
+    def from_json(cls, obj: SetRouterSplitArgsJSON) -> "SetRouterSplitArgs":
+        return cls(split_router=obj["split_router"])

+ 45 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/types/set_splits_args.py

@@ -0,0 +1,45 @@
+from __future__ import annotations
+import typing
+from dataclasses import dataclass
+from construct import Container
+import borsh_construct as borsh
+
+
+class SetSplitsArgsJSON(typing.TypedDict):
+    split_router_default: int
+    split_relayer: int
+
+
+@dataclass
+class SetSplitsArgs:
+    layout: typing.ClassVar = borsh.CStruct(
+        "split_router_default" / borsh.U64, "split_relayer" / borsh.U64
+    )
+    split_router_default: int
+    split_relayer: int
+
+    @classmethod
+    def from_decoded(cls, obj: Container) -> "SetSplitsArgs":
+        return cls(
+            split_router_default=obj.split_router_default,
+            split_relayer=obj.split_relayer,
+        )
+
+    def to_encodable(self) -> dict[str, typing.Any]:
+        return {
+            "split_router_default": self.split_router_default,
+            "split_relayer": self.split_relayer,
+        }
+
+    def to_json(self) -> SetSplitsArgsJSON:
+        return {
+            "split_router_default": self.split_router_default,
+            "split_relayer": self.split_relayer,
+        }
+
+    @classmethod
+    def from_json(cls, obj: SetSplitsArgsJSON) -> "SetSplitsArgs":
+        return cls(
+            split_router_default=obj["split_router_default"],
+            split_relayer=obj["split_relayer"],
+        )

+ 33 - 0
express_relay/sdk/python/express_relay/svm/generated/express_relay/types/submit_bid_args.py

@@ -0,0 +1,33 @@
+from __future__ import annotations
+import typing
+from dataclasses import dataclass
+from construct import Container
+import borsh_construct as borsh
+
+
+class SubmitBidArgsJSON(typing.TypedDict):
+    deadline: int
+    bid_amount: int
+
+
+@dataclass
+class SubmitBidArgs:
+    layout: typing.ClassVar = borsh.CStruct(
+        "deadline" / borsh.I64, "bid_amount" / borsh.U64
+    )
+    deadline: int
+    bid_amount: int
+
+    @classmethod
+    def from_decoded(cls, obj: Container) -> "SubmitBidArgs":
+        return cls(deadline=obj.deadline, bid_amount=obj.bid_amount)
+
+    def to_encodable(self) -> dict[str, typing.Any]:
+        return {"deadline": self.deadline, "bid_amount": self.bid_amount}
+
+    def to_json(self) -> SubmitBidArgsJSON:
+        return {"deadline": self.deadline, "bid_amount": self.bid_amount}
+
+    @classmethod
+    def from_json(cls, obj: SubmitBidArgsJSON) -> "SubmitBidArgs":
+        return cls(deadline=obj["deadline"], bid_amount=obj["bid_amount"])

+ 0 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/__init__.py


+ 2 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/accounts/__init__.py

@@ -0,0 +1,2 @@
+from .order import Order, OrderJSON
+from .global_config import GlobalConfig, GlobalConfigJSON

+ 162 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/accounts/global_config.py

@@ -0,0 +1,162 @@
+import typing
+from dataclasses import dataclass
+from solders.pubkey import Pubkey
+from solana.rpc.async_api import AsyncClient
+from solana.rpc.commitment import Commitment
+import borsh_construct as borsh
+from anchorpy.coder.accounts import ACCOUNT_DISCRIMINATOR_SIZE
+from anchorpy.error import AccountInvalidDiscriminator
+from anchorpy.utils.rpc import get_multiple_accounts
+from anchorpy.borsh_extension import BorshPubkey
+from ..program_id import PROGRAM_ID
+
+
+class GlobalConfigJSON(typing.TypedDict):
+    emergency_mode: int
+    local_admins_blocked: int
+    new_orders_blocked: int
+    orders_matching_blocked: int
+    host_fee_bps: int
+    padding0: list[int]
+    padding1: list[int]
+    pda_authority_previous_lamports_balance: int
+    total_tip_amount: int
+    host_tip_amount: int
+    pda_authority: str
+    pda_authority_bump: int
+    admin_authority: str
+    padding2: list[int]
+
+
+@dataclass
+class GlobalConfig:
+    discriminator: typing.ClassVar = b"\x95\x08\x9c\xca\xa0\xfc\xb0\xd9"
+    layout: typing.ClassVar = borsh.CStruct(
+        "emergency_mode" / borsh.U8,
+        "local_admins_blocked" / borsh.U8,
+        "new_orders_blocked" / borsh.U8,
+        "orders_matching_blocked" / borsh.U8,
+        "host_fee_bps" / borsh.U16,
+        "padding0" / borsh.U8[2],
+        "padding1" / borsh.U64[10],
+        "pda_authority_previous_lamports_balance" / borsh.U64,
+        "total_tip_amount" / borsh.U64,
+        "host_tip_amount" / borsh.U64,
+        "pda_authority" / BorshPubkey,
+        "pda_authority_bump" / borsh.U64,
+        "admin_authority" / BorshPubkey,
+        "padding2" / borsh.U64[247],
+    )
+    emergency_mode: int
+    local_admins_blocked: int
+    new_orders_blocked: int
+    orders_matching_blocked: int
+    host_fee_bps: int
+    padding0: list[int]
+    padding1: list[int]
+    pda_authority_previous_lamports_balance: int
+    total_tip_amount: int
+    host_tip_amount: int
+    pda_authority: Pubkey
+    pda_authority_bump: int
+    admin_authority: Pubkey
+    padding2: list[int]
+
+    @classmethod
+    async def fetch(
+        cls,
+        conn: AsyncClient,
+        address: Pubkey,
+        commitment: typing.Optional[Commitment] = None,
+        program_id: Pubkey = PROGRAM_ID,
+    ) -> typing.Optional["GlobalConfig"]:
+        resp = await conn.get_account_info(address, commitment=commitment)
+        info = resp.value
+        if info is None:
+            return None
+        if info.owner != program_id:
+            raise ValueError("Account does not belong to this program")
+        bytes_data = info.data
+        return cls.decode(bytes_data)
+
+    @classmethod
+    async def fetch_multiple(
+        cls,
+        conn: AsyncClient,
+        addresses: list[Pubkey],
+        commitment: typing.Optional[Commitment] = None,
+        program_id: Pubkey = PROGRAM_ID,
+    ) -> typing.List[typing.Optional["GlobalConfig"]]:
+        infos = await get_multiple_accounts(conn, addresses, commitment=commitment)
+        res: typing.List[typing.Optional["GlobalConfig"]] = []
+        for info in infos:
+            if info is None:
+                res.append(None)
+                continue
+            if info.account.owner != program_id:
+                raise ValueError("Account does not belong to this program")
+            res.append(cls.decode(info.account.data))
+        return res
+
+    @classmethod
+    def decode(cls, data: bytes) -> "GlobalConfig":
+        if data[:ACCOUNT_DISCRIMINATOR_SIZE] != cls.discriminator:
+            raise AccountInvalidDiscriminator(
+                "The discriminator for this account is invalid"
+            )
+        dec = GlobalConfig.layout.parse(data[ACCOUNT_DISCRIMINATOR_SIZE:])
+        return cls(
+            emergency_mode=dec.emergency_mode,
+            local_admins_blocked=dec.local_admins_blocked,
+            new_orders_blocked=dec.new_orders_blocked,
+            orders_matching_blocked=dec.orders_matching_blocked,
+            host_fee_bps=dec.host_fee_bps,
+            padding0=dec.padding0,
+            padding1=dec.padding1,
+            pda_authority_previous_lamports_balance=dec.pda_authority_previous_lamports_balance,
+            total_tip_amount=dec.total_tip_amount,
+            host_tip_amount=dec.host_tip_amount,
+            pda_authority=dec.pda_authority,
+            pda_authority_bump=dec.pda_authority_bump,
+            admin_authority=dec.admin_authority,
+            padding2=dec.padding2,
+        )
+
+    def to_json(self) -> GlobalConfigJSON:
+        return {
+            "emergency_mode": self.emergency_mode,
+            "local_admins_blocked": self.local_admins_blocked,
+            "new_orders_blocked": self.new_orders_blocked,
+            "orders_matching_blocked": self.orders_matching_blocked,
+            "host_fee_bps": self.host_fee_bps,
+            "padding0": self.padding0,
+            "padding1": self.padding1,
+            "pda_authority_previous_lamports_balance": self.pda_authority_previous_lamports_balance,
+            "total_tip_amount": self.total_tip_amount,
+            "host_tip_amount": self.host_tip_amount,
+            "pda_authority": str(self.pda_authority),
+            "pda_authority_bump": self.pda_authority_bump,
+            "admin_authority": str(self.admin_authority),
+            "padding2": self.padding2,
+        }
+
+    @classmethod
+    def from_json(cls, obj: GlobalConfigJSON) -> "GlobalConfig":
+        return cls(
+            emergency_mode=obj["emergency_mode"],
+            local_admins_blocked=obj["local_admins_blocked"],
+            new_orders_blocked=obj["new_orders_blocked"],
+            orders_matching_blocked=obj["orders_matching_blocked"],
+            host_fee_bps=obj["host_fee_bps"],
+            padding0=obj["padding0"],
+            padding1=obj["padding1"],
+            pda_authority_previous_lamports_balance=obj[
+                "pda_authority_previous_lamports_balance"
+            ],
+            total_tip_amount=obj["total_tip_amount"],
+            host_tip_amount=obj["host_tip_amount"],
+            pda_authority=Pubkey.from_string(obj["pda_authority"]),
+            pda_authority_bump=obj["pda_authority_bump"],
+            admin_authority=Pubkey.from_string(obj["admin_authority"]),
+            padding2=obj["padding2"],
+        )

+ 178 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/accounts/order.py

@@ -0,0 +1,178 @@
+import typing
+from dataclasses import dataclass
+from solders.pubkey import Pubkey
+from solana.rpc.async_api import AsyncClient
+from solana.rpc.commitment import Commitment
+import borsh_construct as borsh
+from anchorpy.coder.accounts import ACCOUNT_DISCRIMINATOR_SIZE
+from anchorpy.error import AccountInvalidDiscriminator
+from anchorpy.utils.rpc import get_multiple_accounts
+from anchorpy.borsh_extension import BorshPubkey
+from ..program_id import PROGRAM_ID
+
+
+class OrderJSON(typing.TypedDict):
+    global_config: str
+    maker: str
+    input_mint: str
+    input_mint_program_id: str
+    output_mint: str
+    output_mint_program_id: str
+    initial_input_amount: int
+    expected_output_amount: int
+    remaining_input_amount: int
+    filled_output_amount: int
+    tip_amount: int
+    number_of_fills: int
+    order_type: int
+    status: int
+    in_vault_bump: int
+    padding0: list[int]
+    padding: list[int]
+
+
+@dataclass
+class Order:
+    discriminator: typing.ClassVar = b"\x86\xad\xdf\xb9MV\x1c3"
+    layout: typing.ClassVar = borsh.CStruct(
+        "global_config" / BorshPubkey,
+        "maker" / BorshPubkey,
+        "input_mint" / BorshPubkey,
+        "input_mint_program_id" / BorshPubkey,
+        "output_mint" / BorshPubkey,
+        "output_mint_program_id" / BorshPubkey,
+        "initial_input_amount" / borsh.U64,
+        "expected_output_amount" / borsh.U64,
+        "remaining_input_amount" / borsh.U64,
+        "filled_output_amount" / borsh.U64,
+        "tip_amount" / borsh.U64,
+        "number_of_fills" / borsh.U64,
+        "order_type" / borsh.U8,
+        "status" / borsh.U8,
+        "in_vault_bump" / borsh.U8,
+        "padding0" / borsh.U8[5],
+        "padding" / borsh.U64[21],
+    )
+    global_config: Pubkey
+    maker: Pubkey
+    input_mint: Pubkey
+    input_mint_program_id: Pubkey
+    output_mint: Pubkey
+    output_mint_program_id: Pubkey
+    initial_input_amount: int
+    expected_output_amount: int
+    remaining_input_amount: int
+    filled_output_amount: int
+    tip_amount: int
+    number_of_fills: int
+    order_type: int
+    status: int
+    in_vault_bump: int
+    padding0: list[int]
+    padding: list[int]
+
+    @classmethod
+    async def fetch(
+        cls,
+        conn: AsyncClient,
+        address: Pubkey,
+        commitment: typing.Optional[Commitment] = None,
+        program_id: Pubkey = PROGRAM_ID,
+    ) -> typing.Optional["Order"]:
+        resp = await conn.get_account_info(address, commitment=commitment)
+        info = resp.value
+        if info is None:
+            return None
+        if info.owner != program_id:
+            raise ValueError("Account does not belong to this program")
+        bytes_data = info.data
+        return cls.decode(bytes_data)
+
+    @classmethod
+    async def fetch_multiple(
+        cls,
+        conn: AsyncClient,
+        addresses: list[Pubkey],
+        commitment: typing.Optional[Commitment] = None,
+        program_id: Pubkey = PROGRAM_ID,
+    ) -> typing.List[typing.Optional["Order"]]:
+        infos = await get_multiple_accounts(conn, addresses, commitment=commitment)
+        res: typing.List[typing.Optional["Order"]] = []
+        for info in infos:
+            if info is None:
+                res.append(None)
+                continue
+            if info.account.owner != program_id:
+                raise ValueError("Account does not belong to this program")
+            res.append(cls.decode(info.account.data))
+        return res
+
+    @classmethod
+    def decode(cls, data: bytes) -> "Order":
+        if data[:ACCOUNT_DISCRIMINATOR_SIZE] != cls.discriminator:
+            raise AccountInvalidDiscriminator(
+                "The discriminator for this account is invalid"
+            )
+        dec = Order.layout.parse(data[ACCOUNT_DISCRIMINATOR_SIZE:])
+        return cls(
+            global_config=dec.global_config,
+            maker=dec.maker,
+            input_mint=dec.input_mint,
+            input_mint_program_id=dec.input_mint_program_id,
+            output_mint=dec.output_mint,
+            output_mint_program_id=dec.output_mint_program_id,
+            initial_input_amount=dec.initial_input_amount,
+            expected_output_amount=dec.expected_output_amount,
+            remaining_input_amount=dec.remaining_input_amount,
+            filled_output_amount=dec.filled_output_amount,
+            tip_amount=dec.tip_amount,
+            number_of_fills=dec.number_of_fills,
+            order_type=dec.order_type,
+            status=dec.status,
+            in_vault_bump=dec.in_vault_bump,
+            padding0=dec.padding0,
+            padding=dec.padding,
+        )
+
+    def to_json(self) -> OrderJSON:
+        return {
+            "global_config": str(self.global_config),
+            "maker": str(self.maker),
+            "input_mint": str(self.input_mint),
+            "input_mint_program_id": str(self.input_mint_program_id),
+            "output_mint": str(self.output_mint),
+            "output_mint_program_id": str(self.output_mint_program_id),
+            "initial_input_amount": self.initial_input_amount,
+            "expected_output_amount": self.expected_output_amount,
+            "remaining_input_amount": self.remaining_input_amount,
+            "filled_output_amount": self.filled_output_amount,
+            "tip_amount": self.tip_amount,
+            "number_of_fills": self.number_of_fills,
+            "order_type": self.order_type,
+            "status": self.status,
+            "in_vault_bump": self.in_vault_bump,
+            "padding0": self.padding0,
+            "padding": self.padding,
+        }
+
+    @classmethod
+    def from_json(cls, obj: OrderJSON) -> "Order":
+        return cls(
+            global_config=Pubkey.from_string(obj["global_config"]),
+            maker=Pubkey.from_string(obj["maker"]),
+            input_mint=Pubkey.from_string(obj["input_mint"]),
+            input_mint_program_id=Pubkey.from_string(obj["input_mint_program_id"]),
+            output_mint=Pubkey.from_string(obj["output_mint"]),
+            output_mint_program_id=Pubkey.from_string(obj["output_mint_program_id"]),
+            initial_input_amount=obj["initial_input_amount"],
+            expected_output_amount=obj["expected_output_amount"],
+            remaining_input_amount=obj["remaining_input_amount"],
+            filled_output_amount=obj["filled_output_amount"],
+            tip_amount=obj["tip_amount"],
+            number_of_fills=obj["number_of_fills"],
+            order_type=obj["order_type"],
+            status=obj["status"],
+            in_vault_bump=obj["in_vault_bump"],
+            padding0=obj["padding0"],
+            padding=obj["padding"],
+        )

+ 29 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/errors/__init__.py

@@ -0,0 +1,29 @@
+import typing
+import re
+from solders.transaction_status import (
+    InstructionErrorCustom,
+    TransactionErrorInstructionError,
+)
+from solana.rpc.core import RPCException
+from solders.rpc.errors import SendTransactionPreflightFailureMessage
+from anchorpy.error import extract_code_and_logs
+from ..program_id import PROGRAM_ID
+from . import anchor
+from . import custom
+
+
+def from_code(code: int) -> typing.Union[custom.CustomError, anchor.AnchorError, None]:
+    return custom.from_code(code) if code >= 6000 else anchor.from_code(code)
+
+
+error_re = re.compile(r"Program (\w+) failed: custom program error: (\w+)")
+
+
+def from_tx_error(
+    error: RPCException,
+) -> typing.Union[anchor.AnchorError, custom.CustomError, None]:
+    err_info = error.args[0]
+    extracted = extract_code_and_logs(err_info, PROGRAM_ID)
+    if extracted is None:
+        return None
+    return from_code(extracted[0])

+ 590 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/errors/anchor.py

@@ -0,0 +1,590 @@
+import typing
+from anchorpy.error import ProgramError
+
+
+class InstructionMissing(ProgramError):
+    def __init__(self):
+        super().__init__(100, "8 byte instruction identifier not provided")
+
+    code = 100
+    name = "InstructionMissing"
+    msg = "8 byte instruction identifier not provided"
+
+
+class InstructionFallbackNotFound(ProgramError):
+    def __init__(self):
+        super().__init__(101, "Fallback functions are not supported")
+
+    code = 101
+    name = "InstructionFallbackNotFound"
+    msg = "Fallback functions are not supported"
+
+
+class InstructionDidNotDeserialize(ProgramError):
+    def __init__(self):
+        super().__init__(102, "The program could not deserialize the given instruction")
+
+    code = 102
+    name = "InstructionDidNotDeserialize"
+    msg = "The program could not deserialize the given instruction"
+
+
+class InstructionDidNotSerialize(ProgramError):
+    def __init__(self):
+        super().__init__(103, "The program could not serialize the given instruction")
+
+    code = 103
+    name = "InstructionDidNotSerialize"
+    msg = "The program could not serialize the given instruction"
+
+
+class IdlInstructionStub(ProgramError):
+    def __init__(self):
+        super().__init__(1000, "The program was compiled without idl instructions")
+
+    code = 1000
+    name = "IdlInstructionStub"
+    msg = "The program was compiled without idl instructions"
+
+
+class IdlInstructionInvalidProgram(ProgramError):
+    def __init__(self):
+        super().__init__(
+            1001, "The transaction was given an invalid program for the IDL instruction"
+        )
+
+    code = 1001
+    name = "IdlInstructionInvalidProgram"
+    msg = "The transaction was given an invalid program for the IDL instruction"
+
+
+class ConstraintMut(ProgramError):
+    def __init__(self):
+        super().__init__(2000, "A mut constraint was violated")
+
+    code = 2000
+    name = "ConstraintMut"
+    msg = "A mut constraint was violated"
+
+
+class ConstraintHasOne(ProgramError):
+    def __init__(self):
+        super().__init__(2001, "A has_one constraint was violated")
+
+    code = 2001
+    name = "ConstraintHasOne"
+    msg = "A has_one constraint was violated"
+
+
+class ConstraintSigner(ProgramError):
+    def __init__(self):
+        super().__init__(2002, "A signer constraint was violated")
+
+    code = 2002
+    name = "ConstraintSigner"
+    msg = "A signer constraint was violated"
+
+
+class ConstraintRaw(ProgramError):
+    def __init__(self):
+        super().__init__(2003, "A raw constraint was violated")
+
+    code = 2003
+    name = "ConstraintRaw"
+    msg = "A raw constraint was violated"
+
+
+class ConstraintOwner(ProgramError):
+    def __init__(self):
+        super().__init__(2004, "An owner constraint was violated")
+
+    code = 2004
+    name = "ConstraintOwner"
+    msg = "An owner constraint was violated"
+
+
+class ConstraintRentExempt(ProgramError):
+    def __init__(self):
+        super().__init__(2005, "A rent exempt constraint was violated")
+
+    code = 2005
+    name = "ConstraintRentExempt"
+    msg = "A rent exempt constraint was violated"
+
+
+class ConstraintSeeds(ProgramError):
+    def __init__(self):
+        super().__init__(2006, "A seeds constraint was violated")
+
+    code = 2006
+    name = "ConstraintSeeds"
+    msg = "A seeds constraint was violated"
+
+
+class ConstraintExecutable(ProgramError):
+    def __init__(self):
+        super().__init__(2007, "An executable constraint was violated")
+
+    code = 2007
+    name = "ConstraintExecutable"
+    msg = "An executable constraint was violated"
+
+
+class ConstraintState(ProgramError):
+    def __init__(self):
+        super().__init__(2008, "A state constraint was violated")
+
+    code = 2008
+    name = "ConstraintState"
+    msg = "A state constraint was violated"
+
+
+class ConstraintAssociated(ProgramError):
+    def __init__(self):
+        super().__init__(2009, "An associated constraint was violated")
+
+    code = 2009
+    name = "ConstraintAssociated"
+    msg = "An associated constraint was violated"
+
+
+class ConstraintAssociatedInit(ProgramError):
+    def __init__(self):
+        super().__init__(2010, "An associated init constraint was violated")
+
+    code = 2010
+    name = "ConstraintAssociatedInit"
+    msg = "An associated init constraint was violated"
+
+
+class ConstraintClose(ProgramError):
+    def __init__(self):
+        super().__init__(2011, "A close constraint was violated")
+
+    code = 2011
+    name = "ConstraintClose"
+    msg = "A close constraint was violated"
+
+
+class ConstraintAddress(ProgramError):
+    def __init__(self):
+        super().__init__(2012, "An address constraint was violated")
+
+    code = 2012
+    name = "ConstraintAddress"
+    msg = "An address constraint was violated"
+
+
+class ConstraintZero(ProgramError):
+    def __init__(self):
+        super().__init__(2013, "Expected zero account discriminant")
+
+    code = 2013
+    name = "ConstraintZero"
+    msg = "Expected zero account discriminant"
+
+
+class ConstraintTokenMint(ProgramError):
+    def __init__(self):
+        super().__init__(2014, "A token mint constraint was violated")
+
+    code = 2014
+    name = "ConstraintTokenMint"
+    msg = "A token mint constraint was violated"
+
+
+class ConstraintTokenOwner(ProgramError):
+    def __init__(self):
+        super().__init__(2015, "A token owner constraint was violated")
+
+    code = 2015
+    name = "ConstraintTokenOwner"
+    msg = "A token owner constraint was violated"
+
+
+class ConstraintMintMintAuthority(ProgramError):
+    def __init__(self):
+        super().__init__(2016, "A mint mint authority constraint was violated")
+
+    code = 2016
+    name = "ConstraintMintMintAuthority"
+    msg = "A mint mint authority constraint was violated"
+
+
+class ConstraintMintFreezeAuthority(ProgramError):
+    def __init__(self):
+        super().__init__(2017, "A mint freeze authority constraint was violated")
+
+    code = 2017
+    name = "ConstraintMintFreezeAuthority"
+    msg = "A mint freeze authority constraint was violated"
+
+
+class ConstraintMintDecimals(ProgramError):
+    def __init__(self):
+        super().__init__(2018, "A mint decimals constraint was violated")
+
+    code = 2018
+    name = "ConstraintMintDecimals"
+    msg = "A mint decimals constraint was violated"
+
+
+class ConstraintSpace(ProgramError):
+    def __init__(self):
+        super().__init__(2019, "A space constraint was violated")
+
+    code = 2019
+    name = "ConstraintSpace"
+    msg = "A space constraint was violated"
+
+
+class RequireViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2500, "A require expression was violated")
+
+    code = 2500
+    name = "RequireViolated"
+    msg = "A require expression was violated"
+
+
+class RequireEqViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2501, "A require_eq expression was violated")
+
+    code = 2501
+    name = "RequireEqViolated"
+    msg = "A require_eq expression was violated"
+
+
+class RequireKeysEqViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2502, "A require_keys_eq expression was violated")
+
+    code = 2502
+    name = "RequireKeysEqViolated"
+    msg = "A require_keys_eq expression was violated"
+
+
+class RequireNeqViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2503, "A require_neq expression was violated")
+
+    code = 2503
+    name = "RequireNeqViolated"
+    msg = "A require_neq expression was violated"
+
+
+class RequireKeysNeqViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2504, "A require_keys_neq expression was violated")
+
+    code = 2504
+    name = "RequireKeysNeqViolated"
+    msg = "A require_keys_neq expression was violated"
+
+
+class RequireGtViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2505, "A require_gt expression was violated")
+
+    code = 2505
+    name = "RequireGtViolated"
+    msg = "A require_gt expression was violated"
+
+
+class RequireGteViolated(ProgramError):
+    def __init__(self):
+        super().__init__(2506, "A require_gte expression was violated")
+
+    code = 2506
+    name = "RequireGteViolated"
+    msg = "A require_gte expression was violated"
+
+
+class AccountDiscriminatorAlreadySet(ProgramError):
+    def __init__(self):
+        super().__init__(
+            3000, "The account discriminator was already set on this account"
+        )
+
+    code = 3000
+    name = "AccountDiscriminatorAlreadySet"
+    msg = "The account discriminator was already set on this account"
+
+
+class AccountDiscriminatorNotFound(ProgramError):
+    def __init__(self):
+        super().__init__(3001, "No 8 byte discriminator was found on the account")
+
+    code = 3001
+    name = "AccountDiscriminatorNotFound"
+    msg = "No 8 byte discriminator was found on the account"
+
+
+class AccountDiscriminatorMismatch(ProgramError):
+    def __init__(self):
+        super().__init__(3002, "8 byte discriminator did not match what was expected")
+
+    code = 3002
+    name = "AccountDiscriminatorMismatch"
+    msg = "8 byte discriminator did not match what was expected"
+
+
+class AccountDidNotDeserialize(ProgramError):
+    def __init__(self):
+        super().__init__(3003, "Failed to deserialize the account")
+
+    code = 3003
+    name = "AccountDidNotDeserialize"
+    msg = "Failed to deserialize the account"
+
+
+class AccountDidNotSerialize(ProgramError):
+    def __init__(self):
+        super().__init__(3004, "Failed to serialize the account")
+
+    code = 3004
+    name = "AccountDidNotSerialize"
+    msg = "Failed to serialize the account"
+
+
+class AccountNotEnoughKeys(ProgramError):
+    def __init__(self):
+        super().__init__(3005, "Not enough account keys given to the instruction")
+
+    code = 3005
+    name = "AccountNotEnoughKeys"
+    msg = "Not enough account keys given to the instruction"
+
+
+class AccountNotMutable(ProgramError):
+    def __init__(self):
+        super().__init__(3006, "The given account is not mutable")
+
+    code = 3006
+    name = "AccountNotMutable"
+    msg = "The given account is not mutable"
+
+
+class AccountOwnedByWrongProgram(ProgramError):
+    def __init__(self):
+        super().__init__(
+            3007, "The given account is owned by a different program than expected"
+        )
+
+    code = 3007
+    name = "AccountOwnedByWrongProgram"
+    msg = "The given account is owned by a different program than expected"
+
+
+class InvalidProgramId(ProgramError):
+    def __init__(self):
+        super().__init__(3008, "Program ID was not as expected")
+
+    code = 3008
+    name = "InvalidProgramId"
+    msg = "Program ID was not as expected"
+
+
+class InvalidProgramExecutable(ProgramError):
+    def __init__(self):
+        super().__init__(3009, "Program account is not executable")
+
+    code = 3009
+    name = "InvalidProgramExecutable"
+    msg = "Program account is not executable"
+
+
+class AccountNotSigner(ProgramError):
+    def __init__(self):
+        super().__init__(3010, "The given account did not sign")
+
+    code = 3010
+    name = "AccountNotSigner"
+    msg = "The given account did not sign"
+
+
+class AccountNotSystemOwned(ProgramError):
+    def __init__(self):
+        super().__init__(3011, "The given account is not owned by the system program")
+
+    code = 3011
+    name = "AccountNotSystemOwned"
+    msg = "The given account is not owned by the system program"
+
+
+class AccountNotInitialized(ProgramError):
+    def __init__(self):
+        super().__init__(
+            3012, "The program expected this account to be already initialized"
+        )
+
+    code = 3012
+    name = "AccountNotInitialized"
+    msg = "The program expected this account to be already initialized"
+
+
+class AccountNotProgramData(ProgramError):
+    def __init__(self):
+        super().__init__(3013, "The given account is not a program data account")
+
+    code = 3013
+    name = "AccountNotProgramData"
+    msg = "The given account is not a program data account"
+
+
+class AccountNotAssociatedTokenAccount(ProgramError):
+    def __init__(self):
+        super().__init__(3014, "The given account is not the associated token account")
+
+    code = 3014
+    name = "AccountNotAssociatedTokenAccount"
+    msg = "The given account is not the associated token account"
+
+
+class AccountSysvarMismatch(ProgramError):
+    def __init__(self):
+        super().__init__(
+            3015, "The given public key does not match the required sysvar"
+        )
+
+    code = 3015
+    name = "AccountSysvarMismatch"
+    msg = "The given public key does not match the required sysvar"
+
+
+class StateInvalidAddress(ProgramError):
+    def __init__(self):
+        super().__init__(
+            4000, "The given state account does not have the correct address"
+        )
+
+    code = 4000
+    name = "StateInvalidAddress"
+    msg = "The given state account does not have the correct address"
+
+
+class Deprecated(ProgramError):
+    def __init__(self):
+        super().__init__(
+            5000, "The API being used is deprecated and should no longer be used"
+        )
+
+    code = 5000
+    name = "Deprecated"
+    msg = "The API being used is deprecated and should no longer be used"
+
+
+AnchorError = typing.Union[
+    InstructionMissing,
+    InstructionFallbackNotFound,
+    InstructionDidNotDeserialize,
+    InstructionDidNotSerialize,
+    IdlInstructionStub,
+    IdlInstructionInvalidProgram,
+    ConstraintMut,
+    ConstraintHasOne,
+    ConstraintSigner,
+    ConstraintRaw,
+    ConstraintOwner,
+    ConstraintRentExempt,
+    ConstraintSeeds,
+    ConstraintExecutable,
+    ConstraintState,
+    ConstraintAssociated,
+    ConstraintAssociatedInit,
+    ConstraintClose,
+    ConstraintAddress,
+    ConstraintZero,
+    ConstraintTokenMint,
+    ConstraintTokenOwner,
+    ConstraintMintMintAuthority,
+    ConstraintMintFreezeAuthority,
+    ConstraintMintDecimals,
+    ConstraintSpace,
+    RequireViolated,
+    RequireEqViolated,
+    RequireKeysEqViolated,
+    RequireNeqViolated,
+    RequireKeysNeqViolated,
+    RequireGtViolated,
+    RequireGteViolated,
+    AccountDiscriminatorAlreadySet,
+    AccountDiscriminatorNotFound,
+    AccountDiscriminatorMismatch,
+    AccountDidNotDeserialize,
+    AccountDidNotSerialize,
+    AccountNotEnoughKeys,
+    AccountNotMutable,
+    AccountOwnedByWrongProgram,
+    InvalidProgramId,
+    InvalidProgramExecutable,
+    AccountNotSigner,
+    AccountNotSystemOwned,
+    AccountNotInitialized,
+    AccountNotProgramData,
+    AccountNotAssociatedTokenAccount,
+    AccountSysvarMismatch,
+    StateInvalidAddress,
+    Deprecated,
+]
+ANCHOR_ERROR_MAP: dict[int, AnchorError] = {
+    100: InstructionMissing(),
+    101: InstructionFallbackNotFound(),
+    102: InstructionDidNotDeserialize(),
+    103: InstructionDidNotSerialize(),
+    1000: IdlInstructionStub(),
+    1001: IdlInstructionInvalidProgram(),
+    2000: ConstraintMut(),
+    2001: ConstraintHasOne(),
+    2002: ConstraintSigner(),
+    2003: ConstraintRaw(),
+    2004: ConstraintOwner(),
+    2005: ConstraintRentExempt(),
+    2006: ConstraintSeeds(),
+    2007: ConstraintExecutable(),
+    2008: ConstraintState(),
+    2009: ConstraintAssociated(),
+    2010: ConstraintAssociatedInit(),
+    2011: ConstraintClose(),
+    2012: ConstraintAddress(),
+    2013: ConstraintZero(),
+    2014: ConstraintTokenMint(),
+    2015: ConstraintTokenOwner(),
+    2016: ConstraintMintMintAuthority(),
+    2017: ConstraintMintFreezeAuthority(),
+    2018: ConstraintMintDecimals(),
+    2019: ConstraintSpace(),
+    2500: RequireViolated(),
+    2501: RequireEqViolated(),
+    2502: RequireKeysEqViolated(),
+    2503: RequireNeqViolated(),
+    2504: RequireKeysNeqViolated(),
+    2505: RequireGtViolated(),
+    2506: RequireGteViolated(),
+    3000: AccountDiscriminatorAlreadySet(),
+    3001: AccountDiscriminatorNotFound(),
+    3002: AccountDiscriminatorMismatch(),
+    3003: AccountDidNotDeserialize(),
+    3004: AccountDidNotSerialize(),
+    3005: AccountNotEnoughKeys(),
+    3006: AccountNotMutable(),
+    3007: AccountOwnedByWrongProgram(),
+    3008: InvalidProgramId(),
+    3009: InvalidProgramExecutable(),
+    3010: AccountNotSigner(),
+    3011: AccountNotSystemOwned(),
+    3012: AccountNotInitialized(),
+    3013: AccountNotProgramData(),
+    3014: AccountNotAssociatedTokenAccount(),
+    3015: AccountSysvarMismatch(),
+    4000: StateInvalidAddress(),
+    5000: Deprecated(),
+}
+
+
+def from_code(code: int) -> typing.Optional[AnchorError]:
+    maybe_err = ANCHOR_ERROR_MAP.get(code)
+    if maybe_err is None:
+        return None
+    return maybe_err

+ 191 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/errors/custom.py

@@ -0,0 +1,191 @@
+import typing
+from anchorpy.error import ProgramError
+
+
+class OrderCanNotBeCanceled(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6000, "Order can't be canceled")
+
+    code = 6000
+    name = "OrderCanNotBeCanceled"
+    msg = "Order can't be canceled"
+
+
+class OrderNotActive(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6001, "Order not active")
+
+    code = 6001
+    name = "OrderNotActive"
+    msg = "Order not active"
+
+
+class InvalidAdminAuthority(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6002, "Invalid admin authority")
+
+    code = 6002
+    name = "InvalidAdminAuthority"
+    msg = "Invalid admin authority"
+
+
+class InvalidPdaAuthority(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6003, "Invalid pda authority")
+
+    code = 6003
+    name = "InvalidPdaAuthority"
+    msg = "Invalid pda authority"
+
+
+class InvalidConfigOption(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6004, "Invalid config option")
+
+    code = 6004
+    name = "InvalidConfigOption"
+    msg = "Invalid config option"
+
+
+class InvalidOrderOwner(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6005, "Order owner account is not the order owner")
+
+    code = 6005
+    name = "InvalidOrderOwner"
+    msg = "Order owner account is not the order owner"
+
+
+class OutOfRangeIntegralConversion(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6006, "Out of range integral conversion attempted")
+
+    code = 6006
+    name = "OutOfRangeIntegralConversion"
+    msg = "Out of range integral conversion attempted"
+
+
+class InvalidFlag(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6007, "Invalid boolean flag, valid values are 0 and 1")
+
+    code = 6007
+    name = "InvalidFlag"
+    msg = "Invalid boolean flag, valid values are 0 and 1"
+
+
+class MathOverflow(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6008, "Mathematical operation with overflow")
+
+    code = 6008
+    name = "MathOverflow"
+    msg = "Mathematical operation with overflow"
+
+
+class OrderInputAmountInvalid(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6009, "Order input amount invalid")
+
+    code = 6009
+    name = "OrderInputAmountInvalid"
+    msg = "Order input amount invalid"
+
+
+class OrderOutputAmountInvalid(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6010, "Order input amount invalid")
+
+    code = 6010
+    name = "OrderOutputAmountInvalid"
+    msg = "Order input amount invalid"
+
+
+class InvalidHostFee(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6011, "Host fee bps must be between 0 and 10000")
+
+    code = 6011
+    name = "InvalidHostFee"
+    msg = "Host fee bps must be between 0 and 10000"
+
+
+class IntegerOverflow(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6012, "Conversion between integers failed")
+
+    code = 6012
+    name = "IntegerOverflow"
+    msg = "Conversion between integers failed"
+
+
+class InvalidTipBalance(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6013, "Tip balance less than accounted tip")
+
+    code = 6013
+    name = "InvalidTipBalance"
+    msg = "Tip balance less than accounted tip"
+
+
+class InvalidTipTransferAmount(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6014, "Tip transfer amount is less than expected")
+
+    code = 6014
+    name = "InvalidTipTransferAmount"
+    msg = "Tip transfer amount is less than expected"
+
+
+class InvalidHostTipBalance(ProgramError):
+    def __init__(self) -> None:
+        super().__init__(6015, "Host tup amount is less than accounted for")
+
+    code = 6015
+    name = "InvalidHostTipBalance"
+    msg = "Host tup amount is less than accounted for"
+
+
+CustomError = typing.Union[
+    OrderCanNotBeCanceled,
+    OrderNotActive,
+    InvalidAdminAuthority,
+    InvalidPdaAuthority,
+    InvalidConfigOption,
+    InvalidOrderOwner,
+    OutOfRangeIntegralConversion,
+    InvalidFlag,
+    MathOverflow,
+    OrderInputAmountInvalid,
+    OrderOutputAmountInvalid,
+    InvalidHostFee,
+    IntegerOverflow,
+    InvalidTipBalance,
+    InvalidTipTransferAmount,
+    InvalidHostTipBalance,
+]
+CUSTOM_ERROR_MAP: dict[int, CustomError] = {
+    6000: OrderCanNotBeCanceled(),
+    6001: OrderNotActive(),
+    6002: InvalidAdminAuthority(),
+    6003: InvalidPdaAuthority(),
+    6004: InvalidConfigOption(),
+    6005: InvalidOrderOwner(),
+    6006: OutOfRangeIntegralConversion(),
+    6007: InvalidFlag(),
+    6008: MathOverflow(),
+    6009: OrderInputAmountInvalid(),
+    6010: OrderOutputAmountInvalid(),
+    6011: InvalidHostFee(),
+    6012: IntegerOverflow(),
+    6013: InvalidTipBalance(),
+    6014: InvalidTipTransferAmount(),
+    6015: InvalidHostTipBalance(),
+}
+
+
+def from_code(code: int) -> typing.Optional[CustomError]:
+    maybe_err = CUSTOM_ERROR_MAP.get(code)
+    if maybe_err is None:
+        return None
+    return maybe_err

+ 17 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/instructions/__init__.py

@@ -0,0 +1,17 @@
+from .initialize_global_config import (
+    initialize_global_config,
+    InitializeGlobalConfigAccounts,
+)
+from .initialize_vault import initialize_vault, InitializeVaultAccounts
+from .create_order import create_order, CreateOrderArgs, CreateOrderAccounts
+from .close_order_and_claim_tip import (
+    close_order_and_claim_tip,
+    CloseOrderAndClaimTipAccounts,
+)
+from .take_order import take_order, TakeOrderArgs, TakeOrderAccounts
+from .update_global_config import (
+    update_global_config,
+    UpdateGlobalConfigArgs,
+    UpdateGlobalConfigAccounts,
+)
+from .withdraw_host_tip import withdraw_host_tip, WithdrawHostTipAccounts

+ 49 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/instructions/close_order_and_claim_tip.py

@@ -0,0 +1,49 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.system_program import ID as SYS_PROGRAM_ID
+from solders.instruction import Instruction, AccountMeta
+from ..program_id import PROGRAM_ID
+
+
+class CloseOrderAndClaimTipAccounts(typing.TypedDict):
+    maker: Pubkey
+    order: Pubkey
+    global_config: Pubkey
+    pda_authority: Pubkey
+    input_mint: Pubkey
+    maker_input_ata: Pubkey
+    input_vault: Pubkey
+    input_token_program: Pubkey
+
+
+def close_order_and_claim_tip(
+    accounts: CloseOrderAndClaimTipAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(pubkey=accounts["maker"], is_signer=True, is_writable=True),
+        AccountMeta(pubkey=accounts["order"], is_signer=False, is_writable=True),
+        AccountMeta(
+            pubkey=accounts["global_config"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["pda_authority"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(pubkey=accounts["input_mint"], is_signer=False, is_writable=False),
+        AccountMeta(
+            pubkey=accounts["maker_input_ata"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(pubkey=accounts["input_vault"], is_signer=False, is_writable=True),
+        AccountMeta(
+            pubkey=accounts["input_token_program"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"\xf4\x1b\x0c\xe2-\xf7\xe6+"
+    encoded_args = b""
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 70 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/instructions/create_order.py

@@ -0,0 +1,70 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.instruction import Instruction, AccountMeta
+import borsh_construct as borsh
+from ..program_id import PROGRAM_ID
+
+
+class CreateOrderArgs(typing.TypedDict):
+    input_amount: int
+    output_amount: int
+    order_type: int
+
+
+layout = borsh.CStruct(
+    "input_amount" / borsh.U64, "output_amount" / borsh.U64, "order_type" / borsh.U8
+)
+
+
+class CreateOrderAccounts(typing.TypedDict):
+    maker: Pubkey
+    global_config: Pubkey
+    pda_authority: Pubkey
+    order: Pubkey
+    input_mint: Pubkey
+    output_mint: Pubkey
+    maker_ata: Pubkey
+    input_vault: Pubkey
+    input_token_program: Pubkey
+    output_token_program: Pubkey
+
+
+def create_order(
+    args: CreateOrderArgs,
+    accounts: CreateOrderAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(pubkey=accounts["maker"], is_signer=True, is_writable=True),
+        AccountMeta(
+            pubkey=accounts["global_config"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(
+            pubkey=accounts["pda_authority"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(pubkey=accounts["order"], is_signer=False, is_writable=True),
+        AccountMeta(pubkey=accounts["input_mint"], is_signer=False, is_writable=False),
+        AccountMeta(pubkey=accounts["output_mint"], is_signer=False, is_writable=False),
+        AccountMeta(pubkey=accounts["maker_ata"], is_signer=False, is_writable=True),
+        AccountMeta(pubkey=accounts["input_vault"], is_signer=False, is_writable=True),
+        AccountMeta(
+            pubkey=accounts["input_token_program"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(
+            pubkey=accounts["output_token_program"], is_signer=False, is_writable=False
+        ),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"\x8d6%\xcf\xed\xd2\xfa\xd7"
+    encoded_args = layout.build(
+        {
+            "input_amount": args["input_amount"],
+            "output_amount": args["output_amount"],
+            "order_type": args["order_type"],
+        }
+    )
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 35 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/instructions/initialize_global_config.py

@@ -0,0 +1,35 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.instruction import Instruction, AccountMeta
+from ..program_id import PROGRAM_ID
+
+
+class InitializeGlobalConfigAccounts(typing.TypedDict):
+    admin_authority: Pubkey
+    pda_authority: Pubkey
+    global_config: Pubkey
+
+
+def initialize_global_config(
+    accounts: InitializeGlobalConfigAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(
+            pubkey=accounts["admin_authority"], is_signer=True, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["pda_authority"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["global_config"], is_signer=False, is_writable=True
+        ),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"q\xd8z\x83\xe1\xd1\x167"
+    encoded_args = b""
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 45 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/instructions/initialize_vault.py

@@ -0,0 +1,45 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.system_program import ID as SYS_PROGRAM_ID
+from solders.sysvar import RENT
+from spl.token.constants import TOKEN_PROGRAM_ID
+from solders.instruction import Instruction, AccountMeta
+from ..program_id import PROGRAM_ID
+
+
+class InitializeVaultAccounts(typing.TypedDict):
+    admin_authority: Pubkey
+    global_config: Pubkey
+    pda_authority: Pubkey
+    mint: Pubkey
+    vault: Pubkey
+
+
+def initialize_vault(
+    accounts: InitializeVaultAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(
+            pubkey=accounts["admin_authority"], is_signer=True, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["global_config"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["pda_authority"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=False),
+        AccountMeta(pubkey=accounts["vault"], is_signer=False, is_writable=True),
+        AccountMeta(pubkey=RENT, is_signer=False, is_writable=False),
+        AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False),
+        AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"0\xbf\xa3,G\x81?\xa4"
+    encoded_args = b""
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 96 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/instructions/take_order.py

@@ -0,0 +1,96 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.instruction import Instruction, AccountMeta
+import borsh_construct as borsh
+from ..program_id import PROGRAM_ID
+
+
+class TakeOrderArgs(typing.TypedDict):
+    input_amount: int
+
+
+layout = borsh.CStruct("input_amount" / borsh.U64)
+
+
+class TakeOrderAccounts(typing.TypedDict):
+    taker: Pubkey
+    maker: Pubkey
+    global_config: Pubkey
+    pda_authority: Pubkey
+    order: Pubkey
+    input_mint: Pubkey
+    output_mint: Pubkey
+    input_vault: Pubkey
+    taker_input_ata: Pubkey
+    taker_output_ata: Pubkey
+    maker_output_ata: Pubkey
+    express_relay: Pubkey
+    express_relay_metadata: Pubkey
+    sysvar_instructions: Pubkey
+    permission: Pubkey
+    config_router: Pubkey
+    input_token_program: Pubkey
+    output_token_program: Pubkey
+
+
+def take_order(
+    args: TakeOrderArgs,
+    accounts: TakeOrderAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(pubkey=accounts["taker"], is_signer=True, is_writable=True),
+        AccountMeta(pubkey=accounts["maker"], is_signer=False, is_writable=False),
+        AccountMeta(
+            pubkey=accounts["global_config"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["pda_authority"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(pubkey=accounts["order"], is_signer=False, is_writable=True),
+        AccountMeta(pubkey=accounts["input_mint"], is_signer=False, is_writable=False),
+        AccountMeta(pubkey=accounts["output_mint"], is_signer=False, is_writable=False),
+        AccountMeta(pubkey=accounts["input_vault"], is_signer=False, is_writable=True),
+        AccountMeta(
+            pubkey=accounts["taker_input_ata"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["taker_output_ata"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["maker_output_ata"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["express_relay"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(
+            pubkey=accounts["express_relay_metadata"],
+            is_signer=False,
+            is_writable=False,
+        ),
+        AccountMeta(
+            pubkey=accounts["sysvar_instructions"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(pubkey=accounts["permission"], is_signer=False, is_writable=False),
+        AccountMeta(
+            pubkey=accounts["config_router"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(
+            pubkey=accounts["input_token_program"], is_signer=False, is_writable=False
+        ),
+        AccountMeta(
+            pubkey=accounts["output_token_program"], is_signer=False, is_writable=False
+        ),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"\xa3\xd0\x14\xac\xdfA\xff\xe4"
+    encoded_args = layout.build(
+        {
+            "input_amount": args["input_amount"],
+        }
+    )
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 48 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/instructions/update_global_config.py

@@ -0,0 +1,48 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.system_program import ID as SYS_PROGRAM_ID
+from solders.instruction import Instruction, AccountMeta
+import borsh_construct as borsh
+from ..program_id import PROGRAM_ID
+
+
+class UpdateGlobalConfigArgs(typing.TypedDict):
+    mode: int
+    value: list[int]
+
+
+layout = borsh.CStruct("mode" / borsh.U16, "value" / borsh.U8[128])
+
+
+class UpdateGlobalConfigAccounts(typing.TypedDict):
+    admin_authority: Pubkey
+    global_config: Pubkey
+
+
+def update_global_config(
+    args: UpdateGlobalConfigArgs,
+    accounts: UpdateGlobalConfigAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(
+            pubkey=accounts["admin_authority"], is_signer=True, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["global_config"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"\xa4T\x82\xbdo:\xfa\xc8"
+    encoded_args = layout.build(
+        {
+            "mode": args["mode"],
+            "value": args["value"],
+        }
+    )
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 37 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/instructions/withdraw_host_tip.py

@@ -0,0 +1,37 @@
+from __future__ import annotations
+import typing
+from solders.pubkey import Pubkey
+from solders.system_program import ID as SYS_PROGRAM_ID
+from solders.instruction import Instruction, AccountMeta
+from ..program_id import PROGRAM_ID
+
+
+class WithdrawHostTipAccounts(typing.TypedDict):
+    admin_authority: Pubkey
+    global_config: Pubkey
+    pda_authority: Pubkey
+
+
+def withdraw_host_tip(
+    accounts: WithdrawHostTipAccounts,
+    program_id: Pubkey = PROGRAM_ID,
+    remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None,
+) -> Instruction:
+    keys: list[AccountMeta] = [
+        AccountMeta(
+            pubkey=accounts["admin_authority"], is_signer=True, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["global_config"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(
+            pubkey=accounts["pda_authority"], is_signer=False, is_writable=True
+        ),
+        AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
+    ]
+    if remaining_accounts is not None:
+        keys += remaining_accounts
+    identifier = b"\x8c\xf6i\xa5PU\x8f\x12"
+    encoded_args = b""
+    data = identifier + encoded_args
+    return Instruction(program_id, data, keys)

+ 3 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/program_id.py

@@ -0,0 +1,3 @@
+from solders.pubkey import Pubkey
+
+PROGRAM_ID = Pubkey.from_string("LiMoM9rMhrdYrfzUCxQppvxCSG1FcrUK9G8uLq4A1GF")

+ 15 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/types/__init__.py

@@ -0,0 +1,15 @@
+import typing
+from . import order_status
+from .order_status import OrderStatusKind, OrderStatusJSON
+from . import order_type
+from .order_type import OrderTypeKind, OrderTypeJSON
+from . import update_global_config_mode
+from .update_global_config_mode import (
+    UpdateGlobalConfigModeKind,
+    UpdateGlobalConfigModeJSON,
+)
+from . import update_global_config_value
+from .update_global_config_value import (
+    UpdateGlobalConfigValueKind,
+    UpdateGlobalConfigValueJSON,
+)

+ 105 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/types/order_status.py

@@ -0,0 +1,105 @@
+from __future__ import annotations
+import typing
+from dataclasses import dataclass
+from anchorpy.borsh_extension import EnumForCodegen
+import borsh_construct as borsh
+
+
+class ActiveJSON(typing.TypedDict):
+    kind: typing.Literal["Active"]
+
+
+class FilledJSON(typing.TypedDict):
+    kind: typing.Literal["Filled"]
+
+
+class CancelledJSON(typing.TypedDict):
+    kind: typing.Literal["Cancelled"]
+
+
+@dataclass
+class Active:
+    discriminator: typing.ClassVar = 0
+    kind: typing.ClassVar = "Active"
+
+    @classmethod
+    def to_json(cls) -> ActiveJSON:
+        return ActiveJSON(
+            kind="Active",
+        )
+
+    @classmethod
+    def to_encodable(cls) -> dict:
+        return {
+            "Active": {},
+        }
+
+
+@dataclass
+class Filled:
+    discriminator: typing.ClassVar = 1
+    kind: typing.ClassVar = "Filled"
+
+    @classmethod
+    def to_json(cls) -> FilledJSON:
+        return FilledJSON(
+            kind="Filled",
+        )
+
+    @classmethod
+    def to_encodable(cls) -> dict:
+        return {
+            "Filled": {},
+        }
+
+
+@dataclass
+class Cancelled:
+    discriminator: typing.ClassVar = 2
+    kind: typing.ClassVar = "Cancelled"
+
+    @classmethod
+    def to_json(cls) -> CancelledJSON:
+        return CancelledJSON(
+            kind="Cancelled",
+        )
+
+    @classmethod
+    def to_encodable(cls) -> dict:
+        return {
+            "Cancelled": {},
+        }
+
+
+OrderStatusKind = typing.Union[Active, Filled, Cancelled]
+OrderStatusJSON = typing.Union[ActiveJSON, FilledJSON, CancelledJSON]
+
+
+def from_decoded(obj: dict) -> OrderStatusKind:
+    if not isinstance(obj, dict):
+        raise ValueError("Invalid enum object")
+    if "Active" in obj:
+        return Active()
+    if "Filled" in obj:
+        return Filled()
+    if "Cancelled" in obj:
+        return Cancelled()
+    raise ValueError("Invalid enum object")
+
+
+def from_json(obj: OrderStatusJSON) -> OrderStatusKind:
+    if obj["kind"] == "Active":
+        return Active()
+    if obj["kind"] == "Filled":
+        return Filled()
+    if obj["kind"] == "Cancelled":
+        return Cancelled()
+    kind = obj["kind"]
+    raise ValueError(f"Unrecognized enum kind: {kind}")
+
+
+layout = EnumForCodegen(
+    "Active" / borsh.CStruct(),
+    "Filled" / borsh.CStruct(),
+    "Cancelled" / borsh.CStruct(),
+)

+ 75 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/types/order_type.py

@@ -0,0 +1,75 @@
+from __future__ import annotations
+import typing
+from dataclasses import dataclass
+from anchorpy.borsh_extension import EnumForCodegen
+import borsh_construct as borsh
+
+
+class ExactInJSON(typing.TypedDict):
+    kind: typing.Literal["ExactIn"]
+
+
+class ExactOutJSON(typing.TypedDict):
+    kind: typing.Literal["ExactOut"]
+
+
+@dataclass
+class ExactIn:
+    discriminator: typing.ClassVar = 0
+    kind: typing.ClassVar = "ExactIn"
+
+    @classmethod
+    def to_json(cls) -> ExactInJSON:
+        return ExactInJSON(
+            kind="ExactIn",
+        )
+
+    @classmethod
+    def to_encodable(cls) -> dict:
+        return {
+            "ExactIn": {},
+        }
+
+
+@dataclass
+class ExactOut:
+    discriminator: typing.ClassVar = 1
+    kind: typing.ClassVar = "ExactOut"
+
+    @classmethod
+    def to_json(cls) -> ExactOutJSON:
+        return ExactOutJSON(
+            kind="ExactOut",
+        )
+
+    @classmethod
+    def to_encodable(cls) -> dict:
+        return {
+            "ExactOut": {},
+        }
+
+
+OrderTypeKind = typing.Union[ExactIn, ExactOut]
+OrderTypeJSON = typing.Union[ExactInJSON, ExactOutJSON]
+
+
+def from_decoded(obj: dict) -> OrderTypeKind:
+    if not isinstance(obj, dict):
+        raise ValueError("Invalid enum object")
+    if "ExactIn" in obj:
+        return ExactIn()
+    if "ExactOut" in obj:
+        return ExactOut()
+    raise ValueError("Invalid enum object")
+
+
+def from_json(obj: OrderTypeJSON) -> OrderTypeKind:
+    if obj["kind"] == "ExactIn":
+        return ExactIn()
+    if obj["kind"] == "ExactOut":
+        return ExactOut()
+    kind = obj["kind"]
+    raise ValueError(f"Unrecognized enum kind: {kind}")
+
+
+layout = EnumForCodegen("ExactIn" / borsh.CStruct(), "ExactOut" / borsh.CStruct())

+ 200 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/types/update_global_config_mode.py

@@ -0,0 +1,200 @@
+from __future__ import annotations
+import typing
+from dataclasses import dataclass
+from anchorpy.borsh_extension import EnumForCodegen
+import borsh_construct as borsh
+
+
+class UpdateEmergencyModeJSON(typing.TypedDict):
+    kind: typing.Literal["UpdateEmergencyMode"]
+
+
+class UpdateBlockLocalAdminsJSON(typing.TypedDict):
+    kind: typing.Literal["UpdateBlockLocalAdmins"]
+
+
+class UpdateBlockNewOrdersJSON(typing.TypedDict):
+    kind: typing.Literal["UpdateBlockNewOrders"]
+
+
+class UpdateBlockOrdersTakingJSON(typing.TypedDict):
+    kind: typing.Literal["UpdateBlockOrdersTaking"]
+
+
+class UpdateHostFeeBpsJSON(typing.TypedDict):
+    kind: typing.Literal["UpdateHostFeeBps"]
+
+
+class UpdateAdminAuthorityJSON(typing.TypedDict):
+    kind: typing.Literal["UpdateAdminAuthority"]
+
+
+@dataclass
+class UpdateEmergencyMode:
+    discriminator: typing.ClassVar = 0
+    kind: typing.ClassVar = "UpdateEmergencyMode"
+
+    @classmethod
+    def to_json(cls) -> UpdateEmergencyModeJSON:
+        return UpdateEmergencyModeJSON(
+            kind="UpdateEmergencyMode",
+        )
+
+    @classmethod
+    def to_encodable(cls) -> dict:
+        return {
+            "UpdateEmergencyMode": {},
+        }
+
+
+@dataclass
+class UpdateBlockLocalAdmins:
+    discriminator: typing.ClassVar = 1
+    kind: typing.ClassVar = "UpdateBlockLocalAdmins"
+
+    @classmethod
+    def to_json(cls) -> UpdateBlockLocalAdminsJSON:
+        return UpdateBlockLocalAdminsJSON(
+            kind="UpdateBlockLocalAdmins",
+        )
+
+    @classmethod
+    def to_encodable(cls) -> dict:
+        return {
+            "UpdateBlockLocalAdmins": {},
+        }
+
+
+@dataclass
+class UpdateBlockNewOrders:
+    discriminator: typing.ClassVar = 2
+    kind: typing.ClassVar = "UpdateBlockNewOrders"
+
+    @classmethod
+    def to_json(cls) -> UpdateBlockNewOrdersJSON:
+        return UpdateBlockNewOrdersJSON(
+            kind="UpdateBlockNewOrders",
+        )
+
+    @classmethod
+    def to_encodable(cls) -> dict:
+        return {
+            "UpdateBlockNewOrders": {},
+        }
+
+
+@dataclass
+class UpdateBlockOrdersTaking:
+    discriminator: typing.ClassVar = 3
+    kind: typing.ClassVar = "UpdateBlockOrdersTaking"
+
+    @classmethod
+    def to_json(cls) -> UpdateBlockOrdersTakingJSON:
+        return UpdateBlockOrdersTakingJSON(
+            kind="UpdateBlockOrdersTaking",
+        )
+
+    @classmethod
+    def to_encodable(cls) -> dict:
+        return {
+            "UpdateBlockOrdersTaking": {},
+        }
+
+
+@dataclass
+class UpdateHostFeeBps:
+    discriminator: typing.ClassVar = 4
+    kind: typing.ClassVar = "UpdateHostFeeBps"
+
+    @classmethod
+    def to_json(cls) -> UpdateHostFeeBpsJSON:
+        return UpdateHostFeeBpsJSON(
+            kind="UpdateHostFeeBps",
+        )
+
+    @classmethod
+    def to_encodable(cls) -> dict:
+        return {
+            "UpdateHostFeeBps": {},
+        }
+
+
+@dataclass
+class UpdateAdminAuthority:
+    discriminator: typing.ClassVar = 5
+    kind: typing.ClassVar = "UpdateAdminAuthority"
+
+    @classmethod
+    def to_json(cls) -> UpdateAdminAuthorityJSON:
+        return UpdateAdminAuthorityJSON(
+            kind="UpdateAdminAuthority",
+        )
+
+    @classmethod
+    def to_encodable(cls) -> dict:
+        return {
+            "UpdateAdminAuthority": {},
+        }
+
+
+UpdateGlobalConfigModeKind = typing.Union[
+    UpdateEmergencyMode,
+    UpdateBlockLocalAdmins,
+    UpdateBlockNewOrders,
+    UpdateBlockOrdersTaking,
+    UpdateHostFeeBps,
+    UpdateAdminAuthority,
+]
+UpdateGlobalConfigModeJSON = typing.Union[
+    UpdateEmergencyModeJSON,
+    UpdateBlockLocalAdminsJSON,
+    UpdateBlockNewOrdersJSON,
+    UpdateBlockOrdersTakingJSON,
+    UpdateHostFeeBpsJSON,
+    UpdateAdminAuthorityJSON,
+]
+
+
+def from_decoded(obj: dict) -> UpdateGlobalConfigModeKind:
+    if not isinstance(obj, dict):
+        raise ValueError("Invalid enum object")
+    if "UpdateEmergencyMode" in obj:
+        return UpdateEmergencyMode()
+    if "UpdateBlockLocalAdmins" in obj:
+        return UpdateBlockLocalAdmins()
+    if "UpdateBlockNewOrders" in obj:
+        return UpdateBlockNewOrders()
+    if "UpdateBlockOrdersTaking" in obj:
+        return UpdateBlockOrdersTaking()
+    if "UpdateHostFeeBps" in obj:
+        return UpdateHostFeeBps()
+    if "UpdateAdminAuthority" in obj:
+        return UpdateAdminAuthority()
+    raise ValueError("Invalid enum object")
+
+
+def from_json(obj: UpdateGlobalConfigModeJSON) -> UpdateGlobalConfigModeKind:
+    if obj["kind"] == "UpdateEmergencyMode":
+        return UpdateEmergencyMode()
+    if obj["kind"] == "UpdateBlockLocalAdmins":
+        return UpdateBlockLocalAdmins()
+    if obj["kind"] == "UpdateBlockNewOrders":
+        return UpdateBlockNewOrders()
+    if obj["kind"] == "UpdateBlockOrdersTaking":
+        return UpdateBlockOrdersTaking()
+    if obj["kind"] == "UpdateHostFeeBps":
+        return UpdateHostFeeBps()
+    if obj["kind"] == "UpdateAdminAuthority":
+        return UpdateAdminAuthority()
+    kind = obj["kind"]
+    raise ValueError(f"Unrecognized enum kind: {kind}")
+
+
+layout = EnumForCodegen(
+    "UpdateEmergencyMode" / borsh.CStruct(),
+    "UpdateBlockLocalAdmins" / borsh.CStruct(),
+    "UpdateBlockNewOrders" / borsh.CStruct(),
+    "UpdateBlockOrdersTaking" / borsh.CStruct(),
+    "UpdateHostFeeBps" / borsh.CStruct(),
+    "UpdateAdminAuthority" / borsh.CStruct(),
+)

+ 128 - 0
express_relay/sdk/python/express_relay/svm/generated/limo/types/update_global_config_value.py

@@ -0,0 +1,128 @@
+from __future__ import annotations
+import typing
+from dataclasses import dataclass
+from solders.pubkey import Pubkey
+from anchorpy.borsh_extension import EnumForCodegen, BorshPubkey
+import borsh_construct as borsh
+
+BoolJSONValue = tuple[bool]
+U16JSONValue = tuple[int]
+PubkeyJSONValue = tuple[str]
+BoolValue = tuple[bool]
+U16Value = tuple[int]
+PubkeyValue = tuple[Pubkey]
+
+
+class BoolJSON(typing.TypedDict):
+    value: BoolJSONValue
+    kind: typing.Literal["Bool"]
+
+
+class U16JSON(typing.TypedDict):
+    value: U16JSONValue
+    kind: typing.Literal["U16"]
+
+
+class PubkeyJSON(typing.TypedDict):
+    value: PubkeyJSONValue
+    kind: typing.Literal["Pubkey"]
+
+
+@dataclass
+class Bool:
+    discriminator: typing.ClassVar = 0
+    kind: typing.ClassVar = "Bool"
+    value: BoolValue
+
+    def to_json(self) -> BoolJSON:
+        return BoolJSON(
+            kind="Bool",
+            value=(self.value[0],),
+        )
+
+    def to_encodable(self) -> dict:
+        return {
+            "Bool": {
+                "item_0": self.value[0],
+            },
+        }
+
+
+@dataclass
+class U16:
+    discriminator: typing.ClassVar = 1
+    kind: typing.ClassVar = "U16"
+    value: U16Value
+
+    def to_json(self) -> U16JSON:
+        return U16JSON(
+            kind="U16",
+            value=(self.value[0],),
+        )
+
+    def to_encodable(self) -> dict:
+        return {
+            "U16": {
+                "item_0": self.value[0],
+            },
+        }
+
+
+@dataclass
+class Pubkey:
+    discriminator: typing.ClassVar = 2
+    kind: typing.ClassVar = "Pubkey"
+    value: PubkeyValue
+
+    def to_json(self) -> PubkeyJSON:
+        return PubkeyJSON(
+            kind="Pubkey",
+            value=(str(self.value[0]),),
+        )
+
+    def to_encodable(self) -> dict:
+        return {
+            "Pubkey": {
+                "item_0": self.value[0],
+            },
+        }
+
+
+UpdateGlobalConfigValueKind = typing.Union[Bool, U16, Pubkey]
+UpdateGlobalConfigValueJSON = typing.Union[BoolJSON, U16JSON, PubkeyJSON]
+
+
+def from_decoded(obj: dict) -> UpdateGlobalConfigValueKind:
+    if not isinstance(obj, dict):
+        raise ValueError("Invalid enum object")
+    if "Bool" in obj:
+        val = obj["Bool"]
+        return Bool((val["item_0"],))
+    if "U16" in obj:
+        val = obj["U16"]
+        return U16((val["item_0"],))
+    if "Pubkey" in obj:
+        val = obj["Pubkey"]
+        return Pubkey((val["item_0"],))
+    raise ValueError("Invalid enum object")
+
+
+def from_json(obj: UpdateGlobalConfigValueJSON) -> UpdateGlobalConfigValueKind:
+    if obj["kind"] == "Bool":
+        bool_json_value = typing.cast(BoolJSONValue, obj["value"])
+        return Bool((bool_json_value[0],))
+    if obj["kind"] == "U16":
+        u16json_value = typing.cast(U16JSONValue, obj["value"])
+        return U16((u16json_value[0],))
+    if obj["kind"] == "Pubkey":
+        pubkey_json_value = typing.cast(PubkeyJSONValue, obj["value"])
+        return Pubkey((Pubkey.from_string(pubkey_json_value[0]),))
+    kind = obj["kind"]
+    raise ValueError(f"Unrecognized enum kind: {kind}")
+
+
+layout = EnumForCodegen(
+    "Bool" / borsh.CStruct("item_0" / borsh.Bool),
+    "U16" / borsh.CStruct("item_0" / borsh.U16),
+    "Pubkey" / borsh.CStruct("item_0" / BorshPubkey),
+)

+ 342 - 0
express_relay/sdk/python/express_relay/svm/limo_client.py

@@ -0,0 +1,342 @@
+from decimal import Decimal
+from typing import Sequence, List, TypedDict, Tuple
+
+from solana.constants import SYSTEM_PROGRAM_ID
+from solana.rpc.async_api import AsyncClient
+from solana.rpc.types import MemcmpOpts
+from solders import system_program
+from solders.instruction import Instruction, AccountMeta
+from solders.pubkey import Pubkey
+from solders.system_program import TransferParams
+from solders.sysvar import RENT, INSTRUCTIONS
+from spl.token._layouts import MINT_LAYOUT, ACCOUNT_LAYOUT as TOKEN_ACCOUNT_LAYOUT
+from spl.token.constants import (
+    WRAPPED_SOL_MINT,
+    ASSOCIATED_TOKEN_PROGRAM_ID,
+    TOKEN_PROGRAM_ID,
+)
+
+from express_relay.svm.generated.limo.accounts import Order
+from express_relay.svm.generated.limo.instructions import (
+    take_order,
+    TakeOrderArgs,
+)
+from express_relay.svm.generated.limo.program_id import PROGRAM_ID
+
+import spl.token.instructions as spl_token
+
+ESCROW_VAULT_SEED = b"escrow_vault"
+GLOBAL_AUTH_SEED = b"authority"
+EXPRESS_RELAY_MEATADATA_SEED = b"metadata"
+EXPRESS_RELAY_CONFIG_ROUTER_SEED = b"config_router"
+
+
+class OrderStateAndAddress(TypedDict):
+    state: Order
+    address: Pubkey
+
+
+class WSOLInstructions(TypedDict):
+    create_ixs: List[Instruction]
+    fill_ixs: List[Instruction]
+    close_ixs: List[Instruction]
+    ata: Pubkey
+
+
+class LimoClient:
+    def __init__(self, connection: AsyncClient, global_config: Pubkey):
+        self._connection = connection
+        self._global_config = global_config
+
+    async def get_all_orders_state_and_address_with_filters(
+        self, filters: List[MemcmpOpts]
+    ) -> List[OrderStateAndAddress]:
+        filters.append(MemcmpOpts(offset=8, bytes=str(self._global_config)))
+        programs = await self._connection.get_program_accounts(
+            PROGRAM_ID,
+            commitment=None,
+            encoding="base64",
+            data_slice=None,
+            filters=filters,
+        )
+
+        return [
+            {"state": Order.decode(value.account.data), "address": value.pubkey}
+            for value in programs.value
+        ]
+
+    async def get_mint_decimals(self, mint: Pubkey) -> int:
+        mint_account = await self._connection.get_account_info(mint)
+        if mint_account.value is None:
+            raise ValueError("Mint account not found")
+        bytes_data = mint_account.value.data
+        if len(bytes_data) != MINT_LAYOUT.sizeof():
+            raise ValueError("Invalid mint size")
+
+        decoded_data = MINT_LAYOUT.parse(bytes_data)
+        decimals = decoded_data.decimals
+        return decimals
+
+    async def account_exists(self, address: Pubkey) -> bool:
+        account_info = await self._connection.get_account_info(address)
+        return account_info.value is not None
+
+    def create_associated_token_account_idempotent(
+        self, payer: Pubkey, owner: Pubkey, mint: Pubkey, token_program_id: Pubkey
+    ) -> Instruction:
+        """Creates a transaction instruction to create an associated token account.
+
+        Returns:
+            The instruction to create the associated token account.
+        """
+        associated_token_address = self.get_ata(owner, mint, token_program_id)
+        return Instruction(
+            accounts=[
+                AccountMeta(pubkey=payer, is_signer=True, is_writable=True),
+                AccountMeta(
+                    pubkey=associated_token_address, is_signer=False, is_writable=True
+                ),
+                AccountMeta(pubkey=owner, is_signer=False, is_writable=False),
+                AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
+                AccountMeta(
+                    pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False
+                ),
+                AccountMeta(
+                    pubkey=token_program_id, is_signer=False, is_writable=False
+                ),
+                AccountMeta(pubkey=RENT, is_signer=False, is_writable=False),
+            ],
+            program_id=ASSOCIATED_TOKEN_PROGRAM_ID,
+            data=bytes(1),  # idempotent version of the instruction
+        )
+
+    async def get_ata_and_create_ixn_if_required(
+        self,
+        owner: Pubkey,
+        token_mint_address: Pubkey,
+        token_program_id: Pubkey,
+        payer: Pubkey,
+    ) -> Tuple[Pubkey, Sequence[Instruction]]:
+        ata = self.get_ata(owner, token_mint_address, token_program_id)
+        if not await self.account_exists(ata):
+            ix = self.create_associated_token_account_idempotent(
+                payer, owner, token_mint_address, token_program_id
+            )
+            return ata, [ix]
+        return ata, []
+
+    async def get_init_if_needed_wsol_create_and_close_ixs(
+        self, owner: Pubkey, payer: Pubkey, amount_to_deposit_lamports: int
+    ) -> WSOLInstructions:
+        """
+        Returns necessary instructions to create, fill and close a wrapped SOL account.
+        If the account already exists:
+            it makes sure the balance is at least `amount_to_deposit_lamports` with no create or close instructions.
+        If the account does not exist:
+            it creates the account, fills it with `amount_to_deposit_lamports` lamports and close it in the end.
+        Args:
+            owner: Who owns the WSOL token account
+            payer: Who pays for the instructions
+            amount_to_deposit_lamports: Minimum amount of lamports required in the account
+        """
+        ata = self.get_ata(owner, WRAPPED_SOL_MINT, TOKEN_PROGRAM_ID)
+        ata_info = await self._connection.get_account_info(ata)
+
+        create_ixs = []
+        close_ixs = []
+        if ata_info.value is None:
+            create_ixs = [
+                self.create_associated_token_account_idempotent(
+                    payer, owner, WRAPPED_SOL_MINT, TOKEN_PROGRAM_ID
+                )
+            ]
+            close_ixs = [
+                spl_token.close_account(
+                    spl_token.CloseAccountParams(
+                        program_id=TOKEN_PROGRAM_ID,
+                        account=ata,
+                        dest=owner,
+                        owner=owner,
+                    )
+                )
+            ]
+
+        fill_ixs = []
+        current_balance = (
+            TOKEN_ACCOUNT_LAYOUT.parse(ata_info.value.data).amount
+            if ata_info.value
+            else 0
+        )
+        if current_balance < amount_to_deposit_lamports:
+            fill_ixs = [
+                system_program.transfer(
+                    TransferParams(
+                        from_pubkey=owner,
+                        to_pubkey=ata,
+                        lamports=amount_to_deposit_lamports - current_balance,
+                    )
+                ),
+                spl_token.sync_native(
+                    spl_token.SyncNativeParams(TOKEN_PROGRAM_ID, ata)
+                ),
+            ]
+
+        return WSOLInstructions(
+            create_ixs=create_ixs, fill_ixs=fill_ixs, close_ixs=close_ixs, ata=ata
+        )
+
+    async def take_order_ix(
+        self,
+        taker: Pubkey,
+        order: OrderStateAndAddress,
+        input_amount_decimals: Decimal,
+        input_mint_decimals: int,
+        express_relay_program_id: Pubkey,
+    ) -> List[Instruction]:
+        """
+        Returns the instructions to fulfill an order as a taker.
+        Args:
+            taker: The taker's public key
+            order: The order to fulfill
+            input_amount_decimals: The amount of input tokens to take multiplied by 10 ** input_mint_decimals
+            input_mint_decimals: input mint decimals (can be fetched via get_mint_decimals)
+            express_relay_program_id: Express relay program id
+
+        Returns:
+            A list of instructions to include in the transaction to fulfill the order. The submit_bid instruction for
+            express relay program is not included and should be added separately.
+
+        """
+        ixs: List[Instruction] = []
+        close_wsol_ixns: List[Instruction] = []
+        taker_input_ata: Pubkey
+        if order["state"].input_mint == WRAPPED_SOL_MINT:
+            instructions = await self.get_init_if_needed_wsol_create_and_close_ixs(
+                owner=taker, payer=taker, amount_to_deposit_lamports=0
+            )
+            ixs.extend(instructions["create_ixs"])
+            close_wsol_ixns.extend(instructions["close_ixs"])
+            taker_input_ata = instructions["ata"]
+        else:
+            (
+                taker_input_ata,
+                create_taker_input_ata_ixs,
+            ) = await self.get_ata_and_create_ixn_if_required(
+                owner=taker,
+                token_mint_address=order["state"].input_mint,
+                token_program_id=order["state"].input_mint_program_id,
+                payer=taker,
+            )
+            ixs.extend(create_taker_input_ata_ixs)
+
+        taker_output_ata: Pubkey
+        if order["state"].output_mint == WRAPPED_SOL_MINT:
+            raise NotImplementedError("Output mint is WSOL")
+        else:
+            (
+                taker_output_ata,
+                create_taker_output_ata_ixs,
+            ) = await self.get_ata_and_create_ixn_if_required(
+                owner=taker,
+                token_mint_address=order["state"].output_mint,
+                token_program_id=order["state"].output_mint_program_id,
+                payer=taker,
+            )
+            ixs.extend(create_taker_output_ata_ixs)
+
+        (
+            maker_output_ata,
+            create_maker_output_ata_ixs,
+        ) = await self.get_ata_and_create_ixn_if_required(
+            owner=order["state"].maker,
+            token_mint_address=order["state"].output_mint,
+            token_program_id=order["state"].output_mint_program_id,
+            payer=taker,
+        )
+        ixs.extend(create_maker_output_ata_ixs)
+
+        pda_authority = self.get_pda_authority(PROGRAM_ID, order["state"].global_config)
+        ixs.append(
+            take_order(
+                TakeOrderArgs(
+                    input_amount=int(
+                        input_amount_decimals * (10**input_mint_decimals)
+                    )
+                ),
+                {
+                    "taker": taker,
+                    "maker": order["state"].maker,
+                    "global_config": order["state"].global_config,
+                    "pda_authority": pda_authority,
+                    "order": order["address"],
+                    "input_mint": order["state"].input_mint,
+                    "output_mint": order["state"].output_mint,
+                    "input_vault": self.get_token_vault_pda(
+                        PROGRAM_ID,
+                        order["state"].global_config,
+                        order["state"].input_mint,
+                    ),
+                    "taker_input_ata": taker_input_ata,
+                    "taker_output_ata": taker_output_ata,
+                    "maker_output_ata": maker_output_ata,
+                    "express_relay": express_relay_program_id,
+                    "express_relay_metadata": self.get_express_relay_metadata_pda(
+                        express_relay_program_id
+                    ),
+                    "sysvar_instructions": INSTRUCTIONS,
+                    "permission": order["address"],
+                    "config_router": self.get_express_relay_config_router_pda(
+                        express_relay_program_id, pda_authority
+                    ),
+                    "input_token_program": order["state"].input_mint_program_id,
+                    "output_token_program": order["state"].output_mint_program_id,
+                },
+            )
+        )
+
+        ixs.extend(close_wsol_ixns)
+        return ixs
+
+    @staticmethod
+    def get_program_id() -> Pubkey:
+        return PROGRAM_ID
+
+    @staticmethod
+    def get_token_vault_pda(
+        program_id: Pubkey, global_config: Pubkey, input_mint: Pubkey
+    ) -> Pubkey:
+        return Pubkey.find_program_address(
+            seeds=[ESCROW_VAULT_SEED, bytes(global_config), bytes(input_mint)],
+            program_id=program_id,
+        )[0]
+
+    @staticmethod
+    def get_express_relay_metadata_pda(program_id: Pubkey) -> Pubkey:
+        return Pubkey.find_program_address(
+            seeds=[EXPRESS_RELAY_MEATADATA_SEED], program_id=program_id
+        )[0]
+
+    @staticmethod
+    def get_express_relay_config_router_pda(
+        program_id: Pubkey, router: Pubkey
+    ) -> Pubkey:
+        return Pubkey.find_program_address(
+            seeds=[EXPRESS_RELAY_CONFIG_ROUTER_SEED, bytes(router)],
+            program_id=program_id,
+        )[0]
+
+    @staticmethod
+    def get_pda_authority(program_id: Pubkey, global_config: Pubkey) -> Pubkey:
+        return Pubkey.find_program_address(
+            seeds=[GLOBAL_AUTH_SEED, bytes(global_config)], program_id=program_id
+        )[0]
+
+    @staticmethod
+    def get_ata(
+        owner: Pubkey, token_mint_address: Pubkey, token_program_id: Pubkey
+    ) -> Pubkey:
+        ata, _ = Pubkey.find_program_address(
+            seeds=[bytes(owner), bytes(token_program_id), bytes(token_mint_address)],
+            program_id=ASSOCIATED_TOKEN_PROGRAM_ID,
+        )
+        return ata

+ 1 - 0
express_relay/sdk/python/mypy.ini

@@ -1,5 +1,6 @@
 [mypy]
 plugins = pydantic.mypy
+exclude = express_relay/limo_generated_client
 
 follow_imports = silent
 warn_redundant_casts = True

Разлика између датотеке није приказан због своје велике величине
+ 588 - 278
express_relay/sdk/python/poetry.lock


+ 6 - 3
express_relay/sdk/python/pyproject.toml

@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "express-relay"
-version = "0.8.1"
+version = "0.9.0"
 description = "Utilities for searchers and protocols to interact with the Express Relay protocol."
 authors = ["dourolabs"]
 license = "Apache-2.0"
@@ -12,15 +12,18 @@ web3 = "^6.15.1"
 eth_abi = "^4.2.1"
 eth_account = "^0.10.0"
 httpx = "^0.23.3"
-websockets = "^11.0.3"
+websockets = "^10.0"
 asyncio = "^3.4.3"
 argparse = "^1.4.0"
 pydantic = "^2.6.3"
+anchorpy = "^0.20.1"
+solana = "^0.34.3"
 
 [tool.poetry.group.dev.dependencies]
-black = "^24.1.1"
+black = "^22.3.0"
 pyflakes = "^3.2.0"
 mypy = "^1.9.0"
+anchorpy = {extras = ["cli"], version = "^0.20.1"}
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]

Неке датотеке нису приказане због велике количине промена