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
23 changes: 18 additions & 5 deletions packages/sync/octobot_sync/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,23 @@
from starfish_server.replica import ReplicaManager

import octobot_sync.auth as auth
import octobot_sync.chain as chain
import octobot_sync.chain.evm as evm
import octobot_sync.constants as constants
import octobot_sync.sync as sync
from octobot_sync.sync.role_resolver import VerifySignatureFn

DEFAULT_SUPPORTED_CHAINS = frozenset({"evm:8453"})


async def _verify_evm(canonical: str, signature: str, pubkey: str) -> bool:
return evm.verify_evm(canonical, signature, pubkey)


def create_app(
nonce: auth.NonceStore,
object_store: AbstractObjectStore,
registry: chain.ChainRegistry,
verify_signature: VerifySignatureFn = _verify_evm,
supported_chains: set[str] | None = None,
collections_path: str | None = None,
primary_url: str | None = None,
auth_provider: auth.StarfishAuthProvider | None = None,
Expand All @@ -42,6 +50,9 @@ def create_app(
) -> FastAPI:
app = FastAPI(title="OctoBot Sync — Signal Sync Server")

if supported_chains is None:
supported_chains = DEFAULT_SUPPORTED_CHAINS

platform_pubkey = os.environ["PLATFORM_PUBKEY_EVM"]
encryption_secret = os.environ["ENCRYPTION_SECRET"]
platform_encryption_secret = os.environ["PLATFORM_ENCRYPTION_SECRET"]
Expand All @@ -66,14 +77,16 @@ def create_app(
SyncRouterOptions(
store=object_store,
config=sync_config,
role_resolver=sync.create_role_resolver(registry, nonce, platform_pubkey),
role_enricher=sync.create_role_enricher(registry),
role_resolver=sync.create_role_resolver(
verify_signature, nonce, platform_pubkey, supported_chains,
),
role_enricher=sync.create_role_enricher(object_store),
encryption_secret=encryption_secret,
identity_encryption_info=constants.HKDF_INFO_USER_DATA,
server_encryption_secret=platform_encryption_secret,
server_identity=platform_pubkey,
server_encryption_info=constants.HKDF_INFO_PLATFORM_DATA,
signature_verifier=sync.create_signature_verifier(registry),
signature_verifier=sync.create_signature_verifier(verify_signature),
replica_manager=replica_manager,
)
)
Expand Down
4 changes: 2 additions & 2 deletions packages/sync/octobot_sync/auth/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def address(self) -> str:
return self._address

async def sign_payload(self, data: str) -> str:
msg_hash = evm._eip191_hash(data)
msg_hash = evm.eip191_hash(data)
signed = self._w3.eth.account._sign_hash(msg_hash, private_key=self._private_key)
return signed.signature.hex()

Expand All @@ -47,7 +47,7 @@ async def __call__(
nonce = str(uuid.uuid4())
body_hash = canonical.hash_body(body)
msg = canonical.build_canonical(method, path, ts, nonce, body_hash)
msg_hash = evm._eip191_hash(msg)
msg_hash = evm.eip191_hash(msg)
signed = self._w3.eth.account._sign_hash(msg_hash, private_key=self._private_key)
return {
constants.HEADER_PUBKEY: self._address,
Expand Down
21 changes: 3 additions & 18 deletions packages/sync/octobot_sync/chain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,18 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library.

from octobot_sync.chain import interface
from octobot_sync.chain.interface import (
AbstractChain,
Item,
Wallet,
)

from octobot_sync.chain import evm
from octobot_sync.chain.evm import (
EvmChain,
Wallet,
eip191_hash,
create_evm_wallet,
address_from_evm_key,
verify_evm,
)

from octobot_sync.chain import registry
from octobot_sync.chain.registry import (
ChainRegistry,
)

__all__ = [
"AbstractChain",
"Item",
"Wallet",
"EvmChain",
"eip191_hash",
"create_evm_wallet",
"address_from_evm_key",
"verify_evm",
"ChainRegistry",
]
124 changes: 10 additions & 114 deletions packages/sync/octobot_sync/chain/evm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,67 +14,27 @@
# You should have received a copy of the GNU General Public
# License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.

import functools
from dataclasses import dataclass

from cachetools import TTLCache
from web3 import Web3, AsyncWeb3
from web3.providers import AsyncHTTPProvider
from web3 import Web3

import octobot_sync.chain.interface as chain_interface

_SENTINEL = object()
@dataclass
class Wallet:
private_key: str
address: str


def _async_ttl_cached(ttl_s: float, maxsize: int = 1024):
def decorator(fn):
cache = TTLCache(maxsize=maxsize, ttl=ttl_s)

@functools.wraps(fn)
async def wrapper(*args, **kwargs):
key = args[1:] # skip self
result = cache.get(key, _SENTINEL)
if result is not _SENTINEL:
return result
result = await fn(*args, **kwargs)
cache[key] = result
return result

wrapper.cache = cache
return wrapper

return decorator

OCTOBOT_PRODUCT_ABI = [
{
"type": "function",
"name": "ownerOf",
"inputs": [{"name": "tokenId", "type": "uint256"}],
"outputs": [{"name": "owner", "type": "address"}],
"stateMutability": "view",
},
{
"type": "function",
"name": "hasAccess",
"inputs": [
{"name": "user", "type": "address"},
{"name": "itemId", "type": "uint256"},
],
"outputs": [{"name": "", "type": "bool"}],
"stateMutability": "view",
},
]


def _eip191_hash(text: str) -> bytes:
def eip191_hash(text: str) -> bytes:
"""Compute the EIP-191 personal_sign message hash without eth_account."""
msg_bytes = text.encode("utf-8")
prefix = f"\x19Ethereum Signed Message:\n{len(msg_bytes)}".encode("utf-8")
return Web3.keccak(prefix + msg_bytes)


def create_evm_wallet() -> chain_interface.Wallet:
def create_evm_wallet() -> Wallet:
account = Web3().eth.account.create()
return chain_interface.Wallet(
return Wallet(
private_key=account.key.hex(),
address=account.address,
)
Expand All @@ -87,72 +47,8 @@ def address_from_evm_key(private_key: str) -> str:
def verify_evm(canonical: str, signature: str, address: str) -> bool:
"""Verify an EIP-191 personal_sign signature via web3."""
try:
msg_hash = _eip191_hash(canonical)
msg_hash = eip191_hash(canonical)
recovered = Web3().eth.account._recover_hash(msg_hash, signature=signature)
return recovered.lower() == address.lower()
except Exception:
return False



class EvmChain:
def __init__(
self,
chain_id: str,
rpc_url: str | None = None,
contract_address: str | None = None,
) -> None:
self._id = chain_id
if rpc_url and contract_address:
self._contract_address = AsyncWeb3.to_checksum_address(contract_address)
self._w3 = AsyncWeb3(AsyncHTTPProvider(rpc_url))
self._contract = self._w3.eth.contract(
address=self._contract_address, abi=OCTOBOT_PRODUCT_ABI
)
else:
self._contract = None

@property
def id(self) -> str:
return self._id

@staticmethod
def create_wallet() -> chain_interface.Wallet:
return create_evm_wallet()

@staticmethod
def address_from_key(private_key: str) -> str:
return address_from_evm_key(private_key)

async def verify_signature(
self, canonical: str, signature: str, pubkey_or_address: str
) -> bool:
return verify_evm(canonical, signature, pubkey_or_address)

def _require_contract(self) -> None:
if self._contract is None:
raise RuntimeError(
f"Chain {self._id}: RPC not configured. "
"Set EVM_BASE_RPC and EVM_CONTRACT_BASE environment variables."
)

@_async_ttl_cached(ttl_s=30)
async def get_item(self, item_id: str) -> chain_interface.Item | None:
self._require_contract()
try:
owner = await self._contract.functions.ownerOf(int(item_id)).call()
return chain_interface.Item(id=item_id, owner=owner)
except Exception:
return None

@_async_ttl_cached(ttl_s=365 * 86400)
async def is_item_owner(self, item_id: str, pubkey_or_address: str) -> bool:
item = await self.get_item(item_id)
return item is not None and item.owner.lower() == pubkey_or_address.lower()

@_async_ttl_cached(ttl_s=60)
async def has_access(self, item_id: str, user_address: str) -> bool:
self._require_contract()
return await self._contract.functions.hasAccess(
AsyncWeb3.to_checksum_address(user_address), int(item_id)
).call()
51 changes: 0 additions & 51 deletions packages/sync/octobot_sync/chain/interface.py

This file was deleted.

34 changes: 0 additions & 34 deletions packages/sync/octobot_sync/chain/registry.py

This file was deleted.

Loading
Loading