Skip to content

Commit e2c837d

Browse files
authored
Merge pull request oasisprotocol#2397 from oasisprotocol/matevz/feat/rofl-client-py-sign-submit
rofl-client-py: Add support for sign_submit endpoint
2 parents d66cd0f + 11d2457 commit e2c837d

File tree

4 files changed

+2065
-4
lines changed

4 files changed

+2065
-4
lines changed

rofl-client/py/pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ classifiers = [
2222
]
2323

2424
dependencies = [
25+
"cbor2>=5.7.1",
2526
"httpx>=0.25.0",
27+
"web3>=7.14.0",
2628
]
2729

2830
[dependency-groups]
2931
dev = [
30-
"pytest>=8.4.1",
31-
"ruff>=0.8.4",
32+
"pytest>=8.4.1",
33+
"ruff>=0.8.4",
3234
]
3335

3436
[project.urls]
@@ -105,4 +107,4 @@ known-first-party = ["oasis_rofl_client"]
105107
quote-style = "double"
106108
indent-style = "space"
107109
line-ending = "lf"
108-
skip-magic-trailing-comma = false
110+
skip-magic-trailing-comma = false

rofl-client/py/src/oasis_rofl_client/rofl_client.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from enum import Enum
1111
from typing import Any
1212

13+
import cbor2
1314
import httpx
15+
from web3.types import TxParams
1416

1517
logger = logging.getLogger(__name__)
1618

@@ -122,6 +124,57 @@ async def generate_key(
122124
response: dict[str, Any] = await self._appd_request("POST", path, payload)
123125
return response["key"]
124126

127+
async def sign_submit(
128+
self,
129+
tx: TxParams,
130+
encrypt: bool = False,
131+
) -> dict[str, Any]:
132+
"""Sign the given Ethereum transaction with an endorsed ephemeral key and submit it to Sapphire.
133+
134+
Args:
135+
tx: Transaction parameters
136+
encrypt: End-to-end encrypt the transaction before submitting (default: False)
137+
138+
Returns:
139+
Deserialized response data object.
140+
141+
Raises:
142+
httpx.HTTPStatusError: If the request fails
143+
cbor2.CBORDecodeValueError: If the response data is invalid
144+
145+
Note: Transaction nonce and gas price are ignored.
146+
"""
147+
payload = {
148+
"tx": {
149+
"kind": "eth",
150+
"data": {
151+
"gas_limit": tx["gas"],
152+
"value": tx["value"],
153+
"data": tx["data"][2:]
154+
if tx["data"].startswith("0x")
155+
else tx["data"],
156+
},
157+
},
158+
"encrypt": encrypt,
159+
}
160+
161+
# Contract create transactions don't have "to". For others, include it.
162+
if "to" in tx:
163+
payload["tx"]["data"]["to"] = (
164+
tx["to"][2:] if tx["to"].startswith("0x") else tx["to"]
165+
)
166+
167+
path = "/rofl/v1/tx/sign-submit"
168+
169+
response: dict[str, str] = await self._appd_request(
170+
"POST", path, payload
171+
)
172+
result = {}
173+
# Decode CBOR-encoded data field to python object.
174+
if response.get("data"):
175+
result = cbor2.loads(bytes.fromhex(response["data"]))
176+
return result
177+
125178
async def get_metadata(self) -> dict[str, str]:
126179
"""Get all user-set metadata key-value pairs.
127180

rofl-client/py/tests/test_client.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import unittest
44
from unittest.mock import AsyncMock, MagicMock, patch
55

6+
from web3.types import TxParams
7+
68
from oasis_rofl_client import KeyKind, RoflClient
79

810

@@ -214,6 +216,54 @@ def test_key_kind_enum_values(self):
214216
self.assertEqual(KeyKind.ED25519.value, "ed25519")
215217
self.assertEqual(KeyKind.SECP256K1.value, "secp256k1")
216218

219+
@patch("oasis_rofl_client.rofl_client.httpx.AsyncClient")
220+
async def test_sign_submit(self, mock_client_class):
221+
"""Test sign_submit method."""
222+
# Setup mock
223+
mock_response = MagicMock()
224+
mock_response.json = lambda: {
225+
"data": "a1646661696ca364636f646508666d6f64756c656365766d676d6573736167657272657665727465643a20614a416f4c773d3d"
226+
}
227+
mock_response.raise_for_status = MagicMock()
228+
229+
mock_client = AsyncMock()
230+
mock_client.post.return_value = mock_response
231+
mock_client_class.return_value.__aenter__.return_value = mock_client
232+
233+
# Test set metadata
234+
client = RoflClient()
235+
tx: TxParams = {
236+
"from": "0x1234567890123456789012345678901234567890",
237+
"to": "0x0987654321098765432109876543210987654321",
238+
"data": "0xdae1ee1f00000000000000000000000000000000000000000000000000002695a9e649b2",
239+
"gas": 21000,
240+
"gasPrice": 1000000000,
241+
"value": 1000000000,
242+
"nonce": 0,
243+
}
244+
245+
response = await client.sign_submit(tx, True)
246+
247+
self.assertEqual(response, {"fail": {"code": 8, "module": "evm", "message": "reverted: aJAoLw=="}})
248+
249+
# Verify the API call
250+
mock_client.post.assert_called_once_with(
251+
"http://localhost/rofl/v1/tx/sign-submit",
252+
json={
253+
"tx": {
254+
"kind": "eth",
255+
"data": {
256+
"gas_limit": 21000,
257+
"value": 1000000000,
258+
"data": "dae1ee1f00000000000000000000000000000000000000000000000000002695a9e649b2",
259+
"to": "0987654321098765432109876543210987654321",
260+
},
261+
},
262+
"encrypt": True,
263+
},
264+
timeout=60.0,
265+
)
266+
217267
@patch("oasis_rofl_client.rofl_client.httpx.AsyncClient")
218268
async def test_get_metadata(self, mock_client_class):
219269
"""Test get_metadata method."""

0 commit comments

Comments
 (0)