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/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 diff --git a/src/crud.py b/src/crud.py index 3b7d98c..af6dc62 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,14 @@ 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 +412,59 @@ 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) - sponsee_condition = check_max_calls_per_sponsee( - db, datas.sponsee_address, datas.vault_id +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() + ) + return nb_operations >= sponsee_condition.max + + +def check_conditions(db: Session, data: schemas.CheckConditions): + sponsee_condition = check_max_calls_per_sponsee(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 - 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) + # Check max_entrypoint condition + if 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 1522229..e0038ea 100644 --- a/src/routes.py +++ b/src/routes.py @@ -30,7 +30,7 @@ # Healthcheck @router.get("/") async def root(): - return {"message": "Hello World"} + return {"status": "healthy", "tezos_address": tezos.public_address} # POST endpoints @@ -358,7 +358,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: @@ -445,12 +449,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 diff --git a/src/tezos.py b/src/tezos.py index 6e8f5ea..1a7f557 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() @@ -82,15 +83,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):