Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[WIP] Prototype for a remote API validation #56

Open
wants to merge 5 commits into
base: staging
Choose a base branch
from
Open
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
85 changes: 33 additions & 52 deletions api-usage.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@
"outputs": [],
"source": [
"r = requests.post(\n",
" f\"{gas_station_api}/users\",\n",
" f\"{gas_station_api}/sponsors\",\n",
" json = {\n",
" \"address\": alice.public_key_hash(),\n",
" \"tezos_address\": alice.public_key_hash(),\n",
" \"name\": \"alice\"\n",
" }\n",
")"
Expand All @@ -96,7 +96,7 @@
"metadata": {},
"outputs": [],
"source": [
"r = requests.get(f\"{gas_station_api}/users/{alice.public_key_hash()}\")"
"r = requests.get(f\"{gas_station_api}/sponsors/{alice.public_key_hash()}\")"
]
},
{
Expand All @@ -108,8 +108,8 @@
{
"data": {
"text/plain": [
"{'address': 'tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb',\n",
" 'id': '06d44229-4b75-4df5-9bac-df3b53285859',\n",
"{'tezos_address': 'tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb',\n",
" 'id': '2a5e9326-e725-4e60-a8cb-816bad6a4f7b',\n",
" 'name': 'alice',\n",
" 'withdraw_counter': 0}"
]
Expand All @@ -133,9 +133,9 @@
{
"data": {
"text/plain": [
"{'id': '958caaa8-ed25-4c26-a062-42bd78182399',\n",
"{'id': '0cfb5c6d-9655-46c2-bddc-42b772b05367',\n",
" 'amount': 0,\n",
" 'owner_id': '06d44229-4b75-4df5-9bac-df3b53285859'}"
" 'owner_id': '2a5e9326-e725-4e60-a8cb-816bad6a4f7b'}"
]
},
"execution_count": 9,
Expand All @@ -144,7 +144,7 @@
}
],
"source": [
"r = requests.get(f\"{gas_station_api}/credits/{alice_user['address']}\")\n",
"r = requests.get(f\"{gas_station_api}/credits/{alice_user['tezos_address']}\")\n",
"credits = r.json()[0]\n",
"assert r.status_code == 200\n",
"credits"
Expand Down Expand Up @@ -248,20 +248,9 @@
"execution_count": 16,
"id": "bcba5181",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"200"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"outputs": [],
"source": [
"r.status_code"
"assert r.status_code == 200"
]
},
{
Expand All @@ -273,9 +262,9 @@
{
"data": {
"text/plain": [
"[{'id': '958caaa8-ed25-4c26-a062-42bd78182399',\n",
"[{'id': '0cfb5c6d-9655-46c2-bddc-42b772b05367',\n",
" 'amount': 1000000,\n",
" 'owner_id': '06d44229-4b75-4df5-9bac-df3b53285859'}]"
" 'owner_id': '2a5e9326-e725-4e60-a8cb-816bad6a4f7b'}]"
]
},
"execution_count": 17,
Expand Down Expand Up @@ -310,7 +299,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"14\n"
"25\n"
]
}
],
Expand Down Expand Up @@ -374,15 +363,15 @@
},
{
"cell_type": "code",
"execution_count": 24,
"execution_count": 22,
"id": "8ff9a023",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"15\n"
"25\n"
]
}
],
Expand All @@ -395,7 +384,7 @@
},
{
"cell_type": "code",
"execution_count": 25,
"execution_count": 23,
"id": "5508e9bc",
"metadata": {},
"outputs": [],
Expand All @@ -408,7 +397,7 @@
},
{
"cell_type": "code",
"execution_count": 26,
"execution_count": 24,
"id": "0cbecd7f",
"metadata": {},
"outputs": [],
Expand All @@ -427,7 +416,7 @@
},
{
"cell_type": "code",
"execution_count": 27,
"execution_count": 25,
"id": "33cbdddf",
"metadata": {},
"outputs": [
Expand All @@ -437,7 +426,7 @@
"<Response [200]>"
]
},
"execution_count": 27,
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
Expand All @@ -456,7 +445,7 @@
},
{
"cell_type": "code",
"execution_count": 29,
"execution_count": 26,
"id": "202834f6",
"metadata": {},
"outputs": [
Expand All @@ -466,7 +455,7 @@
"200"
]
},
"execution_count": 29,
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
Expand All @@ -493,7 +482,7 @@
},
{
"cell_type": "code",
"execution_count": 30,
"execution_count": 27,
"id": "b34ffa8d",
"metadata": {},
"outputs": [
Expand All @@ -503,7 +492,7 @@
"400"
]
},
"execution_count": 30,
"execution_count": 27,
"metadata": {},
"output_type": "execute_result"
}
Expand All @@ -530,7 +519,7 @@
},
{
"cell_type": "code",
"execution_count": 31,
"execution_count": 28,
"id": "166eb0a0",
"metadata": {},
"outputs": [
Expand All @@ -540,7 +529,7 @@
"200"
]
},
"execution_count": 31,
"execution_count": 28,
"metadata": {},
"output_type": "execute_result"
}
Expand All @@ -559,25 +548,25 @@
},
{
"cell_type": "code",
"execution_count": 32,
"execution_count": 29,
"id": "9daae29e",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[{'type': 'MAX_CALLS_PER_SPONSEE',\n",
" 'vault_id': '958caaa8-ed25-4c26-a062-42bd78182399',\n",
" 'created_at': '2024-03-12T18:00:11.362107+00:00',\n",
" 'id': '928a842a-68da-48c6-b9fa-cccd71ccacb1',\n",
" 'contract_id': '4b0a9fdf-d36a-4ce8-af4c-1facc8ae1371',\n",
" 'vault_id': '0cfb5c6d-9655-46c2-bddc-42b772b05367',\n",
" 'created_at': '2024-03-15T10:55:20.172575+00:00',\n",
" 'is_active': True,\n",
" 'id': 'c86187e1-f184-4a2d-8dda-094cc581820f',\n",
" 'contract_id': 'a7f3f5ee-a790-4784-b57b-b9e486d88838',\n",
" 'entrypoint_id': None,\n",
" 'max': 1,\n",
" 'current': 2,\n",
" 'is_active': True}]"
" 'current': 2}]"
]
},
"execution_count": 32,
"execution_count": 29,
"metadata": {},
"output_type": "execute_result"
}
Expand All @@ -587,14 +576,6 @@
" f\"{gas_station_api}/condition/{credits['id']}\"\n",
").json()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "01e26527",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
Expand Down
113 changes: 113 additions & 0 deletions examples/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import asyncio
from typing import Any
import os
import requests

from fastapi import FastAPI, APIRouter, HTTPException, Request, status
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel

from Crypto.PublicKey import RSA
import jwt

api_url = os.getenv("GAS_STATION_URL")
gs_user_address = "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb"
router = APIRouter()


class Operation(BaseModel):
"""Data sent when posting an operation. The sender is mandatory."""

sender_address: str
operations: list[dict[str, Any]]


# TODO: the signature should include the action decided by the sponsor API
class Receipt(BaseModel):
"""Signature of an operation to be posted on-chain."""
gas_station_action: str
signature: str


try:
skey = "".join(open("./private.pem").readlines())
pkey = "".join(open("./public.pem").readlines())
except FileNotFoundError:
mykey = RSA.generate(1024)
skey = mykey.export_key()
pkey = mykey.public_key().export_key()
with open("./private.pem", "w") as f:
f.write(skey)
with open("./public.pem", "w") as f:
f.write(pkey)


# We will assume this API runs in only one thread, and ignore race conditions
# for this example.
# Shared database of senders, to limit the number of sponsored operations
# per user. This could be implemented as a condition directly in the gas
# station as well.
seen_senders = dict()


def validate(sender):
seen = seen_senders.get(sender, 0)
print("SEEN", seen, "TIMES")
if seen > 1:
return False
else:
seen_senders[sender] = seen + 1
return True


@router.post("/operation", response_model=Receipt)
async def sign_operation(request: Request):
raw_operation = await request.json()
try:
operation = Operation.parse_obj(raw_operation)
except:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Could not parse operation",
)

if validate(operation.sender_address):
signature = jwt.encode(
raw_operation, key=skey, algorithm="RS256"
)
return Receipt(
gas_station_action="post_operation",
signature=signature
)
else:
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder(
{
"detail": "Invalid call",
"body": "FIXME",
"custom msg": ""
}
)
)

# TODO
# - implement "operation_posted"

# Register to the API
r = requests.get(f"{api_url}/sponsors/{gs_user_address}")
gs_user = r.json()
requests.put(
f"{api_url}/sponsor_api",
json = {
"sponsor_id": gs_user["id"],
"api_url": "http://localhost:8005", # This API
"public_key": pkey
}
)

app = FastAPI()
app.include_router(router)

loop = asyncio.get_event_loop()
5 changes: 3 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ pytezos
python-multipart
uvicorn[standard]
fastapi
python-jose[cryptography]
pyjwt[cryptography]
python-dotenv
pydantic
sqlalchemy
psycopg2
alembic
alembic
aiohttp
4 changes: 2 additions & 2 deletions sql/init_db.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
INSERT INTO users("id", "name", "address", "withdraw_counter")
INSERT INTO sponsors("id", "name", "address", "withdraw_counter")
VALUES
('164b18e2-205b-47fa-8fa5-e9961b3a8437', 'Alfred', 'tz1VLKbNYhmfyQSZzsdLWrbtVbyjsRf9qEjN', 0),
('b8c23360-9a81-4450-93d8-ea32a2d7467e', 'Quentin', 'tz1YdFws2E182i25ezpHvEvcn4vh74XcMDFi', 0);
Expand All @@ -22,4 +22,4 @@ INSERT INTO entrypoints("id", "name", "is_enabled", "contract_id")
('dd225743-65b8-465d-849b-be5f795b0e3e', 'permit', true, 'c8b3f63a-9453-4e9f-98b3-855a0de682aa'),
('18e7ee0d-6e16-4392-9cc7-1609d6f84c0c', 'stake', true, 'f08660dc-34a8-4575-b53c-19d362296ead'),
('33532a9c-51e7-4f88-b60f-67530122c349', 'unstake', true, 'f08660dc-34a8-4575-b53c-19d362296ead'),
('764d5857-1201-4a69-bbe8-137e0326a830', 'dummy', false, '4dfbd6f2-ca41-48d0-adc5-9c0bef8127d1');
('764d5857-1201-4a69-bbe8-137e0326a830', 'dummy', false, '4dfbd6f2-ca41-48d0-adc5-9c0bef8127d1');
Loading
Loading