From fa91cea572d3a0773429b53c376d6183e4e08ff5 Mon Sep 17 00:00:00 2001 From: Arthur Guillon Date: Mon, 11 Mar 2024 14:26:37 +0100 Subject: [PATCH 1/5] Remove demo/ and permit-cameligo/ clutter --- demo.ipynb | 477 ------------------------------------ demo/demo.py | 252 ------------------- demo/staking-contract.mligo | 36 --- permit-cameligo | 1 - 4 files changed, 766 deletions(-) delete mode 100644 demo.ipynb delete mode 100644 demo/demo.py delete mode 100644 demo/staking-contract.mligo delete mode 160000 permit-cameligo diff --git a/demo.ipynb b/demo.ipynb deleted file mode 100644 index 5f37771..0000000 --- a/demo.ipynb +++ /dev/null @@ -1,477 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "422ed9fb", - "metadata": {}, - "outputs": [], - "source": [ - "import requests\n", - "import json\n", - "\n", - "import pytezos as ptz\n", - "from pytezos import Key\n", - "\n", - "import demo.demo as demo" - ] - }, - { - "cell_type": "markdown", - "id": "bf30339a", - "metadata": {}, - "source": [ - "We're using https://packages.ligolang.org/contract/Permit-Cameligo which itself extends `ligo-extendable-fa2` to add a Permit implementation." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "ec0b026a", - "metadata": {}, - "outputs": [], - "source": [ - "!ligo compile contract permit-cameligo/src/main.mligo > permit-contract.tz" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "ca997544", - "metadata": {}, - "outputs": [], - "source": [ - "!ligo compile contract demo/staking-contract.mligo > staking-contract.tz" - ] - }, - { - "cell_type": "markdown", - "id": "aa0d983b", - "metadata": {}, - "source": [ - "Make sure the API is running by visiting http://127.0.0.1:8000/docs. The API is written in Python using FastAPI and can be started with `uvicorn src.main:app --reload`." - ] - }, - { - "cell_type": "markdown", - "id": "70d2bcde", - "metadata": {}, - "source": [ - "# Deploying the contracts" - ] - }, - { - "cell_type": "markdown", - "id": "7d9ff830", - "metadata": {}, - "source": [ - "We're using a local network for this demo (typically using Flextesa), but this has been tested on Ghostnet as well. On Ghostnet, operations and balance changes seem to be correctly picked by the indexers, even for a non-revealed account." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "7faf16d9", - "metadata": {}, - "outputs": [], - "source": [ - "TEZOS_RPC = \"https://ghostnet.tezos.marigold.dev/\"" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "8083ed76", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Decimal('2173.513094')" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "admin_key = Key.from_encoded_key(\"edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq\")\n", - "admin_key.public_key_hash()\n", - "admin = ptz.pytezos.using(TEZOS_RPC, admin_key)\n", - "admin.balance()" - ] - }, - { - "cell_type": "markdown", - "id": "ef5fd066", - "metadata": {}, - "source": [ - "`admin` is a PyTezosClient object. It can query Tezos RPCs, inspect contracts' storage and send operations. This key is used in the API as well, and this is the one doing all the requests for the accounts we're going to define.\n", - "\n", - "Its address is `tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb` and is also known as “alice” among Tezos developers." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "1467f406", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'KT1FkUTvJxzPMGNFkD8ccrjESKWAqvkzUPr4'" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "nft_contract = demo.deploy_permit(admin)\n", - "nft_contract.address" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "9c11ac9b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'extension': {'admin': 'tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb',\n", - " 'counter': 0,\n", - " 'default_expiry': 3600,\n", - " 'extension': 358755,\n", - " 'max_expiry': 3600,\n", - " 'permit_expiries': 358756,\n", - " 'permits': 358757,\n", - " 'user_expiries': 358758},\n", - " 'ledger': 358759,\n", - " 'metadata': 358760,\n", - " 'operators': 358761,\n", - " 'token_metadata': 358762}" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "nft_contract.storage()" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "7cc65629", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'KT1WNbUViQ36Z2cNkYxpNPw6N9iLLMMufWos'" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "staking_contract = demo.deploy_staking_contract(admin, nft_contract)\n", - "staking_contract.address" - ] - }, - { - "cell_type": "markdown", - "id": "512556ff", - "metadata": {}, - "source": [ - "# Minting NFTs for three new accounts" - ] - }, - { - "cell_type": "markdown", - "id": "73be3804", - "metadata": {}, - "source": [ - "The `demo/demo.py` file contains all the code necessary for the demo: generation of off-chain permits, of the hashes and the signatures, and communication with the API." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "66f72979", - "metadata": {}, - "outputs": [], - "source": [ - "demo = demo.Demo(nft_contract, staking_contract)" - ] - }, - { - "cell_type": "markdown", - "id": "facafefb", - "metadata": {}, - "source": [ - "We generate 3 test accounts. None of them own any tez, and they cannot post transactions on Tezos themselves." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "15585dfa", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['tz1L2b5AvKpBB7HzguFhFWCwgdyGa3XiDTQB',\n", - " 'tz1SVfv1WRDgx6KFWLS6AbEgQbJVgvjjSAFU',\n", - " 'tz1gN3oDaxEx6QcnqzwpnPUTkPBLWbXTEtN6']" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "keys = [Key.generate() for i in range(3)]\n", - "senders = [k.public_key_hash() for k in keys]\n", - "senders" - ] - }, - { - "cell_type": "markdown", - "id": "fb08d604", - "metadata": {}, - "source": [ - "We call the API to request it to mint 100 tokens to all these accounts. These calls are done in parallel, but the API automatically groups them in a single transaction." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "9b80b6b2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "calling http://127.0.0.1:8000/operation with sender=tz1L2b5AvKpBB7HzguFhFWCwgdyGa3XiDTQB\n", - "calling http://127.0.0.1:8000/operation with sender=tz1SVfv1WRDgx6KFWLS6AbEgQbJVgvjjSAFU\n", - "calling http://127.0.0.1:8000/operation with sender=tz1gN3oDaxEx6QcnqzwpnPUTkPBLWbXTEtN6\n", - "\n", - "\n", - "\n" - ] - } - ], - "source": [ - "_ = demo.mint_requests(senders)" - ] - }, - { - "cell_type": "markdown", - "id": "12e37965", - "metadata": {}, - "source": [ - "We can inspect the NFT ledger to see that all the addresses received tokens." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "653dc0cb", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "tz1L2b5AvKpBB7HzguFhFWCwgdyGa3XiDTQB owns 100 tokens.\n", - "tz1SVfv1WRDgx6KFWLS6AbEgQbJVgvjjSAFU owns 100 tokens.\n", - "tz1gN3oDaxEx6QcnqzwpnPUTkPBLWbXTEtN6 owns 100 tokens.\n" - ] - } - ], - "source": [ - "for sender in senders:\n", - " minted = nft_contract.storage[\"ledger\"][(sender, 0)]()\n", - " print(f\"{sender} owns {minted} tokens.\")" - ] - }, - { - "cell_type": "markdown", - "id": "16e523f9", - "metadata": {}, - "source": [ - "# Transferring the NFTs by signing off-chain permits" - ] - }, - { - "cell_type": "markdown", - "id": "c079dd7d", - "metadata": {}, - "source": [ - "In this section, we're going to send 10 tokens to the “staking” contract. This contract does nothing special except from receiving the 10 tokens and saving the staker's address in its own storage. \n", - "\n", - "To be more precise, when we call `stake(10, address)`, the staking contract emits a new `transfer` operation, which is the one for which we signed the permit. If this permit wasn't signed, the whole transaction would fail." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "48a32e2c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\n", - "\n", - "Properties\n", - ".key\t\ttz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb\n", - ".shell\t\t['https://ghostnet.tezos.marigold.dev/']\n", - ".address\tKT1JqBcEbDwrZym6419eCi61sHezm4WurhsZ\n", - ".block_id\thead\n", - ".entrypoint\tstake\n", - "\n", - "Builtin\n", - "(*args, **kwargs)\t# build transaction parameters (see typedef)\n", - "\n", - "Typedef\n", - "$stake:\n", - "\t( nat, address )\n", - "\n", - "$nat:\n", - "\tint /* Natural number */\n", - "\n", - "$address:\n", - "\tstr /* Base58 encoded `tz` or `KT` address */\n", - "\n", - "\n", - "Helpers\n", - ".decode()\n", - ".encode()" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "staking_contract.stake" - ] - }, - { - "cell_type": "markdown", - "id": "4af84e70", - "metadata": {}, - "source": [ - "We can now sign the permits off-chain. This is the most complicated part (hidden inside the `Demo` class), as it involves hashing a few structures related to the contracts' entrypoints, and signing them. We're in the process of building SDKs for this.\n", - "\n", - "The hashes are then signed by each key (off-chain), and the permits can be posted on-chain by any account. They are then stored in the NFT contract." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "1d46fbf1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "calling http://127.0.0.1:8000/operation with sender=tz1L2b5AvKpBB7HzguFhFWCwgdyGa3XiDTQB\n", - "calling http://127.0.0.1:8000/operation with sender=tz1SVfv1WRDgx6KFWLS6AbEgQbJVgvjjSAFU\n", - "calling http://127.0.0.1:8000/operation with sender=tz1gN3oDaxEx6QcnqzwpnPUTkPBLWbXTEtN6\n", - "\n", - "\n", - "\n" - ] - } - ], - "source": [ - "_ = demo.permit_requests(keys)" - ] - }, - { - "cell_type": "markdown", - "id": "f4fa6743", - "metadata": {}, - "source": [ - "However, note that this demo uses a contract limited to a single, global counter; for technical reasons, **only one of these permits is valid** and the two others will be refused by the API.\n", - "\n", - "In production, we recommend using a more flexible smart contract. We leave this one in the demo to show that the API is somewhat robust to invalid transactions." - ] - }, - { - "cell_type": "markdown", - "id": "0417eca9", - "metadata": {}, - "source": [ - "Finally, we can stake the 10 tokens. Again, this call could be posted by any account, not necessarily the API. As we don't know which user successfully posted a permit, we try all three." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dcc9b456", - "metadata": {}, - "outputs": [], - "source": [ - "for key in keys:\n", - " demo.stake_request(key.public_key_hash())" - ] - }, - { - "cell_type": "markdown", - "id": "cb325b59", - "metadata": {}, - "source": [ - "But we can check that the tokens have indeed been transfered to the staking contract:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dcc8d944", - "metadata": {}, - "outputs": [], - "source": [ - "nft_contract.storage[\"ledger\"][(staking_contract.address, 0)]()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.8" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/demo/demo.py b/demo/demo.py deleted file mode 100644 index 87dd1e5..0000000 --- a/demo/demo.py +++ /dev/null @@ -1,252 +0,0 @@ -import json -import requests -import asyncio - -from pytezos.contract.interface import ContractInterface -from pytezos.michelson.types import MichelsonType -from pytezos.crypto.key import blake2b_32 -import pytezos - -def tezos_hex(s): - return f"0x{bytes(s, 'utf-8').hex()}" - - -# TODO: could be part of the Python library -def deploy_permit(admin): - fa2_contract = ContractInterface.from_file("permit-contract.tz") - fa2_initial_storage = fa2_contract.storage.dummy() - fa2_initial_storage["metadata"] = { - "": tezos_hex("tezos-storage:m"), - "m": json.dumps({ - "name": "Weapons", - "interfaces": ["TZIP-12"] - }).encode("utf-8") - } - - fa2_initial_storage["token_metadata"] = { - 0: { - "token_id": 0, - "token_info": { - "name": tezos_hex("Rusty sword"), - "description": tezos_hex("a rusty sword"), - "interfaces": tezos_hex(json.dumps(["TZIP-12"])), - "symbol": tezos_hex("TCK1"), - "decimals": tezos_hex("0"), - "thumbnailUri": tezos_hex("ipfs://QmdPmCiyNRUJiWAzRcd2UMUAnpFDhZVxujvPHg1xCx1GZb"), - "displayUri": tezos_hex("ipfs://QmdPmCiyNRUJiWAzRcd2UMUAnpFDhZVxujvPHg1xCx1GZb"), - "artifactUri": tezos_hex("ipfs://QmdPmCiyNRUJiWAzRcd2UMUAnpFDhZVxujvPHg1xCx1GZb") - } - }, - 1: { - "token_id": 1, - "token_info": { - "name": tezos_hex("Pirate sword"), - "description": tezos_hex("a pirate sword"), - "interfaces": tezos_hex(json.dumps(["TZIP-12"])), - "symbol": tezos_hex("TCK1"), - "decimals": tezos_hex("0"), - "thumbnailUri": tezos_hex("ipfs://QmPdwAqgBhM65fzzYT4FQmCELAej8Mei6QQZiBZ93M4Acq"), - "displayUri": tezos_hex("ipfs://QmPdwAqgBhM65fzzYT4FQmCELAej8Mei6QQZiBZ93M4Acq"), - "artifactUri": tezos_hex("ipfs://QmPdwAqgBhM65fzzYT4FQmCELAej8Mei6QQZiBZ93M4Acq") - } - }, - 2: { - "token_id": 2, - "token_info": { - "name": tezos_hex("Small knife"), - "description": tezos_hex("a small knife"), - "interfaces": tezos_hex(json.dumps(["TZIP-12"])), - "symbol": tezos_hex("TCK1"), - "decimals": tezos_hex("0"), - "thumbnailUri": tezos_hex("ipfs://QmV9RAPyo4xnPQG3qJLjHMmxJBjLWyyPKLiNdkzcmNB31c"), - "displayUri": tezos_hex("ipfs://QmV9RAPyo4xnPQG3qJLjHMmxJBjLWyyPKLiNdkzcmNB31c"), - "artifactUri": tezos_hex("ipfs://QmV9RAPyo4xnPQG3qJLjHMmxJBjLWyyPKLiNdkzcmNB31c") - } - }, - 3: { - "token_id": 3, - "token_info": { - "name": tezos_hex("Frying pan"), - "description": tezos_hex("a frying pan"), - "interfaces": tezos_hex(json.dumps(["TZIP-12"])), - "symbol": tezos_hex("TCK1"), - "decimals": tezos_hex("0"), - "thumbnailUri": tezos_hex("ipfs://QmWFiyrm9byfefPFAhsUdAQg6Q42CMj1ux8Jwo4fECRjNw"), - "displayUri": tezos_hex("ipfs://QmWFiyrm9byfefPFAhsUdAQg6Q42CMj1ux8Jwo4fECRjNw"), - "artifactUri": tezos_hex("ipfs://QmWFiyrm9byfefPFAhsUdAQg6Q42CMj1ux8Jwo4fECRjNw") - } - }, - 4: { - "token_id": 4, - "token_info": { - "name": tezos_hex("Slingshot"), - "description": tezos_hex("a slingshot"), - "interfaces": tezos_hex(json.dumps(["TZIP-12"])), - "symbol": tezos_hex("TCK1"), - "decimals": tezos_hex("0"), - "thumbnailUri": tezos_hex("ipfs://QmQZuirmonLR7uPFCfidX6vELLP6CLmD657qUdQUij8RhG"), - "displayUri": tezos_hex("ipfs://QmQZuirmonLR7uPFCfidX6vELLP6CLmD657qUdQUij8RhG"), - "artifactUri": tezos_hex("ipfs://QmQZuirmonLR7uPFCfidX6vELLP6CLmD657qUdQUij8RhG") - } - }, - 5: { - "token_id": 5, - "token_info": { - "name": tezos_hex("Claymore"), - "description": tezos_hex("a claymore"), - "interfaces": tezos_hex(json.dumps(["TZIP-12"])), - "symbol": tezos_hex("TCK1"), - "decimals": tezos_hex("0"), - "thumbnailUri": tezos_hex("ipfs://QmQYwBm31CJzAyGfYajLMULQHZUUYSrfAzJZgjZ7XurZZe"), - "displayUri": tezos_hex("ipfs://QmQYwBm31CJzAyGfYajLMULQHZUUYSrfAzJZgjZ7XurZZe"), - "artifactUri": tezos_hex("ipfs://QmQYwBm31CJzAyGfYajLMULQHZUUYSrfAzJZgjZ7XurZZe") - } - } - } - - fa2_initial_storage["extension"]["extension"] = { - i: 0 for i in range(len(fa2_initial_storage["token_metadata"])) - } - - fa2_initial_storage["extension"]["admin"] = admin.key.public_key_hash() - fa2_initial_storage["extension"]["max_expiry"] = 3600 - fa2_initial_storage["extension"]["default_expiry"] = 3600 - orig = admin.origination(fa2_contract.script( - initial_storage=fa2_initial_storage) - ).autofill().sign().inject(min_confirmations=1) - nft_contract_address = orig["contents"][0]["metadata"]["operation_result"]["originated_contracts"][0] - nft_contract = admin.contract(nft_contract_address) - return nft_contract - - -def deploy_staking_contract(admin, nft_contract): - staking_contract = ContractInterface.from_file("staking-contract.tz") - staking_storage = staking_contract.storage.dummy() - staking_storage["nft_address"] = nft_contract.address - orig = admin.origination( - staking_contract.script(initial_storage=staking_storage) - ).autofill().sign().inject(min_confirmations=1) - staking_address = orig["contents"][0]["metadata"]["operation_result"]["originated_contracts"][0] - return admin.contract(staking_address) - - -class Demo: - def __init__(self, nft_contract, staking_contract, - api_url="http://127.0.0.1:8000/operation"): - self.nft_contract = nft_contract - self.staking_contract = staking_contract - self.api_url = api_url - - def post_op(self, op, sender): - print(f"calling {self.api_url} with sender={sender}") - req = requests.post( - self.api_url, - json={ - "sender": sender, - "operations": op.contents - } - ) - return req - - # This functions is meant to be called from a notebook. - def make_requests(self, func, args): - async def main(): - loop = asyncio.get_event_loop() - futures = [ - loop.run_in_executor( - None, - func, - *arg - ) - for arg in args - ] - for response in await asyncio.gather(*futures): - print(response) - - loop = asyncio.get_event_loop() - return loop.create_task(main()) - - def generate_mint(self, owner): - return self.nft_contract.mint_token([{ - "owner": owner, - "token_id": 0, - "amount_": 100 - }]).as_transaction() - - def mint_requests(self, owners): - mint_ops = [(self.generate_mint(owner), owner) for owner in owners] - return self.make_requests(self.post_op, mint_ops) - - def permit_requests(self, keys): - permits = [(self.generate_permit(key), key.public_key_hash()) - for key in keys] - return self.make_requests(self.post_op, permits) - - def stake_request(self, sender, qty=10): - staking_op = self.staking_contract.stake(qty, sender).as_transaction() - req = requests.post( - self.api_url, - json={ - "sender": sender, - "operations": staking_op.contents - } - ) - return req - - # TODO: what happens if transfer is < 10? - def generate_permit(self, key, qty=10): - transfer = self.nft_contract.transfer([{ - "from_": key.public_key_hash(), - "txs": [{ - "to_": self.staking_contract.address, - "token_id": 0, - "amount": 10 - }] - }]) - - matcher = MichelsonType.match( - self.nft_contract.entrypoints["transfer"].as_micheline_expr()["args"][0] - ) - micheline_encoded = matcher.from_micheline_value( - transfer.parameters["value"][0]["args"] - ) - transfer_hash = blake2b_32(micheline_encoded.pack()).hexdigest() - permit_hashed_type = { - 'prim': 'pair', - 'args': [ - { - 'prim': 'pair', - 'args': [ - {'prim': 'chain_id'}, - {'prim': 'address'} - ] - }, - { - 'prim': 'pair', - 'args': [ - {'prim': 'int'}, - {'prim': 'bytes'} - ] - } - - ] - } - permit_hashed_args = [ - [ - {"string": self.nft_contract.shell.block()["chain_id"]}, - {"string": self.nft_contract.address} - ], - [ - {"int": self.nft_contract.storage()["extension"]["counter"]}, - {"bytes": transfer_hash} - ] - ] - matcher2 = MichelsonType.match(permit_hashed_type) - permit_hash = matcher2.from_micheline_value(permit_hashed_args).pack() - permit_signature = key.sign(permit_hash) - permit_op = self.nft_contract.permit([( - key.public_key(), - permit_signature, - transfer_hash - )]).as_transaction() - return permit_op diff --git a/demo/staking-contract.mligo b/demo/staking-contract.mligo deleted file mode 100644 index c8555da..0000000 --- a/demo/staking-contract.mligo +++ /dev/null @@ -1,36 +0,0 @@ -#import "../permit-cameligo/src/main.mligo" "FA2" - -type storage = - { - nft_address : address; - staked : (address, nat) big_map - } - -(* We need to provide the address of the NFT's owner so that the transfer can be done by someone - * else (we don't rely on Tezos.get_sender ()) *) - -[@entry] -let stake (qty, sender: nat * address) (storage: storage): operation list * storage = - let staked = match Big_map.find_opt sender storage.staked with - | None -> Big_map.add sender qty storage.staked - | Some n -> Big_map.update sender (Some (n + qty)) storage.staked - in - let contract = match (Tezos.get_contract_opt storage.nft_address: FA2.parameter contract option) with - | None -> failwith "Invalid NFT contract" - | Some contract -> contract - in - let transfer = [{ - from_ = sender; - txs = [{ - to_ = Tezos.get_self_address (); - token_id = 0n; - amount = qty; - }] - }] - in - let op = Tezos.transaction (Transfer transfer: FA2.parameter) 0tez contract in - [op], { storage with staked=staked } - -[@entry] -let unstake (_sender: address) (_storage: storage): operation list * storage = - failwith "Not implemented" diff --git a/permit-cameligo b/permit-cameligo deleted file mode 160000 index 6c7fb30..0000000 --- a/permit-cameligo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6c7fb3065ac9f917d9c51532dceba583587a240c From c7b3e6ad671b4ff3d6696a61358aefcab7d5f8a8 Mon Sep 17 00:00:00 2001 From: Arthur Guillon Date: Tue, 12 Mar 2024 15:06:09 +0100 Subject: [PATCH 2/5] Limit waiting time to confirm a deposit --- src/tezos.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/tezos.py b/src/tezos.py index 6e8f5ea..c973ae4 100644 --- a/src/tezos.py +++ b/src/tezos.py @@ -82,15 +82,20 @@ def check_credits(db, estimated_fees): async def confirm_deposit(tx_hash, payer, amount: Union[int, str]): receiver = ptz.key.public_key_hash() - op_result = await find_transaction(tx_hash) - return any( - op - for op in op_result["contents"] - if op["kind"] == "transaction" - and op["source"] == payer - and op["destination"] == receiver - and int(op["amount"]) == int(amount) - ) + block_time = int(constants["minimal_block_delay"]) + try: + async with asyncio.timeout(2*block_time): + op_result = await find_transaction(tx_hash) + return any( + op + for op in op_result["contents"] + if op["kind"] == "transaction" + and op["source"] == payer + and op["destination"] == receiver + and int(op["amount"]) == int(amount) + ) + except TimeoutError: + return False async def confirm_withdraw(tx_hash, db, user_id, withdraw): From 79ccddbd90d643ae9eadb93be173074b71f315b5 Mon Sep 17 00:00:00 2001 From: Arthur Guillon Date: Tue, 12 Mar 2024 15:09:23 +0100 Subject: [PATCH 3/5] Get the API Tezos account from the API --- src/routes.py | 5 ++++- src/tezos.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/routes.py b/src/routes.py index 1522229..6873a79 100644 --- a/src/routes.py +++ b/src/routes.py @@ -30,7 +30,10 @@ # Healthcheck @router.get("/") async def root(): - return {"message": "Hello World"} + return { + "status": "healthy", + "tezos_address": tezos.public_address + } # POST endpoints diff --git a/src/tezos.py b/src/tezos.py index c973ae4..d4404c7 100644 --- a/src/tezos.py +++ b/src/tezos.py @@ -17,6 +17,7 @@ ), "Could not read secret key" admin_key = pytezos.pytezos.key.from_encoded_key(config.SECRET_KEY) +public_address = admin_key.public_key_hash() ptz = pytezos.pytezos.using(config.TEZOS_RPC, admin_key) log.info(f"API address is {ptz.key.public_key_hash()}") constants = ptz.shell.block.context.constants() From d2e8cf21ff4ed675cbeec3191d5473301fc4c28d Mon Sep 17 00:00:00 2001 From: Arthur Guillon Date: Tue, 12 Mar 2024 18:48:55 +0100 Subject: [PATCH 4/5] Change MAX_SPONSEE conditions: now limits the number of operations users can send, after the condition has been created. --- api-usage.ipynb | 621 ++++++++++++++++++++++++++++++++++++++++++++++++ src/crud.py | 78 +++--- src/models.py | 18 +- src/routes.py | 10 +- src/schemas.py | 13 +- 5 files changed, 689 insertions(+), 51 deletions(-) create mode 100644 api-usage.ipynb diff --git a/api-usage.ipynb b/api-usage.ipynb new file mode 100644 index 0000000..07db467 --- /dev/null +++ b/api-usage.ipynb @@ -0,0 +1,621 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "441aaa0c", + "metadata": {}, + "outputs": [], + "source": [ + "import pytezos\n", + "from pytezos.contract.interface import ContractInterface" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0c834381", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import json\n", + "from dotenv import load_dotenv\n", + "import os" + ] + }, + { + "cell_type": "markdown", + "id": "799f697b", + "metadata": {}, + "source": [ + "# Testing Gas station API" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "60855c5c", + "metadata": {}, + "outputs": [], + "source": [ + "gas_station_api = \"http://localhost:8000\"\n", + "contract_address = \"KT1HUdxmgZUw21ED9gqELVvCty5d1ff41p7J\" # permit NFT contract on Ghostnet" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0d1e9f63", + "metadata": {}, + "outputs": [], + "source": [ + "alice_key = \"edskRpm2mUhvoUjHjXgMoDRxMKhtKfww1ixmWiHCWhHuMEEbGzdnz8Ks4vgarKDtxok7HmrEo1JzkXkdkvyw7Rtw6BNtSd7MJ7\"\n", + "alice = pytezos.Key.from_encoded_key(alice_key)\n", + "ptz = pytezos.pytezos.using(\"https://ghostnet.tezos.marigold.dev\", alice)" + ] + }, + { + "cell_type": "markdown", + "id": "b227d685", + "metadata": {}, + "source": [ + "We assume that alice has not been already registered as a user. If she was, skip this." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "23eff5ca", + "metadata": {}, + "outputs": [], + "source": [ + "r = requests.post(\n", + " f\"{gas_station_api}/users\",\n", + " json = {\n", + " \"address\": alice.public_key_hash(),\n", + " \"name\": \"alice\"\n", + " }\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "2e5d1f47", + "metadata": {}, + "outputs": [], + "source": [ + "assert r.status_code == 200" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "edb869bd", + "metadata": {}, + "outputs": [], + "source": [ + "r = requests.get(f\"{gas_station_api}/users/{alice.public_key_hash()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e1596858", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'address': 'tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb',\n", + " 'id': '06d44229-4b75-4df5-9bac-df3b53285859',\n", + " 'name': 'alice',\n", + " 'withdraw_counter': 0}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "alice_user = r.json()\n", + "alice_user" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cec218c1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': '958caaa8-ed25-4c26-a062-42bd78182399',\n", + " 'amount': 0,\n", + " 'owner_id': '06d44229-4b75-4df5-9bac-df3b53285859'}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "r = requests.get(f\"{gas_station_api}/credits/{alice_user['address']}\")\n", + "credits = r.json()[0]\n", + "assert r.status_code == 200\n", + "credits" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "bf71169b", + "metadata": {}, + "outputs": [], + "source": [ + "register_contract = {\n", + " \"address\": contract_address,\n", + " \"owner_id\": alice_user[\"id\"],\n", + " \"name\": \"NFT1\",\n", + " \"entrypoints\": [\n", + " {\n", + " \"name\": \"mint_token\",\n", + " \"is_enabled\": True\n", + " },\n", + " {\n", + " \"name\": \"transfer\",\n", + " \"is_enabled\": True\n", + " },\n", + " {\n", + " \"name\": \"permit\",\n", + " \"is_enabled\": True\n", + " }\n", + " ],\n", + " \"credit_id\": credits[\"id\"]\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "ecc30c7a", + "metadata": {}, + "outputs": [], + "source": [ + "r = requests.post(\n", + " f\"{gas_station_api}/contracts\",\n", + " json = register_contract\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "3a285211", + "metadata": {}, + "outputs": [], + "source": [ + "assert r.status_code == 200" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "a2d993c0", + "metadata": {}, + "outputs": [], + "source": [ + "api_info = requests.get(\n", + " f\"{gas_station_api}/\"\n", + ").json()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "729037fe", + "metadata": {}, + "outputs": [], + "source": [ + "tr = ptz.transaction(destination=api_info[\"tezos_address\"], amount=int(1e6)).send()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "27753cbf", + "metadata": {}, + "outputs": [], + "source": [ + "credits_deposit = {\n", + " \"id\": credits[\"id\"],\n", + " \"amount\": int(1e6),\n", + " \"operation_hash\": tr.hash(),\n", + " \"owner_id\": alice_user[\"id\"]\n", + "}\n", + "r = requests.put(\n", + " f\"{gas_station_api}/deposit\",\n", + " json = credits_deposit\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "bcba5181", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "200" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "r.status_code" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "6d9af87b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'id': '958caaa8-ed25-4c26-a062-42bd78182399',\n", + " 'amount': 1000000,\n", + " 'owner_id': '06d44229-4b75-4df5-9bac-df3b53285859'}]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "r = requests.get(\n", + " f\"{gas_station_api}/credits/{alice_user['id']}\"\n", + ")\n", + "r.json()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "a83d8047", + "metadata": {}, + "outputs": [], + "source": [ + "ct1 = ptz.contract(contract_address)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "eabaebdc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "14\n" + ] + } + ], + "source": [ + "try:\n", + " print(ct1.storage[\"ledger\"][(\"tz1UtRfZGHjuHe5PJ7QpksEnn1K7BMJ4qVok\", 2)]())\n", + "except:\n", + " print(0)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "fd1734cd", + "metadata": {}, + "outputs": [], + "source": [ + "op_content = ct1.mint_token([{\n", + " \"owner\": \"tz1UtRfZGHjuHe5PJ7QpksEnn1K7BMJ4qVok\",\n", + " \"token_id\": 2,\n", + " \"amount_\": 1\n", + "}]).as_transaction().contents" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "140b1471", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "200" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "operation = {\n", + " \"sender_address\": \"tz1UtRfZGHjuHe5PJ7QpksEnn1K7BMJ4qVok\",\n", + " \"operations\": op_content\n", + "}\n", + "r = requests.post(\n", + " f\"{gas_station_api}/operation\",\n", + " json=operation\n", + ")\n", + "r.status_code" + ] + }, + { + "cell_type": "markdown", + "id": "476f9bd6", + "metadata": {}, + "source": [ + "Once the API has received the operation, we check the balance of the user for this token. After a while, it's showing as updated." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "8ff9a023", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "15\n" + ] + } + ], + "source": [ + "try:\n", + " print(ct1.storage[\"ledger\"][(\"tz1UtRfZGHjuHe5PJ7QpksEnn1K7BMJ4qVok\", 2)]())\n", + "except:\n", + " print(0)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "5508e9bc", + "metadata": {}, + "outputs": [], + "source": [ + "r = requests.get(\n", + " f\"{gas_station_api}/contracts/{contract_address}\"\n", + ")\n", + "contract_db = r.json()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "0cbecd7f", + "metadata": {}, + "outputs": [], + "source": [ + "r = requests.post(\n", + " f\"{gas_station_api}/condition\",\n", + " json={\n", + " \"type\": \"MAX_CALLS_PER_SPONSEE\",\n", + " \"contract_id\": contract_db[\"id\"],\n", + " \"entrypoint_id\": None,\n", + " \"vault_id\": credits[\"id\"],\n", + " \"max\": 1\n", + " }\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "33cbdddf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "r" + ] + }, + { + "cell_type": "markdown", + "id": "b39438f0", + "metadata": {}, + "source": [ + "Now that we have a condition, new users can use the service only a limited number of times. The first time we call `mint_token` with the same address, everything should work, as the condition is not retroactive." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "202834f6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "200" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "operation = {\n", + " \"sender_address\": \"tz1UtRfZGHjuHe5PJ7QpksEnn1K7BMJ4qVok\",\n", + " \"operations\": op_content\n", + "}\n", + "r = requests.post(\n", + " f\"{gas_station_api}/operation\",\n", + " json=operation\n", + ")\n", + "r.status_code" + ] + }, + { + "cell_type": "markdown", + "id": "0d60d440", + "metadata": {}, + "source": [ + "But the second time it will fail:" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "b34ffa8d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "400" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "operation = {\n", + " \"sender_address\": \"tz1UtRfZGHjuHe5PJ7QpksEnn1K7BMJ4qVok\",\n", + " \"operations\": op_content\n", + "}\n", + "r = requests.post(\n", + " f\"{gas_station_api}/operation\",\n", + " json=operation\n", + ")\n", + "r.status_code" + ] + }, + { + "cell_type": "markdown", + "id": "4398dcad", + "metadata": {}, + "source": [ + "However, for another user it still works. Note that we only changed sender_address, and kept op_content the same, so the user receiving the new NFT is still the same." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "166eb0a0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "200" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "operation = {\n", + " \"sender_address\": \"tz1aH7Hj1s95wkPjCbMr2RMgTPq4RPHE1LLU\",\n", + " \"operations\": op_content\n", + "}\n", + "r = requests.post(\n", + " f\"{gas_station_api}/operation\",\n", + " json=operation\n", + ")\n", + "r.status_code" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "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", + " 'entrypoint_id': None,\n", + " 'max': 1,\n", + " 'current': 2,\n", + " 'is_active': True}]" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "requests.get(\n", + " f\"{gas_station_api}/condition/{credits['id']}\"\n", + ").json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01e26527", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/crud.py b/src/crud.py index 3b7d98c..be73a5c 100644 --- a/src/crud.py +++ b/src/crud.py @@ -254,7 +254,7 @@ def get_credits_from_contract_address(db: Session, contract_address: str): def create_operation(db: Session, operation: schemas.CreateOperation): db_operation = models.Operation( **{ - "user_address": operation.user_address, + "sender_address": operation.sender_address, "contract_id": operation.contract_id, "entrypoint_id": operation.entrypoint_id, "hash": operation.hash, @@ -317,7 +317,7 @@ def create_max_calls_per_sponsee_condition( # If a condition still exists, do not create a new one existing_condition = ( db.query(models.Condition) - .filter(models.Condition.sponsee_address == condition.sponsee_address) + .filter(models.Condition.contract_id == condition.contract_id) .filter(models.Condition.vault_id == condition.vault_id) .filter(models.Condition.current < models.Condition.max) .one_or_none() @@ -329,17 +329,18 @@ def create_max_calls_per_sponsee_condition( db_condition = models.Condition( **{ "type": schemas.ConditionType.MAX_CALLS_PER_SPONSEE, - "sponsee_address": condition.sponsee_address, + "contract_id": condition.contract_id, "vault_id": condition.vault_id, "max": condition.max, + "is_active": True, "current": 0, } ) db.add(db_condition) db.commit() db.refresh(db_condition) - return schemas.MaxCallsPerSponseeCondition( - sponsee_address=db_condition.sponsee_address, + return schemas.ConditionBase( + contract_id=db_condition.contract_id, vault_id=db_condition.vault_id, max=db_condition.max, current=db_condition.current, @@ -390,12 +391,16 @@ def create_max_calls_per_entrypoint_condition( ) -def check_max_calls_per_sponsee(db: Session, sponsee_address: str, vault_id: UUID4): +def check_max_calls_per_sponsee( + db: Session, contract_id: UUID4, vault_id: UUID4 +): return ( db.query(models.Condition) .filter(models.Condition.type == schemas.ConditionType.MAX_CALLS_PER_SPONSEE) - .filter(models.Condition.sponsee_address == sponsee_address) + .filter(models.Condition.contract_id == contract_id) .filter(models.Condition.vault_id == vault_id) + .filter(models.Condition.is_active == True) + .order_by(models.Condition.created_at.asc()) .one_or_none() ) @@ -409,48 +414,65 @@ def check_max_calls_per_entrypoint( .filter(models.Condition.contract_id == contract_id) .filter(models.Condition.entrypoint_id == entrypoint_id) .filter(models.Condition.vault_id == vault_id) + .filter(models.Condition.is_active == True) .one_or_none() ) -def check_conditions(db: Session, datas: schemas.CheckConditions): - print(datas) +def update_condition(db: Session, condition: models.Condition): + db.query(models.Condition).filter(models.Condition.id == condition.id).update( + {"current": condition.current + 1} + ) + + +def check_max_sponsee_condition( + db: Session, data: schemas.CheckConditions, sponsee_condition: models.Condition +): + nb_operations = ( + db.query(models.Operation) + .filter(models.Operation.sender_address == data.sponsee_address) + .filter(models.Operation.contract_id == data.contract_id) + .filter(models.Operation.created_at >= sponsee_condition.created_at) + .count() + ) + print(nb_operations) + return nb_operations >= sponsee_condition.max + + +def check_conditions(db: Session, data: schemas.CheckConditions): + print(data) sponsee_condition = check_max_calls_per_sponsee( - db, datas.sponsee_address, datas.vault_id + db, data.contract_id, data.vault_id ) entrypoint_condition = check_max_calls_per_entrypoint( - db, datas.contract_id, datas.entrypoint_id, datas.vault_id + db, data.contract_id, data.entrypoint_id, data.vault_id ) # No condition registered if sponsee_condition is None and entrypoint_condition is None: return True - # One of condition is excedeed + # Check max_entrypoint condition if ( - sponsee_condition is not None - and (sponsee_condition.current >= sponsee_condition.max) - ) or ( - entrypoint_condition is not None - and (entrypoint_condition.current >= entrypoint_condition.max) + entrypoint_condition is not None and + (entrypoint_condition.current >= entrypoint_condition.max) ): return False - # Update conditions - # TODO - Rewrite with list + # Check max_sponsee condition + if ( + sponsee_condition is not None and + check_max_sponsee_condition(db, data, sponsee_condition) + ): + return False - if sponsee_condition: - update_condition(db, sponsee_condition) - if entrypoint_condition: + # Update conditions + if entrypoint_condition is not None: update_condition(db, entrypoint_condition) + if sponsee_condition is not None: + update_condition(db, sponsee_condition) return True -def update_condition(db: Session, condition: models.Condition): - db.query(models.Condition).filter(models.Condition.id == condition.id).update( - {"current": condition.current + 1} - ) - - def get_conditions_by_vault(db: Session, vault_id: str): return ( db.query(models.Condition).filter(models.Condition.vault_id == vault_id).all() diff --git a/src/models.py b/src/models.py index b0a7220..42edec7 100644 --- a/src/models.py +++ b/src/models.py @@ -112,7 +112,7 @@ class Operation(Base): id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) cost = Column(Integer) - user_address = Column(String) + sender_address = Column(String) contract_id = Column(UUID(as_uuid=True), ForeignKey("contracts.id")) entrypoint_id = Column(UUID(as_uuid=True), ForeignKey("entrypoints.id")) hash = Column(String) @@ -131,18 +131,11 @@ class Condition(Base): id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) type = Column(Enum(ConditionType)) - sponsee_address = Column( - String, - CheckConstraint( - "(type = 'MAX_CALLS_PER_SPONSEE') = (sponsee_address IS NOT NULL)", - name="sponsee_address_not_null_constraint", - ), - nullable=True, - ) contract_id = Column( UUID(as_uuid=True), CheckConstraint( - "(type = 'MAX_CALLS_PER_ENTRYPOINT') = (contract_id IS NOT NULL)", + "(type = 'MAX_CALLS_PER_ENTRYPOINT' or type = \ + 'MAX_CALLS_PER_SPONSEE') = (contract_id IS NOT NULL)", name="contract_id_not_null_constraint", ), ForeignKey("contracts.id"), @@ -151,7 +144,8 @@ class Condition(Base): entrypoint_id = Column( UUID(as_uuid=True), CheckConstraint( - "(type = 'MAX_CALLS_PER_ENTRYPOINT') = (entrypoint_id IS NOT NULL)", + "(type = 'MAX_CALLS_PER_ENTRYPOINT') = \ + (entrypoint_id IS NOT NULL)", name="entrypoint_id_not_null_constraint", ), ForeignKey("entrypoints.id"), @@ -163,7 +157,7 @@ class Condition(Base): created_at = Column( DateTime(timezone=True), default=datetime.datetime.utcnow(), nullable=False ) - + is_active = Column(Boolean, nullable=False) contract = relationship("Contract", back_populates="conditions") entrypoint = relationship("Entrypoint", back_populates="conditions") vault = relationship("Credit", back_populates="conditions") diff --git a/src/routes.py b/src/routes.py index 6873a79..b45d6ea 100644 --- a/src/routes.py +++ b/src/routes.py @@ -361,7 +361,11 @@ async def post_operation( crud.create_operation( db, schemas.CreateOperation( - user_address=call_data.sender_address, contract_id=str(contract.id), entrypoint_id=str(entrypoint.id), hash=result["transaction_hash"], status=result["result"] # type: ignore + sender_address=call_data.sender_address, + contract_id=str(contract.id), + entrypoint_id=str(entrypoint.id), + hash=result["transaction_hash"], + status=result["result"] # type: ignore ), ) except MichelsonError as e: @@ -448,12 +452,12 @@ async def create_condition( ) elif ( body.type == ConditionType.MAX_CALLS_PER_SPONSEE - and body.sponsee_address is not None + and body.contract_id is not None ): return crud.create_max_calls_per_sponsee_condition( db, schemas.CreateMaxCallsPerSponseeCondition( - sponsee_address=body.sponsee_address, + contract_id=body.contract_id, vault_id=body.vault_id, max=body.max, ), diff --git a/src/schemas.py b/src/schemas.py index c7db896..8143cfa 100644 --- a/src/schemas.py +++ b/src/schemas.py @@ -6,7 +6,9 @@ # -- UTILITY TYPES -- class ConditionType(enum.Enum): + # Max number of calls to a given entrypoint, for all sponsee MAX_CALLS_PER_ENTRYPOINT = "MAX_CALLS_PER_ENTRYPOINT" + # Max number of calls per sponsee per contract MAX_CALLS_PER_SPONSEE = "MAX_CALLS_PER_SPONSEE" @@ -125,7 +127,7 @@ class SignedCall(BaseModel): class CreateOperation(BaseModel): - user_address: str + sender_address: str contract_id: str entrypoint_id: str hash: str @@ -139,7 +141,6 @@ class UpdateMaxCallsPerMonth(BaseModel): class CreateCondition(BaseModel): type: ConditionType - sponsee_address: Optional[str] = None contract_id: Optional[UUID4] = None entrypoint_id: Optional[UUID4] = None vault_id: UUID4 @@ -154,7 +155,7 @@ class CreateMaxCallsPerEntrypointCondition(BaseModel): class CreateMaxCallsPerSponseeCondition(BaseModel): - sponsee_address: str + contract_id: UUID4 vault_id: UUID4 max: int @@ -168,6 +169,7 @@ class CheckConditions(BaseModel): class ConditionBase(BaseModel): vault_id: UUID4 + contract_id: UUID4 max: int current: int type: ConditionType @@ -176,9 +178,4 @@ class ConditionBase(BaseModel): class MaxCallsPerEntrypointCondition(ConditionBase): - contract_id: UUID4 entrypoint_id: UUID4 - - -class MaxCallsPerSponseeCondition(ConditionBase): - sponsee_address: str From 66c1621b03a71f388f35b27753b8f7df7910c3d3 Mon Sep 17 00:00:00 2001 From: Arthur Guillon Date: Wed, 13 Mar 2024 14:36:24 +0100 Subject: [PATCH 5/5] Format things --- src/crud.py | 20 ++++++-------------- src/routes.py | 7 ++----- src/tezos.py | 2 +- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/crud.py b/src/crud.py index be73a5c..af6dc62 100644 --- a/src/crud.py +++ b/src/crud.py @@ -391,9 +391,7 @@ def create_max_calls_per_entrypoint_condition( ) -def check_max_calls_per_sponsee( - db: Session, contract_id: UUID4, vault_id: UUID4 -): +def check_max_calls_per_sponsee(db: Session, contract_id: UUID4, vault_id: UUID4): return ( db.query(models.Condition) .filter(models.Condition.type == schemas.ConditionType.MAX_CALLS_PER_SPONSEE) @@ -435,15 +433,11 @@ def check_max_sponsee_condition( .filter(models.Operation.created_at >= sponsee_condition.created_at) .count() ) - print(nb_operations) return nb_operations >= sponsee_condition.max def check_conditions(db: Session, data: schemas.CheckConditions): - print(data) - sponsee_condition = check_max_calls_per_sponsee( - db, data.contract_id, data.vault_id - ) + sponsee_condition = check_max_calls_per_sponsee(db, data.contract_id, data.vault_id) entrypoint_condition = check_max_calls_per_entrypoint( db, data.contract_id, data.entrypoint_id, data.vault_id ) @@ -452,16 +446,14 @@ def check_conditions(db: Session, data: schemas.CheckConditions): if sponsee_condition is None and entrypoint_condition is None: return True # Check max_entrypoint condition - if ( - entrypoint_condition is not None and - (entrypoint_condition.current >= entrypoint_condition.max) + if entrypoint_condition is not None and ( + entrypoint_condition.current >= entrypoint_condition.max ): return False # Check max_sponsee condition - if ( - sponsee_condition is not None and - check_max_sponsee_condition(db, data, sponsee_condition) + if sponsee_condition is not None and check_max_sponsee_condition( + db, data, sponsee_condition ): return False diff --git a/src/routes.py b/src/routes.py index b45d6ea..e0038ea 100644 --- a/src/routes.py +++ b/src/routes.py @@ -30,10 +30,7 @@ # Healthcheck @router.get("/") async def root(): - return { - "status": "healthy", - "tezos_address": tezos.public_address - } + return {"status": "healthy", "tezos_address": tezos.public_address} # POST endpoints @@ -365,7 +362,7 @@ async def post_operation( contract_id=str(contract.id), entrypoint_id=str(entrypoint.id), hash=result["transaction_hash"], - status=result["result"] # type: ignore + status=result["result"], # type: ignore ), ) except MichelsonError as e: diff --git a/src/tezos.py b/src/tezos.py index d4404c7..1a7f557 100644 --- a/src/tezos.py +++ b/src/tezos.py @@ -85,7 +85,7 @@ async def confirm_deposit(tx_hash, payer, amount: Union[int, str]): receiver = ptz.key.public_key_hash() block_time = int(constants["minimal_block_delay"]) try: - async with asyncio.timeout(2*block_time): + async with asyncio.timeout(2 * block_time): op_result = await find_transaction(tx_hash) return any( op