kopia lustrzana https://github.com/bugout-dev/moonstream
Merge pull request #932 from moonstream-to/complete-call-request
Complete call requestpull/1014/head
commit
9ebe300e45
|
@ -0,0 +1,30 @@
|
|||
"""Tx hash for call requests
|
||||
|
||||
Revision ID: 7191eb70e99e
|
||||
Revises: 6d07739cb13e
|
||||
Create Date: 2023-10-04 11:23:12.516797
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7191eb70e99e'
|
||||
down_revision = '6d07739cb13e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('call_requests', sa.Column('tx_hash', sa.VARCHAR(length=256), nullable=True))
|
||||
op.create_unique_constraint(op.f('uq_call_requests_tx_hash'), 'call_requests', ['tx_hash'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(op.f('uq_call_requests_tx_hash'), 'call_requests', type_='unique')
|
||||
op.drop_column('call_requests', 'tx_hash')
|
||||
# ### end Alembic commands ###
|
|
@ -19,23 +19,17 @@ import argparse
|
|||
import base64
|
||||
import json
|
||||
import time
|
||||
from typing import Any, cast, Dict
|
||||
from typing import Any, Dict, Optional, cast
|
||||
|
||||
import eth_keys
|
||||
from eip712.messages import EIP712Message, _hash_eip191_message
|
||||
from eth_account import Account
|
||||
from eth_account._utils.signing import sign_message_hash
|
||||
import eth_keys
|
||||
from eth_typing import ChecksumAddress
|
||||
from hexbytes import HexBytes
|
||||
from web3 import Web3
|
||||
|
||||
|
||||
AUTH_PAYLOAD_NAME = "MoonstreamAuthorization"
|
||||
AUTH_VERSION = "1"
|
||||
|
||||
# By default, authorizations will remain active for 24 hours.
|
||||
DEFAULT_INTERVAL = 60 * 60 * 24
|
||||
|
||||
|
||||
class MoonstreamAuthorizationVerificationError(Exception):
|
||||
"""
|
||||
Raised when invalid signer is provided.
|
||||
|
@ -48,12 +42,48 @@ class MoonstreamAuthorizationExpired(Exception):
|
|||
"""
|
||||
|
||||
|
||||
class MoonstreamAuthorization(EIP712Message):
|
||||
_name_: "string"
|
||||
_version_: "string"
|
||||
class MoonstreamAuthorizationStructureError(Exception):
|
||||
"""
|
||||
Raised when signature has incorrect structure.
|
||||
"""
|
||||
|
||||
address: "address"
|
||||
deadline: "uint256"
|
||||
|
||||
class MoonstreamAuthorization(EIP712Message):
|
||||
_name_: "string" # type: ignore
|
||||
_version_: "string" # type: ignore
|
||||
|
||||
address: "address" # type: ignore
|
||||
deadline: "uint256" # type: ignore
|
||||
|
||||
|
||||
class MetaTXAuthorization(EIP712Message):
|
||||
_name_: "string" # type: ignore
|
||||
_version_: "string" # type: ignore
|
||||
|
||||
caller: "address" # type: ignore
|
||||
expires_at: "uint256" # type: ignore
|
||||
|
||||
|
||||
EIP712_AUTHORIZATION_TYPES = {
|
||||
"MoonstreamAuthorization": {
|
||||
"name": "MoonstreamAuthorization",
|
||||
"version": "1",
|
||||
"eip712_message_class": MoonstreamAuthorization,
|
||||
"primary_types": [
|
||||
{"name": "address", "type": "address"},
|
||||
{"name": "deadline", "type": int},
|
||||
],
|
||||
},
|
||||
"MetaTXAuthorization": {
|
||||
"name": "MetaTXAuthorization",
|
||||
"version": "1",
|
||||
"eip712_message_class": MetaTXAuthorization,
|
||||
"primary_types": [
|
||||
{"name": "caller", "type": "address"},
|
||||
{"name": "expires_at", "type": int},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def sign_message(message_hash_bytes: HexBytes, private_key: HexBytes) -> HexBytes:
|
||||
|
@ -64,52 +94,77 @@ def sign_message(message_hash_bytes: HexBytes, private_key: HexBytes) -> HexByte
|
|||
return signed_message_bytes
|
||||
|
||||
|
||||
def authorize(deadline: int, address: str, private_key: HexBytes) -> Dict[str, Any]:
|
||||
message = MoonstreamAuthorization(
|
||||
_name_=AUTH_PAYLOAD_NAME,
|
||||
_version_=AUTH_VERSION,
|
||||
address=address,
|
||||
deadline=deadline,
|
||||
)
|
||||
def authorize(
|
||||
authorization_type: Dict[str, Any],
|
||||
primary_types: Dict[str, Any],
|
||||
private_key: HexBytes,
|
||||
signature_name_output: str,
|
||||
) -> Dict[str, Any]:
|
||||
# Initializing instance of EIP712Message class
|
||||
attrs: Dict[str, Any] = {
|
||||
"_name_": authorization_type["name"],
|
||||
"_version_": authorization_type["version"],
|
||||
}
|
||||
attrs.update(primary_types)
|
||||
message = authorization_type["eip712_message_class"](**attrs)
|
||||
|
||||
# Generating message hash and signature
|
||||
msg_hash_bytes = HexBytes(_hash_eip191_message(message.signable_message))
|
||||
|
||||
signed_message = sign_message(msg_hash_bytes, private_key)
|
||||
|
||||
api_payload: Dict[str, Any] = {
|
||||
"address": address,
|
||||
"deadline": deadline,
|
||||
"signed_message": signed_message.hex(),
|
||||
}
|
||||
api_payload: Dict[str, Any] = {signature_name_output: signed_message.hex()}
|
||||
api_payload.update(primary_types)
|
||||
|
||||
return api_payload
|
||||
|
||||
|
||||
def verify(authorization_payload: Dict[str, Any]) -> bool:
|
||||
def verify(
|
||||
authorization_type: Dict[str, Any],
|
||||
authorization_payload: Dict[str, Any],
|
||||
signature_name_input: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Verifies provided signature signer by correct address.
|
||||
|
||||
**Important** Assume that not address field is timefield (live_at, expires_at, deadline, etc)
|
||||
"""
|
||||
# Initializing instance of EIP712Message class
|
||||
attrs: Dict[str, Any] = {
|
||||
"_name_": authorization_type["name"],
|
||||
"_version_": authorization_type["version"],
|
||||
}
|
||||
|
||||
time_now = int(time.time())
|
||||
web3_client = Web3()
|
||||
address = Web3.toChecksumAddress(cast(str, authorization_payload["address"]))
|
||||
deadline = cast(int, authorization_payload["deadline"])
|
||||
signature = cast(str, authorization_payload["signed_message"])
|
||||
|
||||
message = MoonstreamAuthorization(
|
||||
_name_=AUTH_PAYLOAD_NAME,
|
||||
_version_=AUTH_VERSION,
|
||||
address=address,
|
||||
deadline=deadline,
|
||||
)
|
||||
address: Optional[ChecksumAddress] = None
|
||||
time_field: Optional[int] = None
|
||||
|
||||
for pt in authorization_type["primary_types"]:
|
||||
pt_name = pt["name"]
|
||||
pt_type = pt["type"]
|
||||
if pt_type == "address":
|
||||
address = Web3.toChecksumAddress(cast(str, authorization_payload[pt_name]))
|
||||
attrs[pt_name] = address
|
||||
else:
|
||||
time_field = cast(pt_type, authorization_payload[pt_name])
|
||||
attrs[pt_name] = time_field
|
||||
if address is None or time_field is None:
|
||||
raise MoonstreamAuthorizationStructureError(
|
||||
"Field address or time_field could not be None"
|
||||
)
|
||||
|
||||
message = authorization_type["eip712_message_class"](**attrs)
|
||||
signature = cast(str, authorization_payload[signature_name_input])
|
||||
signer_address = web3_client.eth.account.recover_message(
|
||||
message.signable_message, signature=signature
|
||||
)
|
||||
if signer_address != address:
|
||||
raise MoonstreamAuthorizationVerificationError("Invalid signer")
|
||||
|
||||
if deadline < time_now:
|
||||
raise MoonstreamAuthorizationExpired("Deadline exceeded")
|
||||
if time_field < time_now:
|
||||
raise MoonstreamAuthorizationExpired("Time field exceeded")
|
||||
|
||||
return True
|
||||
|
||||
|
@ -121,15 +176,37 @@ def decrypt_keystore(keystore_path: str, password: str) -> HexBytes:
|
|||
|
||||
|
||||
def handle_authorize(args: argparse.Namespace) -> None:
|
||||
address, private_key = decrypt_keystore(args.signer, args.password)
|
||||
authorization = authorize(args.deadline, address, private_key)
|
||||
if args.authorization_type not in EIP712_AUTHORIZATION_TYPES:
|
||||
raise Exception("Provided unsupported EIP712 Authorization type")
|
||||
|
||||
authorization_type = EIP712_AUTHORIZATION_TYPES[args.authorization_type]
|
||||
primary_types = json.loads(args.primary_types)
|
||||
for ptk in authorization_type["primary_types"]:
|
||||
if ptk["name"] not in primary_types:
|
||||
raise Exception(f"Lost primary type: {ptk}")
|
||||
|
||||
_, private_key = decrypt_keystore(args.signer, args.password)
|
||||
authorization = authorize(
|
||||
authorization_type=authorization_type,
|
||||
primary_types=primary_types,
|
||||
private_key=private_key,
|
||||
signature_name_output=args.signature_name_output,
|
||||
)
|
||||
print(json.dumps(authorization))
|
||||
|
||||
|
||||
def handle_verify(args: argparse.Namespace) -> None:
|
||||
if args.authorization_type not in EIP712_AUTHORIZATION_TYPES:
|
||||
raise Exception("Provided unsupported EIP712 Authorization type")
|
||||
|
||||
authorization_type = EIP712_AUTHORIZATION_TYPES[args.authorization_type]
|
||||
payload_json = base64.decodebytes(args.payload).decode("utf-8")
|
||||
payload = json.loads(payload_json)
|
||||
verify(payload)
|
||||
verify(
|
||||
authorization_type=authorization_type,
|
||||
authorization_payload=payload,
|
||||
signature_name_input=args.signature_name_input,
|
||||
)
|
||||
print("Verified!")
|
||||
|
||||
|
||||
|
@ -140,13 +217,6 @@ def generate_cli() -> argparse.ArgumentParser:
|
|||
subcommands = parser.add_subparsers()
|
||||
|
||||
authorize_parser = subcommands.add_parser("authorize")
|
||||
authorize_parser.add_argument(
|
||||
"-t",
|
||||
"--deadline",
|
||||
type=int,
|
||||
default=int(time.time()) + DEFAULT_INTERVAL,
|
||||
help="Authorization deadline (seconds since epoch timestamp).",
|
||||
)
|
||||
authorize_parser.add_argument(
|
||||
"-s",
|
||||
"--signer",
|
||||
|
@ -159,6 +229,30 @@ def generate_cli() -> argparse.ArgumentParser:
|
|||
required=False,
|
||||
help="(Optional) password for signing account. If you don't provide it here, you will be prompte for it.",
|
||||
)
|
||||
authorize_parser.add_argument(
|
||||
"-t",
|
||||
"--authorization-type",
|
||||
required=True,
|
||||
choices=[k for k in EIP712_AUTHORIZATION_TYPES.keys()],
|
||||
help="One of supported EIP712 Message authorization types",
|
||||
)
|
||||
authorize_parser.add_argument(
|
||||
"--primary-types",
|
||||
required=True,
|
||||
help="Primary types for specified EIP712 Message authorization in JSON format {0}. Available keys: {1}".format(
|
||||
{"name_1": "value", "name_2": "value"},
|
||||
[
|
||||
f"{v['primary_types']} for {k}"
|
||||
for k, v in EIP712_AUTHORIZATION_TYPES.items()
|
||||
],
|
||||
),
|
||||
)
|
||||
authorize_parser.add_argument(
|
||||
"--signature-name-output",
|
||||
type=str,
|
||||
default="signed_message",
|
||||
help="Key in output dictionary of signature",
|
||||
)
|
||||
authorize_parser.set_defaults(func=handle_authorize)
|
||||
|
||||
verify_parser = subcommands.add_parser("verify")
|
||||
|
@ -168,6 +262,19 @@ def generate_cli() -> argparse.ArgumentParser:
|
|||
required=True,
|
||||
help="Base64-encoded payload to verify",
|
||||
)
|
||||
verify_parser.add_argument(
|
||||
"-t",
|
||||
"--authorization-type",
|
||||
required=True,
|
||||
choices=[k for k in EIP712_AUTHORIZATION_TYPES.keys()],
|
||||
help="One of supported EIP712 Message authorization types",
|
||||
)
|
||||
verify_parser.add_argument(
|
||||
"--signature-name-input",
|
||||
type=str,
|
||||
default="signed_message",
|
||||
help="Key for signature in payload",
|
||||
)
|
||||
verify_parser.set_defaults(func=handle_verify)
|
||||
|
||||
return parser
|
||||
|
|
|
@ -100,6 +100,7 @@ def parse_call_request_response(
|
|||
method=obj[0].method,
|
||||
request_id=str(obj[0].request_id),
|
||||
parameters=obj[0].parameters,
|
||||
tx_hash=obj[0].tx_hash,
|
||||
expires_at=obj[0].expires_at,
|
||||
live_at=obj[0].live_at,
|
||||
created_at=obj[0].created_at,
|
||||
|
@ -480,7 +481,7 @@ def list_call_requests(
|
|||
limit: int = 10,
|
||||
offset: Optional[int] = None,
|
||||
show_expired: bool = False,
|
||||
show_before_live_at: bool = False,
|
||||
live_after: Optional[int] = None,
|
||||
metatx_requester_id: Optional[uuid.UUID] = None,
|
||||
) -> List[Row[Tuple[CallRequest, RegisteredContract, CallRequestType]]]:
|
||||
"""
|
||||
|
@ -526,15 +527,17 @@ def list_call_requests(
|
|||
query = query.filter(
|
||||
CallRequest.metatx_requester_id == metatx_requester_id,
|
||||
)
|
||||
if not show_before_live_at:
|
||||
query = query.filter(
|
||||
or_(CallRequest.live_at < func.now(), CallRequest.live_at == None)
|
||||
)
|
||||
else:
|
||||
query = query.filter(
|
||||
or_(CallRequest.live_at < func.now(), CallRequest.live_at == None)
|
||||
)
|
||||
|
||||
if live_after is not None:
|
||||
assert live_after == int(live_after)
|
||||
if live_after <= 0:
|
||||
raise ValueError("live_after must be positive")
|
||||
query = query.filter(CallRequest.live_at >= datetime.fromtimestamp(live_after))
|
||||
|
||||
if offset is not None:
|
||||
query = query.offset(offset)
|
||||
|
||||
|
@ -579,6 +582,46 @@ def delete_requests(
|
|||
return requests_to_delete_num
|
||||
|
||||
|
||||
def complete_call_request(
|
||||
db_session: Session,
|
||||
tx_hash: str,
|
||||
call_request_id: uuid.UUID,
|
||||
caller: str,
|
||||
) -> CallRequest:
|
||||
results = (
|
||||
db_session.query(CallRequest, RegisteredContract)
|
||||
.join(
|
||||
RegisteredContract,
|
||||
CallRequest.registered_contract_id == RegisteredContract.id,
|
||||
)
|
||||
.filter(CallRequest.id == call_request_id)
|
||||
.filter(CallRequest.caller == caller)
|
||||
.all()
|
||||
)
|
||||
|
||||
if len(results) == 0:
|
||||
raise CallRequestNotFound("Call request with given ID not found")
|
||||
elif len(results) != 1:
|
||||
raise Exception(
|
||||
f"Incorrect number of results found for request_id {call_request_id}"
|
||||
)
|
||||
call_request, registered_contract = results[0]
|
||||
|
||||
call_request.tx_hash = tx_hash
|
||||
|
||||
try:
|
||||
db_session.add(call_request)
|
||||
db_session.commit()
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"complete_call_request -- error updating in database: {repr(err)}"
|
||||
)
|
||||
db_session.rollback()
|
||||
raise
|
||||
|
||||
return (call_request, registered_contract)
|
||||
|
||||
|
||||
def handle_register(args: argparse.Namespace) -> None:
|
||||
"""
|
||||
Handles the register command.
|
||||
|
|
|
@ -306,6 +306,7 @@ class CallRequestResponse(BaseModel):
|
|||
method: str
|
||||
request_id: str
|
||||
parameters: Dict[str, Any]
|
||||
tx_hash: Optional[str] = None
|
||||
expires_at: Optional[datetime] = None
|
||||
live_at: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
|
@ -328,6 +329,10 @@ class CallRequestResponse(BaseModel):
|
|||
return Web3.toChecksumAddress(v)
|
||||
|
||||
|
||||
class CompleteCallRequestsAPIRequest(BaseModel):
|
||||
tx_hash: str
|
||||
|
||||
|
||||
class QuartilesResponse(BaseModel):
|
||||
percentile_25: Dict[str, Any]
|
||||
percentile_50: Dict[str, Any]
|
||||
|
|
|
@ -6,18 +6,24 @@ from uuid import UUID
|
|||
|
||||
from bugout.data import BugoutResource, BugoutResources, BugoutUser
|
||||
from bugout.exceptions import BugoutResponseException
|
||||
from fastapi import Header, HTTPException, Request, Response
|
||||
from eip712.messages import EIP712Message, _hash_eip191_message
|
||||
from eth_account.messages import encode_defunct
|
||||
from fastapi import Depends, Header, HTTPException, Request, Response
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from hexbytes import HexBytes
|
||||
from pydantic import AnyHttpUrl, parse_obj_as
|
||||
from starlette.datastructures import Headers
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import Response
|
||||
from starlette.types import ASGIApp
|
||||
from web3 import Web3
|
||||
from web3.auto import w3 as w3_auto
|
||||
|
||||
from . import data
|
||||
from .auth import (
|
||||
EIP712_AUTHORIZATION_TYPES,
|
||||
MoonstreamAuthorizationExpired,
|
||||
MoonstreamAuthorizationStructureError,
|
||||
MoonstreamAuthorizationVerificationError,
|
||||
verify,
|
||||
)
|
||||
|
@ -34,6 +40,8 @@ from .settings import bugout_client as bc
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
|
||||
|
||||
class InvalidAuthHeaderFormat(Exception):
|
||||
"""
|
||||
|
@ -77,7 +85,44 @@ def bugout_auth(token: str) -> BugoutUser:
|
|||
return user
|
||||
|
||||
|
||||
async def user_for_auth_header(
|
||||
def brood_auth(token: UUID) -> BugoutUser:
|
||||
try:
|
||||
user: BugoutUser = bugout_auth(token=token)
|
||||
except BugoutUnverifiedAuth:
|
||||
logger.info(f"Attempted access by unverified Brood account: {user.id}")
|
||||
raise EngineHTTPException(
|
||||
status_code=403,
|
||||
detail="Only verified accounts can have access",
|
||||
)
|
||||
except BugoutAuthWrongApp:
|
||||
raise EngineHTTPException(
|
||||
status_code=403,
|
||||
detail="User does not belong to this application",
|
||||
)
|
||||
except BugoutResponseException as e:
|
||||
raise EngineHTTPException(
|
||||
status_code=e.status_code,
|
||||
detail=e.detail,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing Brood response: {str(e)}")
|
||||
raise EngineHTTPException(
|
||||
status_code=500,
|
||||
detail="Internal server error",
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def request_user_auth(
|
||||
token: UUID = Depends(oauth2_scheme),
|
||||
) -> BugoutUser:
|
||||
user = brood_auth(token=token)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def request_none_or_user_auth(
|
||||
authorization: str = Header(None),
|
||||
) -> Optional[BugoutUser]:
|
||||
"""
|
||||
|
@ -85,39 +130,74 @@ async def user_for_auth_header(
|
|||
"""
|
||||
user: Optional[BugoutUser] = None
|
||||
if authorization is not None:
|
||||
user_token: str = ""
|
||||
token: str = ""
|
||||
try:
|
||||
_, user_token = parse_auth_header(auth_header=authorization)
|
||||
_, token = parse_auth_header(auth_header=authorization)
|
||||
except InvalidAuthHeaderFormat:
|
||||
raise EngineHTTPException(
|
||||
status_code=403, detail="Wrong authorization header"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing Brood response: {str(e)}")
|
||||
logger.error(f"Error parsing auth header: {str(e)}")
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
if user_token != "":
|
||||
try:
|
||||
user: BugoutUser = bugout_auth(token=user_token)
|
||||
except BugoutUnverifiedAuth:
|
||||
logger.info(f"Attempted access by unverified Brood account: {user.id}")
|
||||
raise EngineHTTPException(
|
||||
status_code=403,
|
||||
detail="Only verified accounts can have access",
|
||||
)
|
||||
except BugoutAuthWrongApp:
|
||||
raise EngineHTTPException(
|
||||
status_code=403, detail="User does not belong to this application"
|
||||
)
|
||||
except BugoutResponseException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing Brood response: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
if token != "":
|
||||
user = brood_auth(token=token)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def metatx_verify_header(
|
||||
authorization: str = Header(None),
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
message: Optional[Dict[str, Any]] = None
|
||||
if authorization is not None:
|
||||
try:
|
||||
auth_format, user_token = parse_auth_header(auth_header=authorization)
|
||||
except InvalidAuthHeaderFormat:
|
||||
raise EngineHTTPException(
|
||||
status_code=403, detail="Wrong authorization header"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing auth header: {str(e)}")
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
if auth_format != "metatx":
|
||||
raise EngineHTTPException(
|
||||
status_code=403,
|
||||
detail=f"Wrong authorization header format: {auth_format}",
|
||||
)
|
||||
|
||||
try:
|
||||
json_payload_str = base64.b64decode(user_token).decode("utf-8")
|
||||
payload = json.loads(json_payload_str)
|
||||
verify(
|
||||
authorization_type=EIP712_AUTHORIZATION_TYPES["MetaTXAuthorization"],
|
||||
authorization_payload=payload,
|
||||
signature_name_input="signature",
|
||||
)
|
||||
message = {
|
||||
"caller": Web3.toChecksumAddress(payload.get("caller")),
|
||||
"expires_at": payload.get("expires_at"),
|
||||
}
|
||||
except MoonstreamAuthorizationVerificationError as e:
|
||||
logger.info("MetaTX authorization verification error: %s", e)
|
||||
raise EngineHTTPException(status_code=403, detail="Invalid signer")
|
||||
except MoonstreamAuthorizationExpired as e:
|
||||
logger.info("MetaTX authorization expired: %s", e)
|
||||
raise EngineHTTPException(status_code=403, detail="Authorization expired")
|
||||
except MoonstreamAuthorizationStructureError as e:
|
||||
logger.info("MetaTX authorization incorrect structure error: %s", e)
|
||||
raise EngineHTTPException(
|
||||
status_code=403, detail="Incorrect signature structure"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Unexpected exception: %s", e)
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
return message
|
||||
|
||||
|
||||
class BroodAuthMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Checks the authorization header on the request. If it represents a verified Brood user,
|
||||
|
@ -155,7 +235,7 @@ class BroodAuthMiddleware(BaseHTTPMiddleware):
|
|||
except InvalidAuthHeaderFormat:
|
||||
return Response(status_code=403, content="Wrong authorization header")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing Brood response: {str(e)}")
|
||||
logger.error(f"Error parsing auth header: {str(e)}")
|
||||
return Response(status_code=500, content="Internal server error")
|
||||
|
||||
try:
|
||||
|
@ -226,9 +306,15 @@ class EngineAuthMiddleware(BaseHTTPMiddleware):
|
|||
authorization_header_components[-1]
|
||||
).decode("utf-8")
|
||||
|
||||
json_payload = json.loads(json_payload_str)
|
||||
verified = verify(json_payload)
|
||||
address = json_payload.get("address")
|
||||
payload = json.loads(json_payload_str)
|
||||
verified = verify(
|
||||
authorization_type=EIP712_AUTHORIZATION_TYPES[
|
||||
"MoonstreamAuthorization"
|
||||
],
|
||||
authorization_payload=payload,
|
||||
signature_name_input="signed_message",
|
||||
)
|
||||
address = payload.get("address")
|
||||
if address is not None:
|
||||
address = Web3.toChecksumAddress(address)
|
||||
else:
|
||||
|
|
|
@ -315,6 +315,7 @@ class CallRequest(Base):
|
|||
method = Column(String, nullable=False, index=True)
|
||||
request_id = Column(DECIMAL, nullable=False, index=True)
|
||||
parameters = Column(JSONB, nullable=False)
|
||||
tx_hash = Column(VARCHAR(256), unique=True, nullable=True)
|
||||
|
||||
expires_at = Column(DateTime(timezone=True), nullable=True, index=True)
|
||||
live_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
|
|
@ -10,16 +10,17 @@ from typing import Dict, List, Optional
|
|||
from uuid import UUID
|
||||
|
||||
from bugout.data import BugoutUser
|
||||
from fastapi import Body, Depends, FastAPI, Path, Query, Request
|
||||
from fastapi import Body, Depends, FastAPI, Form, Path, Query, Request
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .. import contracts_actions, data, db
|
||||
from ..middleware import (
|
||||
BroodAuthMiddleware,
|
||||
BugoutCORSMiddleware,
|
||||
EngineHTTPException,
|
||||
user_for_auth_header,
|
||||
metatx_verify_header,
|
||||
request_none_or_user_auth,
|
||||
request_user_auth,
|
||||
)
|
||||
from ..settings import DOCS_TARGET_PATH
|
||||
from ..version import VERSION
|
||||
|
@ -40,15 +41,6 @@ tags_metadata = [
|
|||
]
|
||||
|
||||
|
||||
whitelist_paths = {
|
||||
"/metatx/openapi.json": "GET",
|
||||
f"/metatx/{DOCS_TARGET_PATH}": "GET",
|
||||
"/metatx/blockchains": "GET",
|
||||
"/metatx/contracts/types": "GET",
|
||||
"/metatx/requests/types": "GET",
|
||||
"/metatx/requests": "GET", # Controls by custom authentication check
|
||||
}
|
||||
|
||||
app = FastAPI(
|
||||
title=TITLE,
|
||||
description=DESCRIPTION,
|
||||
|
@ -59,9 +51,6 @@ app = FastAPI(
|
|||
redoc_url=f"/{DOCS_TARGET_PATH}",
|
||||
)
|
||||
|
||||
|
||||
app.add_middleware(BroodAuthMiddleware, whitelist=whitelist_paths)
|
||||
|
||||
app.add_middleware(
|
||||
BugoutCORSMiddleware,
|
||||
allow_credentials=True,
|
||||
|
@ -95,11 +84,11 @@ async def blockchains_route(
|
|||
response_model=List[data.RegisteredContractResponse],
|
||||
)
|
||||
async def list_registered_contracts_route(
|
||||
request: Request,
|
||||
blockchain: Optional[str] = Query(None),
|
||||
address: Optional[str] = Query(None),
|
||||
limit: int = Query(10),
|
||||
offset: Optional[int] = Query(None),
|
||||
user: BugoutUser = Depends(request_user_auth),
|
||||
db_session: Session = Depends(db.yield_db_read_only_session),
|
||||
) -> List[data.RegisteredContractResponse]:
|
||||
"""
|
||||
|
@ -109,7 +98,7 @@ async def list_registered_contracts_route(
|
|||
registered_contracts_with_blockchain = (
|
||||
contracts_actions.lookup_registered_contracts(
|
||||
db_session=db_session,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
metatx_requester_id=user.id,
|
||||
blockchain=blockchain,
|
||||
address=address,
|
||||
limit=limit,
|
||||
|
@ -132,8 +121,8 @@ async def list_registered_contracts_route(
|
|||
response_model=data.RegisteredContractResponse,
|
||||
)
|
||||
async def get_registered_contract_route(
|
||||
request: Request,
|
||||
contract_id: UUID = Path(...),
|
||||
user: BugoutUser = Depends(request_user_auth),
|
||||
db_session: Session = Depends(db.yield_db_read_only_session),
|
||||
) -> List[data.RegisteredContractResponse]:
|
||||
"""
|
||||
|
@ -142,7 +131,7 @@ async def get_registered_contract_route(
|
|||
try:
|
||||
contract_with_blockchain = contracts_actions.get_registered_contract(
|
||||
db_session=db_session,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
metatx_requester_id=user.id,
|
||||
contract_id=contract_id,
|
||||
)
|
||||
except NoResultFound:
|
||||
|
@ -163,8 +152,8 @@ async def get_registered_contract_route(
|
|||
"/contracts", tags=["contracts"], response_model=data.RegisteredContractResponse
|
||||
)
|
||||
async def register_contract_route(
|
||||
request: Request,
|
||||
contract: data.RegisterContractRequest = Body(...),
|
||||
user: BugoutUser = Depends(request_user_auth),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> data.RegisteredContractResponse:
|
||||
"""
|
||||
|
@ -173,7 +162,7 @@ async def register_contract_route(
|
|||
try:
|
||||
contract_with_blockchain = contracts_actions.register_contract(
|
||||
db_session=db_session,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
metatx_requester_id=user.id,
|
||||
blockchain_name=contract.blockchain,
|
||||
address=contract.address,
|
||||
title=contract.title,
|
||||
|
@ -204,15 +193,15 @@ async def register_contract_route(
|
|||
response_model=data.RegisteredContractResponse,
|
||||
)
|
||||
async def update_contract_route(
|
||||
request: Request,
|
||||
contract_id: UUID = Path(...),
|
||||
update_info: data.UpdateContractRequest = Body(...),
|
||||
user: BugoutUser = Depends(request_user_auth),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> data.RegisteredContractResponse:
|
||||
try:
|
||||
contract_with_blockchain = contracts_actions.update_registered_contract(
|
||||
db_session=db_session,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
metatx_requester_id=user.id,
|
||||
contract_id=contract_id,
|
||||
title=update_info.title,
|
||||
description=update_info.description,
|
||||
|
@ -239,8 +228,8 @@ async def update_contract_route(
|
|||
response_model=data.RegisteredContractResponse,
|
||||
)
|
||||
async def delete_contract_route(
|
||||
request: Request,
|
||||
contract_id: UUID = Path(...),
|
||||
user: BugoutUser = Depends(request_user_auth),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> data.RegisteredContractResponse:
|
||||
"""
|
||||
|
@ -249,7 +238,7 @@ async def delete_contract_route(
|
|||
try:
|
||||
deleted_contract_with_blockchain = contracts_actions.delete_registered_contract(
|
||||
db_session=db_session,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
metatx_requester_id=user.id,
|
||||
registered_contract_id=contract_id,
|
||||
)
|
||||
except Exception as err:
|
||||
|
@ -296,8 +285,8 @@ async def list_requests_route(
|
|||
limit: int = Query(100),
|
||||
offset: Optional[int] = Query(None),
|
||||
show_expired: bool = Query(False),
|
||||
show_before_live_at: bool = Query(False),
|
||||
user: Optional[BugoutUser] = Depends(user_for_auth_header),
|
||||
live_after: Optional[int] = Query(None),
|
||||
user: Optional[BugoutUser] = Depends(request_none_or_user_auth),
|
||||
db_session: Session = Depends(db.yield_db_read_only_session),
|
||||
) -> List[data.CallRequestResponse]:
|
||||
"""
|
||||
|
@ -314,7 +303,7 @@ async def list_requests_route(
|
|||
limit=limit,
|
||||
offset=offset,
|
||||
show_expired=show_expired,
|
||||
show_before_live_at=show_before_live_at,
|
||||
live_after=live_after,
|
||||
metatx_requester_id=user.id if user is not None else None,
|
||||
)
|
||||
except ValueError as e:
|
||||
|
@ -332,6 +321,7 @@ async def list_requests_route(
|
|||
)
|
||||
async def get_request(
|
||||
request_id: UUID = Path(...),
|
||||
_: BugoutUser = Depends(request_user_auth),
|
||||
db_session: Session = Depends(db.yield_db_read_only_session),
|
||||
) -> List[data.CallRequestResponse]:
|
||||
"""
|
||||
|
@ -358,8 +348,8 @@ async def get_request(
|
|||
|
||||
@app.post("/requests", tags=["requests"], response_model=int)
|
||||
async def create_requests(
|
||||
request: Request,
|
||||
data: data.CreateCallRequestsAPIRequest = Body(...),
|
||||
user: BugoutUser = Depends(request_user_auth),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> int:
|
||||
"""
|
||||
|
@ -370,7 +360,7 @@ async def create_requests(
|
|||
try:
|
||||
num_requests = contracts_actions.create_request_calls(
|
||||
db_session=db_session,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
metatx_requester_id=user.id,
|
||||
registered_contract_id=data.contract_id,
|
||||
contract_address=data.contract_address,
|
||||
call_specs=data.specifications,
|
||||
|
@ -411,8 +401,8 @@ async def create_requests(
|
|||
|
||||
@app.delete("/requests", tags=["requests"], response_model=int)
|
||||
async def delete_requests(
|
||||
request: Request,
|
||||
request_ids: List[UUID] = Body(...),
|
||||
user: BugoutUser = Depends(request_user_auth),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> int:
|
||||
"""
|
||||
|
@ -421,7 +411,7 @@ async def delete_requests(
|
|||
try:
|
||||
deleted_requests = contracts_actions.delete_requests(
|
||||
db_session=db_session,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
metatx_requester_id=user.id,
|
||||
request_ids=request_ids,
|
||||
)
|
||||
except Exception as err:
|
||||
|
@ -429,3 +419,32 @@ async def delete_requests(
|
|||
raise EngineHTTPException(status_code=500)
|
||||
|
||||
return deleted_requests
|
||||
|
||||
|
||||
@app.post("/requests/{request_id}/complete", tags=["requests"])
|
||||
async def complete_call_request_route(
|
||||
complete_request: data.CompleteCallRequestsAPIRequest = Body(...),
|
||||
request_id: UUID = Path(...),
|
||||
message=Depends(metatx_verify_header),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
):
|
||||
"""
|
||||
Set tx hash for specified call_request by verified account.
|
||||
"""
|
||||
try:
|
||||
request = contracts_actions.complete_call_request(
|
||||
db_session=db_session,
|
||||
tx_hash=complete_request.tx_hash,
|
||||
call_request_id=request_id,
|
||||
caller=message["caller"],
|
||||
)
|
||||
except contracts_actions.CallRequestNotFound:
|
||||
raise EngineHTTPException(
|
||||
status_code=404,
|
||||
detail="There is no call request with that ID.",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(repr(e))
|
||||
raise EngineHTTPException(status_code=500)
|
||||
|
||||
return contracts_actions.parse_call_request_response(request)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
[mypy]
|
||||
|
||||
[mypy-eth_keys.*]
|
||||
ignore_missing_imports = True
|
Ładowanie…
Reference in New Issue