Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/rrelayer/__pycache__/
1 change: 1 addition & 0 deletions sdk/python/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
Empty file added sdk/python/README.md
Empty file.
17 changes: 17 additions & 0 deletions sdk/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[project]
name = "rrelayer"
version = "0.1.0"
description = "The rrelayer sdk"
readme = "README.md"
authors = [
{ name = "parizval", email = "[email protected]" }
]
requires-python = ">=3.13"
dependencies = [
"pydantic>=2.12.3",
"web3>=7.14.0",
]

[build-system]
requires = ["uv_build>=0.9.2,<0.10.0"]
build-backend = "uv_build"
Empty file.
97 changes: 97 additions & 0 deletions sdk/python/src/rrelayer/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from typing import Any
import aiohttp
import asyncio
from pydantic import BaseModel, ConfigDict, PrivateAttr
from base64 import b64encode


class API(BaseModel):
_session: aiohttp.ClientSession | None = PrivateAttr(default=None)

model_config = ConfigDict(arbitrary_types_allowed=True)

def __del__(self):
if self._session and not self._session.closed:
try:
loop = asyncio.get_running_loop()
loop.create_task(self._session.close())
except RuntimeError:
# No running event loop (e.g., program exit) — close synchronously
asyncio.run(self._session.close())
print("Destroyed API Session")

def build_headers(self, baseConfig: dict[str, str]) -> dict[str, str]:
headers = {
"Content-Type": "application/json",
}

if "apiKey" in baseConfig:
headers["x-api-key"] = baseConfig["apiKey"]

elif "username" in baseConfig and "password" in baseConfig:
credentials = f"{baseConfig['username']}:{baseConfig['password']}"

headers["Authorization"] = (
f"Basic {b64encode(credentials.encode()).decode()}"
)
else:
raise ValueError("API::Invalid authentication credentials")

return headers

async def _get_session(self):
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession()
return self._session

async def getApi(
self, baseConfig: dict[str, str], endpoint: str, params: dict[str, Any] = {}
) -> dict[str, Any]:
session = await self._get_session()

headers = self.build_headers(baseConfig)

async with session.get(
f"{baseConfig['serverURL']}/{endpoint}", headers=headers, params=params
) as response:
return await response.json()

async def postApi(
self, baseConfig: dict[str, str], endpoint: str, body: dict[str, Any]
) -> dict[str, Any]:
session = await self._get_session()

headers = self.build_headers(baseConfig)

async with session.post(
f"{baseConfig['serverURL']}/{endpoint}", headers=headers, json=body
) as response:
return await response.json()

async def putApi(
self, baseConfig: dict[str, str], endpoint: str, body: dict[str, Any]
) -> dict[str, Any]:
session = await self._get_session()

headers = self.build_headers(baseConfig)

async with session.put(
f"{baseConfig['serverURL']}/{endpoint}", headers=headers, json=body
) as response:
return await response.json()

async def deleteApi(
self, baseConfig: dict[str, str], endpoint: str, body: dict = {}
) -> dict[str, Any]:
session = await self._get_session()

headers = self.build_headers(baseConfig)

async with session.delete(
f"{baseConfig['serverURL']}/{endpoint}", headers=headers, json=body
) as response:
return await response.json()

async def close(self):
if self._session and not self._session.closed:
await self._session.close()
191 changes: 191 additions & 0 deletions sdk/python/src/rrelayer/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
from typing import Any
from pydantic import BaseModel, validate_call, ConfigDict, PrivateAttr
from rrelayer.api import API
from rrelayer.types import PagingContext, defaultPagingContext
from rrelayer.relayer import RelayerClient


class Client(BaseModel):
class Relayer:
def __init__(self, client: "Client"):
self._client: "Client" = client

@validate_call
async def create(self, chainId: int, name: str):
return await self._client._api.postApi(
self._client._apiBaseConfig, f"relayers/{chainId}/new", {"name": name}
)

@validate_call
async def clone(self, relayerId: str, chainId: int, name: str):
return await self._client._api.postApi(
self._client._apiBaseConfig,
f"relayers/{relayerId}/clone",
{"newRelayerName": name, "chainId": chainId},
)

@validate_call
async def delete(self, id: str):
_ = await self._client._api.deleteApi(
self._client._apiBaseConfig, f"relayers/{id}"
)

@validate_call
async def get(self, id: str) -> dict[str, Any]:
return await self._client._api.getApi(
self._client._apiBaseConfig, f"relayers/{id}"
)

@validate_call
async def getAll(
self,
pagingContext: PagingContext = defaultPagingContext,
onlyForChainId: int | None = None,
):
params = pagingContext.model_dump()

if onlyForChainId:
params["chainId"] = onlyForChainId

return await self._client._api.getApi(
self._client._apiBaseConfig, "relayers", params
)

class Network:
def __init__(self, client: "Client"):
self._client: "Client" = client

@validate_call
async def get(self, chainId: int):
return await self._client._api.getApi(
self._client._apiBaseConfig, f"networks/{chainId}"
)

@validate_call
async def getAll(self):
return await self._client._api.getApi(
self._client._apiBaseConfig, "networks"
)

@validate_call
async def getGasPrices(self, chainId: int):
return await self._client._api.getApi(
self._client._apiBaseConfig, f"networks/gas/price/{chainId}"
)

class Transaction:
def __init__(self, client: "Client"):
self._client: "Client" = client

@validate_call
async def get(self, transactionId: str):
return await self._client._api.getApi(
self._client._apiBaseConfig, f"transactions/{transactionId}"
)

@validate_call
async def getStatus(self, transactionId: str):
return await self._client._api.getApi(
self._client._apiBaseConfig, f"transactions/status/{transactionId}"
)

@validate_call
async def sendRandom(self):
pass

class AllowList:
def __init__(self, client: "Client"):
self._client: "Client" = client

@validate_call
async def get(
self, relayerId: str, pagingContext: PagingContext = defaultPagingContext
):
params = pagingContext.model_dump()

return await self._client._api.getApi(
self._client._apiBaseConfig, f"relayers/{relayerId}/allowlists", params
)

_apiBaseConfig: dict[str, str] = PrivateAttr()

_api: API = PrivateAttr()

_allowlist: AllowList = PrivateAttr()
_network: Network = PrivateAttr()
_relayer: Relayer = PrivateAttr()
_transaction: Transaction = PrivateAttr()

model_config = ConfigDict(arbitrary_types_allowed=True)

def __init__(self, serverURL: str, auth_username: str, auth_password: str, **data):
super().__init__(**data)

self._apiBaseConfig = {
"serverURL": serverURL,
"username": auth_username,
"password": auth_password,
}

self._api = API()

self._allowlist = self.AllowList(self)
self._network = self.Network(self)
self._relayer = self.Relayer(self)
self._transaction = self.Transaction(self)

@property
def allowlist(self) -> AllowList:
return self._allowlist

@property
def network(self) -> Network:
return self._network

@property
def relayer(self) -> Relayer:
return self._relayer

@property
def transaction(self) -> Transaction:
return self._transaction

@validate_call
async def getRelayerClient(
self, relayerId: str, providerURL: str, defaultSpeed: None = None
) -> RelayerClient:
relayer = await self._relayer.get(relayerId)
if relayer:
pass

auth = {
"username": self._apiBaseConfig["username"],
"password": self._apiBaseConfig["password"],
}

return RelayerClient(
self._apiBaseConfig["serverURL"], providerURL, relayerId, auth
)


@validate_call
def createClient(serverURL: str, auth_username: str, auth_password: str) -> Client:
return Client(
serverURL=serverURL,
auth_username=auth_username,
auth_password=auth_password,
)


@validate_call
def createRelayerClient(
serverURL: str,
providerURL: str,
relayerId: str,
apiKey: str,
) -> RelayerClient:
auth = {
"apiKey": apiKey,
}

return RelayerClient(serverURL, providerURL, relayerId, auth)
Empty file.
Loading