From 1e2e688453125bf3864bd6042615a6d9e4a427f5 Mon Sep 17 00:00:00 2001 From: Sai Raj <39055732+sairajzero@users.noreply.github.com> Date: Mon, 15 Jul 2024 01:18:10 -0400 Subject: [PATCH] Adding API server files --- src/api/.gitignore | 12 + src/api/README.md | 1068 +++++++++++++ src/api/config-example.py | 8 + src/api/fetchRates.py | 150 ++ src/api/parsing.py | 1240 +++++++++++++++ src/api/ranchimallflo_api.py | 2880 ++++++++++++++++++++++++++++++++++ src/api/requirements.txt | 31 + src/api/static/broadcast.js | 24 + src/api/templates/index.html | 12 + src/api/wsgi.py | 4 + 10 files changed, 5429 insertions(+) create mode 100644 src/api/.gitignore create mode 100644 src/api/README.md create mode 100644 src/api/config-example.py create mode 100644 src/api/fetchRates.py create mode 100644 src/api/parsing.py create mode 100644 src/api/ranchimallflo_api.py create mode 100644 src/api/requirements.txt create mode 100644 src/api/static/broadcast.js create mode 100644 src/api/templates/index.html create mode 100644 src/api/wsgi.py diff --git a/src/api/.gitignore b/src/api/.gitignore new file mode 100644 index 0000000..7950ab2 --- /dev/null +++ b/src/api/.gitignore @@ -0,0 +1,12 @@ +.vscode/ +__pycache__/ +*.swp +config.py +.idea/ +py3.7/ +py3/ +py3.8/ +*.db +*.code-workspace +*.log +py*/ diff --git a/src/api/README.md b/src/api/README.md new file mode 100644 index 0000000..3586388 --- /dev/null +++ b/src/api/README.md @@ -0,0 +1,1068 @@ +# Ranchi Mall FLO API +Ranchi Mall FLO Tokens and Smart Contract API + +The above API provides details about the token and smart contract system on the FLO blockchain. + +## Running the API + +### Pre-requisites +1. flo-token-tracking database +2. A FLO public-private key pair for the socket API - The public key will be put on the API and private key will be used to connect to the socket API by the client. The socket API provides live feed of new incoming blocks and transactions + +### Installation +``` +git clone https://github.com/ranchimall/ranchimallflo-api +cd ranchimallflo-api +python3 -m venv py3.. +source py3/bin/activate +pip install requests quart quart_cors ecdsa config arrow +``` +Create a **config.py** file inside the API folder, with the details mentioned in the pre-requisite +``` +# Create a config file +dbfolder = '< PATH TO YOUR FLO-TOKEN-TRACKING FOLDER >' +sse_pubKey = '< YOUR PUBLIC KEY FROM THE PUBLIC KEY PAIR >' +``` + +To start the API, execute the following from inside the folder +``` +python ranchimallflo_api.py +``` + +## API HTTP Endpoints + +### List of token names +Get a list of all the active tokens on FLO blockchain +``` + /api/v1.0/getTokenList +``` +Output: +``` +{ + "result": "ok", + "tokens": [ + "chainserve", + "rupee", + "rmt", + "utopia" + ] +} +``` + +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getTokenList') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getTokenList'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` + + +### Information about a token +Get information about a token on the FLO blockchain +``` + /api/v1.0/getTokenInfo? +``` +Output: +``` +{ + "activeAddress_no": 25, + "blockchainReference": "https://flosight.duckdns.org/tx/a74a03ec1e77fa50e0b586b1e9745225ad4f78ce96ca59d6ac025f8057dd095c", + "incorporationAddress": "FKNW5eCCp2SnJMJ6pLLpUCvk5hAage8Jtk", + "result": "ok", + "token": "rmt", + "tokenSupply": 21000000, + "transactionHash": "a74a03ec1e77fa50e0b586b1e9745225ad4f78ce96ca59d6ac025f8057dd095c" +} +``` + +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getTokenInfo?') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getTokenInfo?'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` + +### Information about a token's transactions +Get information about a token's related transactions on the blockchain +``` + /api/v1.0/getTokenTransactions? +``` +Optional URL parameters : +senderFloAddress +destFloAddress + +Output: +``` +{ +"result": "ok", +"transactions": [ + { + "blockNumber": 3454503, + "blockchainReference": "https://flosight.duckdns.org/tx/b57cf412c8cb16e473d04bae44214705c64d2c25146be22695bf1ac36e166ee0", + "destFloAddress": "FFXX4i986DzDYZsGYXoozm6714WHtHZSod", + "sourceFloAddress": "F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1", + "transferAmount": 0.00133333 + }, + { + "blockNumber": 3454503, + "blockchainReference": "https://flosight.duckdns.org/tx/b57cf412c8cb16e473d04bae44214705c64d2c25146be22695bf1ac36e166ee0", + "destFloAddress": "FCMLYTNBUXiC8R3xwnGJVaDzoUYkJMqHKd", + "sourceFloAddress": "F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1", + "transfe},rAmount": 0.00133333 + } + ] +} +``` + +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getTokenTransactions?') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getTokenTransactions?'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` +### Information about a token's address balances +Get information about a token's address balances +``` + /api/v1.0/getTokenBalances? +``` + +Output: +``` +{ + "balances": [ + { + "balance": 0.0023333299999999998, + "floAddress": "F6EMAHjivqrcrdAHNABq2R1FLNkx8xfEaT" + }, + { + "balance": 0.0023333299999999998, + "floAddress": "F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z" + }, + { + "balance": 0.0023333299999999998, + "floAddress": "F7k7bTPStxr6wKCzUNVW48j3N2tN3PVgxZ" + } + ], + "result": "ok" +} +``` +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getTokenBalances?') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getTokenBalances?'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` +### Information about a FLO address +Get information about a FLO address +``` + /api/v1.0/getFloAddressDetails?floAddress=F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z +``` + +Output: +``` +{ + "floAddress": "F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z", + "floAddressDetails": [ + { + "balance": 0.0023333299999999998, + "token": "rmt" + } + ], + "result": "ok" +} +``` +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getFloAddressDetails?floAddress=F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getFloAddressDetails?floAddress=F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` +### Information about a FLO address of a particular token +Get information about a FLO address of a particular token +``` + /api/v1.0/getFloAddressBalance?token=rmt&floAddress=F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z +``` + +Output: +``` +{ + "balance": 0.0023333299999999998, + "floAddress": "F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z", + "result": "ok", + "token": "rmt" +} +``` +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getFloAddressBalance?token=rmt&floAddress=F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getFloAddressBalance?token=rmt&floAddress=F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` + +### Information about a FLO address's transactions +Get information about a FLO address's transactions +``` + /api/v1.0/getFloAddressTransactions?floAddress=F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z +``` + +Output: +``` +{ +"allTransactions": [ + { + "token": "rmt", + "transactions": [ + { + "blockNumber": 3454503, + "blockchainReference": "https://flosight.duckdns.org/tx/b57cf412c8cb16e473d04bae44214705c64d2c25146be22695bf1ac36e166ee0", + "destFloAddress": "FFXX4i986DzDYZsGYXoozm6714WHtHZSod", + "sourceFloAddress": "F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1", + "transferAmount": 0.00133333 + }, + { + "blockNumber": 3454503, + "blockchainReference": "https://flosight.duckdns.org/tx/b57cf412c8cb16e473d04bae44214705c64d2c25146be22695bf1ac36e166ee0", + "destFloAddress": "FCMLYTNBUXiC8R3xwnGJVaDzoUYkJMqHKd", + "sourceFloAddress": "F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1", + "transferAmount": 0.00133333 + } + ] + } +], +"floAddress": "F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z", +"result": "ok" +} +``` +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getFloAddressTransactions?floAddress=F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getFloAddressTransactions?floAddress=F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` +### Get list of all active smart contracts +Get list of all active smart contracts +``` + /api/v1.0/getSmartContractList +``` +Optional parameters +contractName +contractAddress + +Output: +``` +{ + "result": "ok", + "smartContracts": [ + { + "contractAddress": "F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1", + "contractName": "india-elections-2019", + "expiryDate": "1558539107", + "incorporationDate": "1557576932", + "status": "closed", + "transactionHash": "c6eb7adc731a60b2ffa0c48d0d72d33b2ec3a33e666156e729a63b25f6c5cd56" + } + ] +} +``` +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getSmartContractList') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getSmartContractList'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` +### Smart Contract's information +Get information about a specified Smart Contract +``` + /api/v1.0/getSmartContractInfo?contractName=india-elections-2019&contractAddress=F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1 +``` +Output: +``` +{ + "contractInfo": { + "contractAddress": "F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1", + "contractAmount": "0.001", + "contractName": "india-elections-2019", + "contractType": "one-time-event", + "expiryDate": "1558539107", + "expiryTime": "wed may 22 2019 21:00:00 gmt+0530", + "flodata": "Create Smart Contract with the name India-elections-2019@ of the type one-time-event* using the asset rmt# at the address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1$ with contract-conditions: (1) contractAmount=0.001rmt (2) userChoices=Narendra Modi wins| Narendra Modi loses (3) expiryTime= Wed May 22 2019 21:00:00 GMT+0530", + "incorporationDate": "1557576932", + "numberOfParticipants": 16, + "status": "closed", + "tokenAmountDeposited": 0.016000000000000007, + "tokenIdentification": "rmt", + "userChoice": [ + "narendra modi wins", + "narendra modi loses" + ] + }, + "result": "ok" +} +``` +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getSmartContractInfo?contractName=india-elections-2019&contractAddress=F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getSmartContractInfo?contractName=india-elections-2019&contractAddress=F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` +### Smart Contract's participants +Get information about a specified Smart Contract's participants +``` + /api/v1.0/getSmartContractParticipants?contractName=india-elections-2019&contractAddress=F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1 +``` +Output: +``` +{ + "participantInfo": { + "1": { + "participantFloAddress": "FGyDAHZ3AU5TqRV2zrTju9DBCbKWZtearf", + "tokenAmount": 0.001, + "transactionHash": "26f08763cd177e2d55080041637527a7769eb3507b023a25bc9edbe8649c2fe2", + "userChoice": "narendra modi wins", + "winningAmount": 0.00133333 + }, + "2": { + "participantFloAddress": "F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z", + "tokenAmount": 0.001, + "transactionHash": "511f16a69c5f62ad1cce70a2f9bfba133589e3ddc560d406c4fbf3920eae8469", + "userChoice": "narendra modi wins", + "winningAmount": 0.00133333 + }, + "3": { + "participantFloAddress": "FEDUxQPznerYapqhdiBDT54rGhAdRikqJA", + "tokenAmount": 0.001, + "transactionHash": "e92bf6a8bddf177a5e2d793fa86c7ad059c89157f683f90f02b3590c0e4282c5", + "userChoice": "narendra modi wins", + "winningAmount": 0.00133333 + } + }, + "result": "ok" +} +``` +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getSmartContractParticipants?contractName=india-elections-2019&contractAddress=F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getSmartContractParticipants?contractName=india-elections-2019&contractAddress=F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` +### Smart Contract's participant details +Get information about a specified Smart Contract's participants +``` + /api/v1.0/getSmartContractParticipantDetails?floAddress= +``` +Output: +``` +{ + "participantInfo": { + "1": { + "participantFloAddress": "FGyDAHZ3AU5TqRV2zrTju9DBCbKWZtearf", + "tokenAmount": 0.001, + "transactionHash": "26f08763cd177e2d55080041637527a7769eb3507b023a25bc9edbe8649c2fe2", + "userChoice": "narendra modi wins", + "winningAmount": 0.00133333 + }, + "2": { + "participantFloAddress": "F6WPx2WFdmVQ6AMutZ2FJzpyiwdxqLyd2z", + "tokenAmount": 0.001, + "transactionHash": "511f16a69c5f62ad1cce70a2f9bfba133589e3ddc560d406c4fbf3920eae8469", + "userChoice": "narendra modi wins", + "winningAmount": 0.00133333 + }, + "3": { + "participantFloAddress": "FEDUxQPznerYapqhdiBDT54rGhAdRikqJA", + "tokenAmount": 0.001, + "transactionHash": "e92bf6a8bddf177a5e2d793fa86c7ad059c89157f683f90f02b3590c0e4282c5", + "userChoice": "narendra modi wins", + "winningAmount": 0.00133333 + } + }, + "result": "ok" +} +``` +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getSmartContractParticipantDetails?floAddress=') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getSmartContractParticipantDetails?floAddress='){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` +### Block details +Get information about a block by specifying its blockno +``` + /api/v1.0/getBlockDetails/ +``` +Output: +``` +{ + "bits": 486729686, + "chainwork": "000000000000000000000000000000000000000000000000000001e1750030bd", + "confirmations": 3615543, + "difficulty": 3455, + "hash": "50694aafcfbf72e687f15e00b17cf4603797414d49169c1674a7bf60dd1f2173", + "height": 1524, + "isMainChain": true, + "merkleroot": "7caf89db3ac334a98023952370e2e73058e1437e34e3a6d5500e66f388d7c3e3", + "nextblockhash": "6924186cb0235373d2daa2ee6e07319c92fc617bb72a86c64b22832b721ac060", + "nonce": 1557664000, + "poolInfo": {}, + "previousblockhash": "9c90782124c5c074def3191bcf059db0df2c40c642645290c216b8fceed85e63", + "reward": 100, + "size": 604, + "time": 1371543439, + "tx": [ + "7caf89db3ac334a98023952370e2e73058e1437e34e3a6d5500e66f388d7c3e3" + ], + "version": 1 +} +``` +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getBlockDetails/') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getBlockDetails/'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` +### Transaction details +Get information about a transaction +``` + /api/v1.0/getTransactionDetails/ +``` +Output: +``` +{ + "parsingDetails": { + "contractName": "india-elections-2019", + "flodata": "send 0.001 rmt# to india-elections-2019@ to FLO address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1 with the userchoice:'narendra modi wins''", + "operation": "transfer", + "tokenAmount": 0.001, + "tokenIdentification": "rmt", + "transferType": "smartContract", + "type": "transfer", + "userChoice": "narendra modi wins" + }, + "transactionDetails": { + "blockhash": "042d8229355ab7256f8f4234f8385a6834d88c12fedd0f576f128234206f7633", + "blockheight": 3447257, + "blocktime": 1558532628, + "confirmations": 169813, + "fees": 0.0005, + "floData": "send 0.001 rmt# to india-elections-2019@ to FLO address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1 with the userchoice:'narendra modi wins''", + "locktime": 0, + "size": 506, + "time": 1558532628, + "txid": "5a36fce4646358c751b5403ec5c7465f1b11c8dca6d86e8a9cd4e26184e07b1a", + "valueIn": 0.0025, + "valueOut": 0.002, + "version": 2, + "vin": [ + { + "addr": "FCMLYTNBUXiC8R3xwnGJVaDzoUYkJMqHKd", + "confirmations": null, + "doubleSpentTxID": null, + "isConfirmed": null, + "n": 0, + "scriptSig": { + "asm": "3044022002f365ccbf0142f23a54a0c05c782f7d35bf10019a259ba089bc6892071121fc022031a128bd67155bdfc92c3b5329ad5f94541a940ec361f59623aa9b2db4434c1501 021df9a231a28e4a7f913ec8fe1e9c6e77b8672553d12ac3125c4976e184d76dc2", + "hex": "473044022002f365ccbf0142f23a54a0c05c782f7d35bf10019a259ba089bc6892071121fc022031a128bd67155bdfc92c3b5329ad5f94541a940ec361f59623aa9b2db4434c150121021df9a231a28e4a7f913ec8fe1e9c6e77b8672553d12ac3125c4976e184d76dc2" + }, + "sequence": 4294967295, + "txid": "e4ee5448dac2f378b65c7ec7ddd596e329510ef22175940d7e447b5d60df75ae", + "unconfirmedInput": null, + "value": 0.0005, + "valueSat": 50000, + "vout": 1 + }, + { + "addr": "FCMLYTNBUXiC8R3xwnGJVaDzoUYkJMqHKd", + "confirmations": null, + "doubleSpentTxID": null, + "isConfirmed": null, + "n": 1, + "scriptSig": { + "asm": "3045022100abbe2a0c5a6efc88ee619b58ee2ae4d38cc46dd42c79fd4db2391fe1b76dfaed022078ad9c2fa6a712dc51efbc586298ed813dd37ce4d209a38fe313f494079e301901 021df9a231a28e4a7f913ec8fe1e9c6e77b8672553d12ac3125c4976e184d76dc2", + "hex": "483045022100abbe2a0c5a6efc88ee619b58ee2ae4d38cc46dd42c79fd4db2391fe1b76dfaed022078ad9c2fa6a712dc51efbc586298ed813dd37ce4d209a38fe313f494079e30190121021df9a231a28e4a7f913ec8fe1e9c6e77b8672553d12ac3125c4976e184d76dc2" + }, + "sequence": 4294967295, + "txid": "5e67fee05ed5f6598a85e8fb12e207183bc4441c8f81d54b89ce6bfd196c5fdf", + "unconfirmedInput": null, + "value": 0.002, + "valueSat": 200000, + "vout": 0 + } + ], + "vout": [ + { + "n": 0, + "scriptPubKey": { + "addresses": [ + "F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1" + ], + "asm": "OP_DUP OP_HASH160 15b3eb460d593f74775167d589a3a443eb78b55b OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a91415b3eb460d593f74775167d589a3a443eb78b55b88ac", + "type": "pubkeyhash" + }, + "spentHeight": null, + "spentIndex": null, + "spentTxId": null, + "value": "0.00100000" + }, + { + "n": 1, + "scriptPubKey": { + "addresses": [ + "FCMLYTNBUXiC8R3xwnGJVaDzoUYkJMqHKd" + ], + "asm": "OP_DUP OP_HASH160 478827032c14de407edb37c91830bc18954a7a53 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914478827032c14de407edb37c91830bc18954a7a5388ac", + "type": "pubkeyhash" + }, + "spentHeight": null, + "spentIndex": null, + "spentTxId": null, + "value": "0.00100000" + } + ] + } +} +``` +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getTransactionDetails/') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getTransactionDetails/'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` +### Latest Blocks details +Get information about latest blocks +``` + /api/v1.0/getLatestBlockDetails +``` +Output: +``` +{ + "latestBlocks": [ + { + "bits": "1b1471d4", + "chainwork": "00000000000000000000000000000000000000000000000099f3f203a3078115", + "confirmations": 175091, + "difficulty": 3205.485468631051, + "hash": "c55d34d5efc558bcdfd1f63e6b4bcfe105f950ce2698033d4e6f0baac68840a6", + "height": 3447073, + "mediantime": 1558524867, + "merkleroot": "5b87fee25703cb6ae2a3e8c8f5390070c940b26963cc2b31e7566185b0748b77", + "nextblockhash": "f11c0f6cd958f3a769350c2436ea76f3f776d86a66fcdc180d90a5d09ce09cf0", + "nonce": 1775056665, + "previousblockhash": "58497e82453051744fcde4fef6ff755f750f668db61a7a5edca818e7c5e47df5", + "size": 860, + "strippedsize": 824, + "time": 1558525219, + "tx": [ + "38da76036d533967993e767702364cface0336609d9c9aa6cea05994514c1401", + "ad50d8bffe7214473b6f8f454f55749d23a26ab3fc854d63e1ccc808045d98ba" + ], + "version": 536870912, + "versionHex": "20000000", + "weight": 3332 + }, + { + "bits": "1b12a3cc", + "chainwork": "00000000000000000000000000000000000000000000000099fd54362122a23e", + "confirmations": 174907, + "difficulty": 3515.85795445243, + "hash": "042d8229355ab7256f8f4234f8385a6834d88c12fedd0f576f128234206f7633", + "height": 3447257, + "mediantime": 1558532349, + "merkleroot": "85f9303ab800f686bcc58cd495fd4d45d462c8f37a279277504c7da02d062b7b", + "nextblockhash": "8c1a4f8301bd594c4d3503b03d29fd007c75011dd9fafcd7f7abea340ee2ca92", + "nonce": 1161774987, + "previousblockhash": "b8d2570bec995696affca41eb3466477c7ead4372d809fff0d349a5894f8da9c", + "size": 4448, + "strippedsize": 4448, + "time": 1558532628, + "tx": [ + "ffbacc1ec3ffc5fa9390ff76e2705685bd1a9886c29295bdd2a993829cc2cf71", + "dea1f346524565e9ef7ca68c5aea862edf30b096cc782983e05ce8cdd6827a86", + "824db22bc0d2653f2bc9a6b475294760d7867bfbb8bad8b6b65dc49986735b2e", + "e2b1b0a576765138e77ac86c87653617879c38670ff9b55f26cc2a057ba71935", + "9698b7e50279f2bb0334984dac0a671417b7796e93c8115641ec3ee131805044", + "aacb9a24d278102f4ab2c344211dac5a016968666f7a762b0a211f8b840ba17a", + "aa710482732c50d18db41a8f8775ccf9244e7867fc80effc0868bb513f2cccb6", + "761411f77ea5d1d11f988c98bbe2ad9f66ce4d5b327dfa7434cd7b4f2797dbab", + "6ac7d43394de8dbeffc80609b2996e1626606c7fccca51b827f928b7162c60af", + "016e0c62ce10114adf867c0d4ad24c45223e0a8e83ed6b78e91aead59437e1df", + "5a36fce4646358c751b5403ec5c7465f1b11c8dca6d86e8a9cd4e26184e07b1a" + ], + "version": 536870912, + "versionHex": "20000000", + "weight": 17792 + }, + { + "bits": "1b11db34", + "chainwork": "00000000000000000000000000000000000000000000000099fe633b321afd59", + "confirmations": 174886, + "difficulty": 3670.14099816446, + "hash": "9a7124bb45e489fbec02d41f7b22c7695493a70cc7fe97b864290e2727c6d7cc", + "height": 3447278, + "mediantime": 1558533117, + "merkleroot": "a33fa061af2c67ba20a26718866dafb50ee96a4373a8bd2c364d377571779e4d", + "nextblockhash": "b2188357cfef92e374aab9cbfdf6cf54b9d56edea4a1400a2047f2b7d2e8e0fc", + "nonce": 1300764883, + "previousblockhash": "c8ff8f84fd277e054f44885c7df7bf48d076a09b7595fd302b2d2598e2d4d2b1", + "size": 1819, + "strippedsize": 1819, + "time": 1558533249, + "tx": [ + "2682d8a248bef10bcf28f04625300e03c071fce346ec590401be2d9d4af5de02", + "90197c5614d321d8ecc8d6073ca926b1c5d06d5dace0382e6572ef3d96db2239", + "c928c63574c6c4280ad8d87fd0dfee128ce02b957c1d2cd5306fe33ecf922a3b", + "79ae8f0a1ebcee00f534311046f4806cedc8e7db7db7ecd44f04c9986791cff4", + "3520cda2de65249f7629683c4b8f3efb15479076fe9521250aba2f1abe3f2ce9" + ], + "version": 536870912, + "versionHex": "20000000", + "weight": 7276 + }, + { + "bits": "1b1d5382", + "chainwork": "000000000000000000000000000000000000000000000000a25b992cbf49685d", + "confirmations": 119468, + "difficulty": 2234.690981215679, + "hash": "734a190218bcf6cb0abd1fb825ccf2cedb7b555e35a96673cb0c9ff214925f7a", + "height": 3503146, + "mediantime": 1561044091, + "merkleroot": "4b26a4232d1a64d873f80f871cc54aaf12f7206b305796ad421adcb422d06943", + "nextblockhash": "05edb88439a7fa74e649b4030c674053dada9b0044a793b8b0b9250326ec172e", + "nonce": 282711878, + "previousblockhash": "b4362a3fd0ba877f4aa4a8edded28816b2a19f11cb30a8374effedb966c23ffb", + "size": 650, + "strippedsize": 650, + "time": 1561044213, + "tx": [ + "bda24ce4a936d31bb1e3ede159303eb0beca9d3cd2c85e78e3e071be4b2512ca", + "ebc99f41c4ccaea32bd9dbc251515838ba47660b7422c7cae39ee3e2ae0107fe" + ], + "version": 536870912, + "versionHex": "20000000", + "weight": 2600 + } + ], + "result": "ok" +} +``` +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getLatestBlockDetails') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getLatestBlockDetails'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` +### Latest Transactions details +Get information about latest transactions +``` + /api/v1.0/getLatestTransactionDetails +``` +Output: +``` +{ + "latestTransactions": [ + { + "parsedFloData": { + "contractName": "india-elections-2019", + "flodata": "send 0.001 rmt# to india-elections-2019@ to FLO address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1 with the userchoice:'narendra modi wins'", + "operation": "transfer", + "tokenAmount": 0.001, + "tokenIdentification": "rmt", + "transactionType": "transfer", + "transferType": "smartContract", + "type": "transfer", + "userChoice": "narendra modi wins" + }, + "transactionDetails": { + "blockhash": "9a7124bb45e489fbec02d41f7b22c7695493a70cc7fe97b864290e2727c6d7cc", + "blockheight": 3447278, + "blocktime": 1558533249, + "confirmations": 174886, + "floData": "send 0.001 rmt# to india-elections-2019@ to FLO address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1 with the userchoice:'narendra modi wins'", + "hash": "3520cda2de65249f7629683c4b8f3efb15479076fe9521250aba2f1abe3f2ce9", + "hex": "020000000126130ca39fc381791c053582a8af6694be3b0937d3723672cf3d49c3306c43d8000000006b483045022100f090fe20e4b525b699f580d62375f515a7d2d6daa9b9e10d80521d7d4f0bbe0d02203da43d9343b9c8edd9a65eec15e9a3094b19b2cc5bfe26985350fa006812cccf012102097cd225015f274dcc7d6a482b4a93c89467b7dc3a6c43451bf0203775e2af85ffffffff02a0860100000000001976a91415b3eb460d593f74775167d589a3a443eb78b55b88ac50c30000000000001976a9146a5d78edcec0d27ee30a0bdf8ae2b2a9312cffa288ac000000008373656e6420302e30303120726d742320746f20696e6469612d656c656374696f6e732d323031394020746f20464c4f20616464726573732046376f7342706a444456316d53536e4d4e724c7564455151336377444a3264505231207769746820746865207573657263686f6963653a276e6172656e647261206d6f64692077696e7327", + "locktime": 0, + "size": 358, + "time": 1558533249, + "txid": "3520cda2de65249f7629683c4b8f3efb15479076fe9521250aba2f1abe3f2ce9", + "version": 2, + "vin": [ + { + "scriptSig": { + "asm": "3045022100f090fe20e4b525b699f580d62375f515a7d2d6daa9b9e10d80521d7d4f0bbe0d02203da43d9343b9c8edd9a65eec15e9a3094b19b2cc5bfe26985350fa006812cccf[ALL] 02097cd225015f274dcc7d6a482b4a93c89467b7dc3a6c43451bf0203775e2af85", + "hex": "483045022100f090fe20e4b525b699f580d62375f515a7d2d6daa9b9e10d80521d7d4f0bbe0d02203da43d9343b9c8edd9a65eec15e9a3094b19b2cc5bfe26985350fa006812cccf012102097cd225015f274dcc7d6a482b4a93c89467b7dc3a6c43451bf0203775e2af85" + }, + "sequence": 4294967295, + "txid": "d8436c30c3493dcf723672d337093bbe9466afa88235051c7981c39fa30c1326", + "vout": 0 + } + ], + "vout": [ + { + "n": 0, + "scriptPubKey": { + "addresses": [ + "F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1" + ], + "asm": "OP_DUP OP_HASH160 15b3eb460d593f74775167d589a3a443eb78b55b OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a91415b3eb460d593f74775167d589a3a443eb78b55b88ac", + "reqSigs": 1, + "type": "pubkeyhash" + }, + "value": 0.001 + }, + { + "n": 1, + "scriptPubKey": { + "addresses": [ + "FFXX4i986DzDYZsGYXoozm6714WHtHZSod" + ], + "asm": "OP_DUP OP_HASH160 6a5d78edcec0d27ee30a0bdf8ae2b2a9312cffa2 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9146a5d78edcec0d27ee30a0bdf8ae2b2a9312cffa288ac", + "reqSigs": 1, + "type": "pubkeyhash" + }, + "value": 0.0005 + } + ], + "vsize": 358 + } + }, + { + "parsedFloData": { + "flodata": "Incorporate 10 million tokens for Utopia#", + "tokenAmount": 10000000, + "tokenIdentification": "utopia", + "transactionType": "tokenIncorporation", + "type": "tokenIncorporation" + }, + "transactionDetails": { + "blockhash": "734a190218bcf6cb0abd1fb825ccf2cedb7b555e35a96673cb0c9ff214925f7a", + "blockheight": 3503146, + "blocktime": 1561044213, + "confirmations": 119468, + "floData": "Incorporate 10 million tokens for Utopia#", + "hash": "ebc99f41c4ccaea32bd9dbc251515838ba47660b7422c7cae39ee3e2ae0107fe", + "hex": "0200000002dd3f9d64f87cadf1ab603a7d34a5b331bb4ac2611350ca7584ebf00353c0d3e4010000006a47304402205b9ffe2ae901c1c62f21409d68bf4f18f94ce4c482569ed5f6c21aa15257a3a402203f85f0b04d8982bb09e8852a7f88169dbd9fb404144c5048846b796a96edf24f012102cc5eaa7bd1c37ebf8483c014d38a1608d06cf6be6d61bae64b697725bae7696bffffffffe900e5690d95dbc12800799d786236af2a22ab6aafcb9f9a734a161ff9c61479000000006b483045022100a6c13e00f1fb53122424c35c581628830cf3b0cfab600397e7d3b6dd6d8aa89a022071add87aaa08239f30d58f2991525c66888e1f3fb8d52eec3c72c1871cced50f012102cc5eaa7bd1c37ebf8483c014d38a1608d06cf6be6d61bae64b697725bae7696bffffffff0220a10700000000001976a914868190f1c3c9839fc34f994a44898739624fcfbc88ace00f9700000000001976a914b9d8e7f578161fb3fa16a061abe412926cbfc85488ac0000000029496e636f72706f72617465203130206d696c6c696f6e20746f6b656e7320666f722055746f70696123", + "locktime": 0, + "size": 415, + "time": 1561044213, + "txid": "ebc99f41c4ccaea32bd9dbc251515838ba47660b7422c7cae39ee3e2ae0107fe", + "version": 2, + "vin": [ + { + "scriptSig": { + "asm": "304402205b9ffe2ae901c1c62f21409d68bf4f18f94ce4c482569ed5f6c21aa15257a3a402203f85f0b04d8982bb09e8852a7f88169dbd9fb404144c5048846b796a96edf24f[ALL] 02cc5eaa7bd1c37ebf8483c014d38a1608d06cf6be6d61bae64b697725bae7696b", + "hex": "47304402205b9ffe2ae901c1c62f21409d68bf4f18f94ce4c482569ed5f6c21aa15257a3a402203f85f0b04d8982bb09e8852a7f88169dbd9fb404144c5048846b796a96edf24f012102cc5eaa7bd1c37ebf8483c014d38a1608d06cf6be6d61bae64b697725bae7696b" + }, + "sequence": 4294967295, + "txid": "e4d3c05303f0eb8475ca501361c24abb31b3a5347d3a60abf1ad7cf8649d3fdd", + "vout": 1 + }, + { + "scriptSig": { + "asm": "3045022100a6c13e00f1fb53122424c35c581628830cf3b0cfab600397e7d3b6dd6d8aa89a022071add87aaa08239f30d58f2991525c66888e1f3fb8d52eec3c72c1871cced50f[ALL] 02cc5eaa7bd1c37ebf8483c014d38a1608d06cf6be6d61bae64b697725bae7696b", + "hex": "483045022100a6c13e00f1fb53122424c35c581628830cf3b0cfab600397e7d3b6dd6d8aa89a022071add87aaa08239f30d58f2991525c66888e1f3fb8d52eec3c72c1871cced50f012102cc5eaa7bd1c37ebf8483c014d38a1608d06cf6be6d61bae64b697725bae7696b" + }, + "sequence": 4294967295, + "txid": "7914c6f91f164a739a9fcbaf6aab222aaf3662789d790028c1db950d69e500e9", + "vout": 0 + } + ], + "vout": [ + { + "n": 0, + "scriptPubKey": { + "addresses": [ + "FJ6KDvWCeaiNdger53eWCwiLSZgjEPMHxf" + ], + "asm": "OP_DUP OP_HASH160 868190f1c3c9839fc34f994a44898739624fcfbc OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914868190f1c3c9839fc34f994a44898739624fcfbc88ac", + "reqSigs": 1, + "type": "pubkeyhash" + }, + "value": 0.005 + }, + { + "n": 1, + "scriptPubKey": { + "addresses": [ + "FNmnKBw4PuXMdPoCv6v8DvnEKS3bM5W6eX" + ], + "asm": "OP_DUP OP_HASH160 b9d8e7f578161fb3fa16a061abe412926cbfc854 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914b9d8e7f578161fb3fa16a061abe412926cbfc85488ac", + "reqSigs": 1, + "type": "pubkeyhash" + }, + "value": 0.099 + } + ], + "vsize": 415 + } + } + ], + "result": "ok" +} +``` +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getLatestTransactionDetails') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getLatestTransactionDetails'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` +### Latest Block Transactions +Get information about block transactions +``` + /api/v1.0/getBlockTransactions/ +``` +Output: +``` + +{ + + "blockKeyword": "3454503", + "result": "ok", + "transactions": { + "b57cf412c8cb16e473d04bae44214705c64d2c25146be22695bf1ac36e166ee0": { + "parsedFloData": { + "contractName": "india-elections-2019", + "triggerCondition": "narendra modi wins", + "type": "smartContractPays" + }, + "transactionDetails": { + "blockhash": "5036019f878cdd22cbd1563cc25b999fe103931a7784a6d67c9f6548f44076a3", + "blockheight": 3454503, + "blocktime": 1558862083, + "confirmations": 1342505, + "fees": 0.0005, + "floData": "india-elections-2019@ winning-choice:'narendra modi wins'", + "locktime": 0, + "size": 283, + "time": 1558862083, + "txid": "b57cf412c8cb16e473d04bae44214705c64d2c25146be22695bf1ac36e166ee0", + "valueIn": 0.001, + "valueOut": 0.0005, + "version": 2, + "vin": [ + { + "addr": "FRwwCqbP7DN4z5guffzzhCSgpD8Q33hUG8", + "confirmations": null, + "doubleSpentTxID": null, + "isConfirmed": null, + "n": 0, + "scriptSig": { + "asm": "304402206c414be1ad91eb32603c696b2fde7836585573371f25be742b1a88fa9215a8fd0220035932fe3286973267c3e9467bad6ca7237bcd0aa8a4e6607b076274ad5e78cd01 03eb349253a45fe086cf9594825d1dbd7bf50b348bc1cccb921485acf06811b982", + "hex": "47304402206c414be1ad91eb32603c696b2fde7836585573371f25be742b1a88fa9215a8fd0220035932fe3286973267c3e9467bad6ca7237bcd0aa8a4e6607b076274ad5e78cd012103eb349253a45fe086cf9594825d1dbd7bf50b348bc1cccb921485acf06811b982" + }, + "sequence": 4294967295, + "txid": "6a49fd8fcdf557845788ac4d578568aac7c675c27ac3a57a09807a55ce9b56fa", + "unconfirmedInput": null, + "value": 0.001, + "valueSat": 100000, + "vout": 0 + } + ], + "vout": [ + { + "n": 0, + "scriptPubKey": { + "addresses": [ + "F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1" + ], + "asm": "OP_DUP OP_HASH160 15b3eb460d593f74775167d589a3a443eb78b55b OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a91415b3eb460d593f74775167d589a3a443eb78b55b88ac", + "type": "pubkeyhash" + }, + "spentHeight": null, + "spentIndex": null, + "spentTxId": null, + "value": "0.00010000" + }, + { + "n": 1, + "scriptPubKey": { + "addresses": [ + "FRwwCqbP7DN4z5guffzzhCSgpD8Q33hUG8" + ], + "asm": "OP_DUP OP_HASH160 dcacdd4e7c523b9ee05e980e9fc2ced5fb343669 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914dcacdd4e7c523b9ee05e980e9fc2ced5fb34366988ac", + "type": "pubkeyhash" + }, + "spentHeight": null, + "spentIndex": null, + "spentTxId": null, + "value": "0.00040000" + } + ] + } + } + } + +} +``` +Python example code +``` +response = requests.get('https://ranchimallflo.duckdns.org/api/v1.0/getBlockTransactions/') +if response.status_code == 200: + json_response = response.json() +elif response.status_code == 404: + print('Not Found.') +``` + +Javascript example code +``` +fetch('https://ranchimallflo.duckdns.org/api/v1.0/getBlockTransactions/'){ + .then(response => response.json()) + .then(data => console.log(data)); +} +``` diff --git a/src/api/config-example.py b/src/api/config-example.py new file mode 100644 index 0000000..66b24e0 --- /dev/null +++ b/src/api/config-example.py @@ -0,0 +1,8 @@ +dbfolder = '' +debug_status = False +sse_pubKey = '' +apiUrl = 'https://flosight.duckdns.org/api/' + +# your apilayer.net access key +apilayerAccesskey = '' + diff --git a/src/api/fetchRates.py b/src/api/fetchRates.py new file mode 100644 index 0000000..379c5a7 --- /dev/null +++ b/src/api/fetchRates.py @@ -0,0 +1,150 @@ +import requests +import json +import sqlite3 +import os +from config import * +import requests +import json +import sqlite3 +import os +from config import * +import time + +RETRY_TIMEOUT_DB = 60 # 60 sec +RETRY_TIMEOUT_REQUEST = 10 * 60 # 10 minsd + +prices = {} +# 1. fetch old price data if its there, else create an empty db +def connect_database(): + if not os.path.isfile(f"system.db"): + # create an empty db + while True: + try: + conn = sqlite3.connect('system.db') + c = conn.cursor() + c.execute('''CREATE TABLE ratepairs + (id integer primary key, ratepair text, price real)''') + c.execute("INSERT INTO ratepairs(ratepair, price) VALUES ('BTCBTC', 1)") + c.execute("INSERT INTO ratepairs(ratepair, price) VALUES ('BTCUSD', -1)") + c.execute("INSERT INTO ratepairs(ratepair, price) VALUES ('BTCINR', -1)") + c.execute("INSERT INTO ratepairs(ratepair, price) VALUES ('USDINR', -1)") + c.execute("INSERT INTO ratepairs(ratepair, price) VALUES ('FLOUSD', -1)") + conn.commit() + conn.close() + except: + print(f"Unable to create system.db, retrying in {RETRY_TIMEOUT_DB} sec") + time.sleep(RETRY_TIMEOUT_DB) + else: + break + + + # load old price data + # load older price data + global prices + while True: + try: + conn = sqlite3.connect('system.db') + c = conn.cursor() + ratepairs = c.execute('select ratepair, price from ratepairs') + ratepairs = ratepairs.fetchall() + for ratepair in ratepairs: + ratepair = list(ratepair) + prices[ratepair[0]] = ratepair[1] + except: + print(f"Unable to read system.db, retrying in {RETRY_TIMEOUT_DB} sec") + time.sleep(RETRY_TIMEOUT_DB) + else: + break + +# 2. fetch new price data +def fetch_newprice(): + global prices + while True: + try: + # apilayer + response = requests.get(f"http://apilayer.net/api/live?access_key={apilayerAccesskey}") + try: + price = response.json() + prices['USDINR'] = price['quotes']['USDINR'] + break + except ValueError: + print('Json parse error. retrying in {RETRY_TIMEOUT_REQUEST} sec') + time.sleep(RETRY_TIMEOUT_REQUEST) + except: + print(f"Unable to fetch new price data, retrying in {RETRY_TIMEOUT_REQUEST} sec") + time.sleep(RETRY_TIMEOUT_REQUEST) + + +def fetch_bitpay_or_coindesk(): + # bitpay + global prices + while True: + print("Trying bitpay API") + try: + response = requests.get('https://bitpay.com/api/rates') + bitcoinRates = response.json() + for currency in bitcoinRates: + if currency['code'] == 'USD': + prices['BTCUSD'] = currency['rate'] + elif currency['code'] == 'INR': + prices['BTCINR'] = currency['rate'] + except ValueError: + print("Json parse error in bitpay") + except: + print(f"Unable to fetch bitpay") + else: + break # if data is accrued from bitpay, break from loop and procees to next process + print("Trying coindesk API") + # coindesk + try: + response = requests.get('https://api.coindesk.com/v1/bpi/currentprice.json') + price = response.json() + prices['BTCUSD'] = price['bpi']['USD']['rate'] + except ValueError: + print(f'Json parse error in coindesk') + except: + print(f"Unable to fetch coindesk") + else: + break # if data is accrued from coindesk, break from loop and procees to next process + + print(f"Retrying in {RETRY_TIMEOUT_REQUEST} sec") + time.sleep(RETRY_TIMEOUT_REQUEST) + + +# cryptocompare +def fetch_cryptocompare(): + while True: + try: + response = requests.get('https://min-api.cryptocompare.com/data/histoday?fsym=FLO&tsym=USD&limit=1&aggregate=3&e=CCCAGG') + price = response.json() + prices['FLOUSD'] = price['Data'][-1]['close'] + except ValueError: + print(f'Json parse error in cryptocompare, retrying in {RETRY_TIMEOUT_REQUEST} sec') + except: + print(f"Unable to fetch cryptocompare, retrying in {RETRY_TIMEOUT_REQUEST} sec") + else: + break # if data is accrued from coindesk, break from loop and procees to next process + +# 3. update latest price data +def update_latest_prices(): + while True: + try: + conn = sqlite3.connect('system.db') + c = conn.cursor() + for pair in list(prices.items()): + pair = list(pair) + c.execute(f"UPDATE ratepairs SET price={pair[1]} WHERE ratepair='{pair[0]}'") + conn.commit() + except: + print(f"Unable to write to system.db, retrying in {RETRY_TIMEOUT_DB} sec") + time.sleep(RETRY_TIMEOUT_DB) + else: + break + +connect_database() +fetch_newprice() +fetch_bitpay_or_coindesk() +fetch_cryptocompare() +print('\n\n') +print(prices) +update_latest_prices() \ No newline at end of file diff --git a/src/api/parsing.py b/src/api/parsing.py new file mode 100644 index 0000000..9395ac3 --- /dev/null +++ b/src/api/parsing.py @@ -0,0 +1,1240 @@ +import pdb +import re +import arrow +import pyflo +import logging +import json + +""" +Find make lists of #, *, @ words + +If only 1 hash word and nothing else, then it is token related ( tokencreation or tokentransfer ) + +If @ is present, then we know it is smart contract related + @ (#)pre: - participation , deposit + @ * (#)pre: - one time event creation + @ * (# #)post: - token swap creation + @ - trigger + +Check for 1 @ only +Check for 1 # only +Check for @ (#)pre: +Check for @ * (#)pre: +Check for @ * (# #)post: + +special_character_frequency = { + 'precolon': { + '#':0, + '*':0, + '@':0, + ':':0 +} + +for word in allList: + if word.endswith('#'): + special_character_frequency['#'] = special_character_frequency['#'] + 1 + elif word.endswith('*'): + special_character_frequency['*'] = special_character_frequency['*'] + 1 + elif word.endswith('@'): + special_character_frequency['@'] = special_character_frequency['@'] + 1 + elif word.endswith(':'): + special_character_frequency[':'] = special_character_frequency[':'] + 1 + +""" + +''' +def className(rawstring): + # Create a list that contains @ , # , * and : ; in actual order of occurence with their words. Only : is allowed to exist without a word in front of it. + # Check for 1 @ only followed by :, and the class is trigger + # Check for 1 # only, then the class is tokensystem + # Check for @ in the first position, * in the second position, # in the third position and : in the fourth position, then class is one time event creation + # Check for @ in the first position, * in the second position and : in the third position, then hash is in 4th position, then hash in 5th position | Token swap creation + + allList = findrules(rawstring,['#','*','@',':']) + + pattern_list1 = ['rmt@','rmt*',':',"rmt#","rmt#"] + pattern_list2 = ['rmt#',':',"rmt@"] + pattern_list3 = ['rmt#'] + pattern_list4 = ["rmt@","one-time-event*","floAddress$",':',"rupee#","bioscope#"] + patternmatch = find_first_classification(pattern_list4, search_patterns) + print(f"Patternmatch is {patternmatch}") + + +rawstring = "test rmt# rmt@ rmt* : rmt# rmt# test" +#className(rawstring) ''' + +# Variable configurations +search_patterns = { + 'tokensystem-C':{ + 1:['#'] + }, + 'smart-contract-creation-C':{ + 1:['@','*','#','$',':'], + 2:['@','*','#','$',':','#'] + }, + 'smart-contract-participation-deposit-C':{ + 1:['#','@',':'], + 2:['#','@','$',':'] + }, + 'userchoice-trigger':{ + 1:['@'] + }, + 'smart-contract-participation-ote-ce-C':{ + 1:['#','@'], + 2:['#','@','$'] + }, + 'smart-contract-creation-ce-tokenswap':{ + 1:['@','*','$',':','#','#'] + } +} + +conflict_matrix = { + 'tokensystem-C':{ + # Check for send, check for create, if both are there noise, else conflict resolved + 'tokentransfer', + 'tokencreation' + }, + 'smart-contract-creation-C':{ + # Check contract-conditions for userchoice, if present then userchoice contract, else time based contract + 'creation-one-time-event-userchoice', + 'creation-one-time-event-timebased' + }, + 'smart-contract-participation-deposit-C':{ + # Check *-word, its either one-time-event or a continuos-event + 'participation-one-time-event-userchoice', + 'deposit-continuos-event-tokenswap' + }, + 'smart-contract-participation-ote-ce-C':{ + # Check *-word, its either one-time-event or a continuos-event + 'participation-one-time-event-timebased', + 'participation-continuos-event-tokenswap' + } +} + +months = { + 'jan': 1, + 'feb': 2, + 'mar': 3, + 'apr': 4, + 'may': 5, + 'jun': 6, + 'jul': 7, + 'aug': 8, + 'sep': 9, + 'oct': 10, + 'nov': 11, + 'dec': 12 +} + +# HELPER FUNCTIONS + +# Find some value or return as noise +def apply_rule1(*argv): + a = argv[0](*argv[1:]) + if a is False: + return False + else: + return a + + +def extract_substing_between(test_str, sub1, sub2): + # getting index of substrings + idx1 = test_str.index(sub1) + idx2 = test_str.index(sub2) + + # length of substring 1 is added to + # get string from next character + res = test_str[idx1 + len(sub1) + 1: idx2] + + # return result + return res + +# StateF functions +def isStateF(text): + try: + statef_string = extract_substing_between(text, 'statef', 'end-statef').strip() + i=iter(statef_string.split(":")) + statef_list = [":".join(x) for x in zip(i,i)] + statef = {} + for keyval in statef_list: + keyval = keyval.split(':') + statef[keyval[0]] = keyval[1] + return statef + except: + return False + + +# conflict_list = [['userchoice','payeeaddress'],['userchoice','xxx']] +def resolve_incategory_conflict(input_dictionary , conflict_list): + for conflict_pair in conflict_list: + key0 = conflict_pair[0] + key1 = conflict_pair[1] + dictionary_keys = input_dictionary.keys() + if (key0 in dictionary_keys and key1 in dictionary_keys) or (key0 not in dictionary_keys and key1 not in dictionary_keys): + return False + else: + return True + + +def remove_empty_from_dict(d): + if type(d) is dict: + return dict((k, remove_empty_from_dict(v)) for k, v in d.items() if v and remove_empty_from_dict(v)) + elif type(d) is list: + return [remove_empty_from_dict(v) for v in d if v and remove_empty_from_dict(v)] + else: + return d + + +def outputreturn(*argv): + if argv[0] == 'noise': + parsed_data = {'type': 'noise'} + return parsed_data + elif argv[0] == 'token_incorporation': + parsed_data = { + 'type': 'tokenIncorporation', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'tokenAmount': argv[3], #initTokens + 'stateF': argv[4] + } + return parsed_data + elif argv[0] == 'token_transfer': + parsed_data = { + 'type': 'transfer', + 'transferType': 'token', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'tokenAmount': argv[3], #amount + 'stateF': argv[4] + } + return parsed_data + elif argv[0] == 'one-time-event-userchoice-smartcontract-incorporation': + parsed_data = { + 'type': 'smartContractIncorporation', + 'contractType': 'one-time-event', + 'tokenIdentification': argv[1], #hashList[0][:-1] + 'contractName': argv[2], #atList[0][:-1] + 'contractAddress': argv[3], #contractaddress[:-1] + 'flodata': argv[4], #string + 'contractConditions': { + 'contractAmount' : argv[5], + 'minimumsubscriptionamount' : argv[6], + 'maximumsubscriptionamount' : argv[7], + 'userchoices' : argv[8], + 'expiryTime' : argv[9] + }, + 'stateF': argv[10] + } + return remove_empty_from_dict(parsed_data) + elif argv[0] == 'one-time-event-userchoice-smartcontract-participation': + parsed_data = { + 'type': 'transfer', + 'transferType': 'smartContract', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'operation': 'transfer', + 'tokenAmount': argv[3], #amount + 'contractName': argv[4], #atList[0][:-1] + 'contractAddress': argv[5], + 'userChoice': argv[6], #userChoice + 'stateF': argv[7] + } + return remove_empty_from_dict(parsed_data) + elif argv[0] == 'one-time-event-userchoice-smartcontract-trigger': + parsed_data = { + 'type': 'smartContractPays', + 'contractName': argv[1], #atList[0][:-1] + 'triggerCondition': argv[2], #triggerCondition.group().strip()[1:-1] + 'stateF': argv[3] + } + return parsed_data + elif argv[0] == 'one-time-event-time-smartcontract-incorporation': + parsed_data = { + 'type': 'smartContractIncorporation', + 'contractType': 'one-time-event', + 'tokenIdentification': argv[1], #hashList[0][:-1] + 'contractName': argv[2], #atList[0][:-1] + 'contractAddress': argv[3], #contractaddress[:-1] + 'flodata': argv[4], #string + 'contractConditions': { + 'contractAmount' : argv[5], + 'minimumsubscriptionamount' : argv[6], + 'maximumsubscriptionamount' : argv[7], + 'payeeAddress' : argv[8], + 'expiryTime' : argv[9] + }, + 'stateF': argv[10] + } + return remove_empty_from_dict(parsed_data) + elif argv[0] == 'continuos-event-token-swap-incorporation': + parsed_data = { + 'type': 'smartContractIncorporation', + 'contractType': 'continuos-event', + 'tokenIdentification': argv[1], #hashList[0][:-1] + 'contractName': argv[2], #atList[0][:-1] + 'contractAddress': argv[3], #contractaddress[:-1] + 'flodata': argv[4], #string + 'contractConditions': { + 'subtype' : argv[5], #tokenswap + 'accepting_token' : argv[6], + 'selling_token' : argv[7], + 'pricetype' : argv[8], + 'price' : argv[9], + }, + 'stateF': argv[10] + } + return parsed_data + elif argv[0] == 'continuos-event-token-swap-deposit': + parsed_data = { + 'type': 'smartContractDeposit', + 'tokenIdentification': argv[1], #hashList[0][:-1] + 'depositAmount': argv[2], #depositAmount + 'contractName': argv[3], #atList[0][:-1] + 'flodata': argv[4], #string + 'depositConditions': { + 'expiryTime' : argv[5] + }, + 'stateF': argv[6] + } + return parsed_data + elif argv[0] == 'smart-contract-one-time-event-continuos-event-participation': + parsed_data = { + 'type': 'transfer', + 'transferType': 'smartContract', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'tokenAmount': argv[3], #amount + 'contractName': argv[4], #atList[0][:-1] + 'contractAddress': argv[5], + 'stateF': argv[6] + } + return remove_empty_from_dict(parsed_data) + elif argv[0] == 'nft_create': + parsed_data = { + 'type': 'nftIncorporation', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'tokenAmount': argv[3], #initTokens, + 'nftHash': argv[4], #nftHash + 'stateF': argv[5] + } + return parsed_data + elif argv[0] == 'nft_transfer': + parsed_data = { + 'type': 'transfer', + 'transferType': 'nft', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'tokenAmount': argv[3], #initTokens, + 'stateF': argv[4] + } + return parsed_data + elif argv[0] == 'infinite_token_create': + parsed_data = { + 'type': 'infiniteTokenIncorporation', + 'flodata': argv[1], #string + 'tokenIdentification': argv[2], #hashList[0][:-1] + 'stateF': argv[3] + } + return parsed_data + + +def extract_specialcharacter_words(rawstring, special_characters): + wordList = [] + for word in rawstring.split(' '): + if (len(word) not in [0,1] or word==":") and word[-1] in special_characters: + wordList.append(word) + return wordList + + +def extract_contract_conditions(text, contract_type, marker=None, blocktime=None): + try: + rulestext = extract_substing_between(text, 'contract-conditions', 'end-contract-conditions') + except: + return False + if rulestext.strip()[0] == ':': + rulestext = rulestext.strip()[1:].strip() + #rulestext = re.split('contract-conditions:\s*', text)[-1] + # rulelist = re.split('\d\.\s*', rulestext) + rulelist = [] + numberList = re.findall(r'\(\d\d*\)', rulestext) + + for idx, item in enumerate(numberList): + numberList[idx] = int(item[1:-1]) + + numberList = sorted(numberList) + for idx, item in enumerate(numberList): + if numberList[idx] + 1 != numberList[idx + 1]: + logger.info('Contract condition numbers are not in order') + return False + if idx == len(numberList) - 2: + break + + for i in range(len(numberList)): + rule = rulestext.split('({})'.format(i + 1))[1].split('({})'.format(i + 2))[0] + rulelist.append(rule.strip()) + + if contract_type == 'one-time-event': + extractedRules = {} + for rule in rulelist: + if rule == '': + continue + elif rule[:10] == 'expirytime': + expirytime = re.split('expirytime[\s]*=[\s]*', rule)[1].strip() + try: + expirytime_split = expirytime.split(' ') + parse_string = '{}/{}/{} {}'.format(expirytime_split[3], months[expirytime_split[1]], expirytime_split[2], expirytime_split[4]) + expirytime_object = arrow.get(parse_string, 'YYYY/M/D HH:mm:ss').replace(tzinfo=expirytime_split[5]) + blocktime_object = arrow.get(blocktime) + if expirytime_object < blocktime_object: + logger.info('Expirytime of the contract is earlier than the block it is incorporated in. This incorporation will be rejected ') + return False + extractedRules['expiryTime'] = expirytime + except: + logger.info('Error parsing expiry time') + return False + + for rule in rulelist: + if rule == '': + continue + elif rule[:14] == 'contractamount': + pattern = re.compile('(^contractamount\s*=\s*)(.*)') + searchResult = pattern.search(rule).group(2) + contractamount = searchResult.split(marker)[0] + try: + extractedRules['contractAmount'] = float(contractamount) + except: + logger.info("Contract amount entered is not a decimal") + elif rule[:11] == 'userchoices': + pattern = re.compile('(^userchoices\s*=\s*)(.*)') + conditions = pattern.search(rule).group(2) + conditionlist = conditions.split('|') + extractedRules['userchoices'] = {} + for idx, condition in enumerate(conditionlist): + extractedRules['userchoices'][idx] = condition.strip() + elif rule[:25] == 'minimumsubscriptionamount': + pattern = re.compile('(^minimumsubscriptionamount\s*=\s*)(.*)') + searchResult = pattern.search(rule).group(2) + minimumsubscriptionamount = searchResult.split(marker)[0] + try: + extractedRules['minimumsubscriptionamount'] = float(minimumsubscriptionamount) + except: + logger.info("Minimum subscription amount entered is not a decimal") + elif rule[:25] == 'maximumsubscriptionamount': + pattern = re.compile('(^maximumsubscriptionamount\s*=\s*)(.*)') + searchResult = pattern.search(rule).group(2) + maximumsubscriptionamount = searchResult.split(marker)[0] + try: + extractedRules['maximumsubscriptionamount'] = float(maximumsubscriptionamount) + except: + logger.info("Maximum subscription amount entered is not a decimal") + elif rule[:12] == 'payeeaddress': + pattern = re.compile('(^payeeaddress\s*=\s*)(.*)') + searchResult = pattern.search(rule).group(2) + payeeAddress = searchResult.split(marker)[0] + extractedRules['payeeAddress'] = payeeAddress + + if len(extractedRules) > 1 and 'expiryTime' in extractedRules: + return extractedRules + else: + return False + + elif contract_type == 'continuous-event': + extractedRules = {} + for rule in rulelist: + if rule == '': + continue + elif rule[:7] == 'subtype': + # todo : recheck the regular expression for subtype, find an elegant version which covers all permutations and combinations + pattern = re.compile('(^subtype\s*=\s*)(.*)') + subtype = pattern.search(rule).group(2) + extractedRules['subtype'] = subtype + elif rule[:15] == 'accepting_token': + pattern = re.compile('(?<=accepting_token\s=\s)(.*)(? 1: + return extractedRules + else: + return False + return False + + +def extract_tokenswap_contract_conditions(processed_text, contract_type, contract_token): + rulestext = re.split('contract-conditions:\s*', processed_text)[-1] + rulelist = [] + numberList = re.findall(r'\(\d\d*\)', rulestext) + + for idx, item in enumerate(numberList): + numberList[idx] = int(item[1:-1]) + + numberList = sorted(numberList) + for idx, item in enumerate(numberList): + if numberList[idx] + 1 != numberList[idx + 1]: + logger.info('Contract condition numbers are not in order') + return False + if idx == len(numberList) - 2: + break + + for i in range(len(numberList)): + rule = rulestext.split('({})'.format(i + 1))[1].split('({})'.format(i + 2))[0] + rulelist.append(rule.strip()) + + if contract_type == 'continuous-event': + extractedRules = {} + for rule in rulelist: + if rule == '': + continue + elif rule[:7] == 'subtype': + # todo : recheck the regular expression for subtype, find an elegant version which covers all permutations and combinations + pattern = re.compile('(^subtype\s*=\s*)(.*)') + searchResult = pattern.search(rule).group(2) + subtype = searchResult.split(marker)[0] + #extractedRules['subtype'] = rule.split('=')[1].strip() + extractedRules['subtype'] = subtype + elif rule[:15] == 'accepting_token': + pattern = re.compile('(?<=accepting_token\s=\s).*(? 1: + return extractedRules + else: + return False + + return False + + +def extract_deposit_conditions(text, blocktime=None): + rulestext = re.split('deposit-conditions:\s*', text)[-1] + # rulelist = re.split('\d\.\s*', rulestext) + rulelist = [] + numberList = re.findall(r'\(\d\d*\)', rulestext) + for idx, item in enumerate(numberList): + numberList[idx] = int(item[1:-1]) + + numberList = sorted(numberList) + for idx, item in enumerate(numberList): + if len(numberList) > 1 and numberList[idx] + 1 != numberList[idx + 1]: + logger.info('Deposit condition numbers are not in order') + return False + if idx == len(numberList) - 2: + break + + for i in range(len(numberList)): + rule = rulestext.split('({})'.format(i + 1))[1].split('({})'.format(i + 2))[0] + rulelist.append(rule.strip()) + + # elif contracttype == 'continuous-event*': + extractedRules = {} + for rule in rulelist: + if rule == '': + continue + elif rule[:10] == 'expirytime': + expirytime = re.split('expirytime[\s]*=[\s]*', rule)[1].strip() + try: + expirytime_split = expirytime.split(' ') + parse_string = '{}/{}/{} {}'.format(expirytime_split[3], months[expirytime_split[1]], expirytime_split[2], expirytime_split[4]) + expirytime_object = arrow.get(parse_string, 'YYYY/M/D HH:mm:ss').replace(tzinfo=expirytime_split[5]) + blocktime_object = arrow.get(blocktime) + if expirytime_object < blocktime_object: + logger.info('Expirytime of the contract is earlier than the block it is incorporated in. This incorporation will be rejected ') + return False + extractedRules['expiryTime'] = expirytime + except: + logger.info('Error parsing expiry time') + return False + + """for rule in rulelist: + if rule == '': + continue + elif rule[:7] == 'subtype': + subtype=rule[8:] + #pattern = re.compile('[^subtype\s*=\s*].*') + #searchResult = pattern.search(rule).group(0) + #contractamount = searchResult.split(marker)[0] + extractedRules['subtype'] = subtype """ + + if len(extractedRules) > 0: + return extractedRules + else: + return False + + +def extract_special_character_word(special_character_list, special_character): + for word in special_character_list: + if word.endswith(special_character): + return word[:-1] + return False + + +def extract_NFT_hash(clean_text): + nft_hash = re.search(r"(?:0[xX])?[0-9a-fA-F]{64}",clean_text) + if nft_hash is None: + return False + else: + return nft_hash.group(0) + + +def find_original_case(contract_address, original_text): + dollar_word = extract_specialcharacter_words(original_text,["$"]) + if len(dollar_word)==1 and dollar_word[0][:-1].lower()==contract_address: + return dollar_word[0][:-1] + else: + None + + +def find_word_index_fromstring(originaltext, word): + lowercase_text = originaltext.lower() + result = lowercase_text.find(word) + return originaltext[result:result+len(word)] + + +def find_first_classification(parsed_word_list, search_patterns): + for first_classification in search_patterns.keys(): + counter = 0 + for key in search_patterns[first_classification].keys(): + if checkSearchPattern(parsed_word_list, search_patterns[first_classification][key]): + return {'categorization':f"{first_classification}",'key':f"{key}",'pattern':search_patterns[first_classification][key], 'wordlist':parsed_word_list} + return {'categorization':"noise"} + + +def sort_specialcharacter_wordlist(inputlist): + weight_values = { + '@': 5, + '*': 4, + '#': 3, + '$': 2 + } + + weightlist = [] + for word in inputlist: + if word.endswith("@"): + weightlist.append(5) + elif word.endswith("*"): + weightlist.append(4) + elif word.endswith("#"): + weightlist.append(4) + elif word.endswith("$"): + weightlist.append(4) + + +def firstclassification_rawstring(rawstring): + specialcharacter_wordlist = extract_specialcharacter_words(rawstring,['@','*','$','#',':']) + first_classification = find_first_classification(specialcharacter_wordlist, search_patterns) + return first_classification + + +def checkSearchPattern(parsed_list, searchpattern): + if len(parsed_list)!=len(searchpattern): + return False + else: + for idx,val in enumerate(parsed_list): + if not parsed_list[idx].endswith(searchpattern[idx]): + return False + return True + + +def extractAmount_rule_new(text): + base_units = {'thousand': 10 ** 3, 'k': 10 ** 3, 'million': 10 ** 6, 'm': 10 ** 6, 'billion': 10 ** 9, 'b': 10 ** 9, 'trillion': 10 ** 12, 'lakh':10 ** 5, 'crore':10 ** 7, 'quadrillion':10 ** 15} + amount_tuple = re.findall(r'\b(-?[.\d]+)\s*(thousand|million|billion|trillion|m|b|t|k|lakh|crore|quadrillion)*\b', text) + if len(amount_tuple) > 1 or len(amount_tuple) == 0: + return False + else: + amount_tuple_list = list(amount_tuple[0]) + extracted_amount = float(amount_tuple_list[0]) + extracted_base_unit = amount_tuple_list[1] + if extracted_base_unit in base_units.keys(): + extracted_amount = float(extracted_amount) * base_units[extracted_base_unit] + return extracted_amount + +def extractAmount_rule_new1(text, split_word=None, split_direction=None): + base_units = {'thousand': 10 ** 3, 'k': 10 ** 3, 'million': 10 ** 6, 'm': 10 ** 6, 'billion': 10 ** 9, 'b': 10 ** 9, 'trillion': 10 ** 12, 'lakh':10 ** 5, 'crore':10 ** 7, 'quadrillion':10 ** 15} + if split_word and split_direction: + if split_direction=='pre': + text = text.split(split_word)[0] + if split_direction=='post': + text = text.split(split_word)[1] + + # appending dummy because the regex does not recognize a number at the start of a string + # text = f"dummy {text}" + text = text.replace("'", "") + text = text.replace('"', '') + amount_tuple = re.findall(r'\b(-?[.\d]+)\s*(thousand|million|billion|trillion|m|b|t|k|lakh|crore|quadrillion)*\b', text) + if len(amount_tuple) > 1 or len(amount_tuple) == 0: + return False + else: + amount_tuple_list = list(amount_tuple[0]) + extracted_amount = float(amount_tuple_list[0]) + extracted_base_unit = amount_tuple_list[1] + if extracted_base_unit in base_units.keys(): + extracted_amount = float(extracted_amount) * base_units[extracted_base_unit] + return extracted_amount + + +def extract_userchoice(text): + result = re.split('userchoice:\s*', text) + if len(result) != 1 and result[1] != '': + return result[1].strip().strip('"').strip("'") + else: + return False + + +def findWholeWord(w): + return re.compile(r'\b({0})\b'.format(w), flags=re.IGNORECASE).search + + +def check_flo_address(floaddress, is_testnet): + if pyflo.is_address_valid(floaddress, testnet=is_testnet): + return floaddress + else: + return False + + +def extract_trigger_condition(text): + searchResult = re.search('\".*\"', text) + if searchResult is None: + searchResult = re.search('\'.*\'', text) + + if searchResult is not None: + return searchResult.group().strip()[1:-1] + else: + return False + + +# Regex pattern for Smart Contract and Token name ^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$ +def check_regex(pattern, test_string): + matched = re.match(pattern, test_string) + is_match = bool(matched) + return is_match + + +def check_existence_of_keyword(inputlist, keywordlist): + for word in keywordlist: + if not word in inputlist: + return False + return True + + +def check_word_existence_instring(word, text): + word_exists = re.search(fr"\b{word}\b",text) + if word_exists is None: + return False + else: + return word_exists.group(0) + +send_category = ['transfer', 'send', 'give'] # keep everything lowercase +create_category = ['incorporate', 'create', 'start'] # keep everything lowercase +deposit_category = ['submit','deposit'] + + +def truefalse_rule2(rawstring, permitted_list, denied_list): + # Find transfer , send , give + foundPermitted = None + foundDenied = None + + for word in permitted_list: + if findWholeWord(word)(rawstring): + foundPermitted = word + break + + for word in denied_list: + if findWholeWord(word)(rawstring): + foundDenied = word + break + + if (foundPermitted is not None) and (foundDenied is None): + return True + else: + return False + + +def selectCategory(rawstring, category1, category2): + foundCategory1 = None + foundCategory2 = None + + for word in category1: + if findWholeWord(word)(rawstring): + foundCategory1 = word + break + + for word in category2: + if findWholeWord(word)(rawstring): + foundCategory2 = word + break + + if ((foundCategory1 is not None) and (foundCategory2 is not None)) or ((foundCategory1 is None) and (foundCategory2 is None)): + return False + elif foundCategory1 is not None: + return 'category1' + elif foundCategory2 is not None: + return 'category2' + + +def select_category_reject(rawstring, category1, category2, reject_list): + foundCategory1 = None + foundCategory2 = None + rejectCategory = None + + for word in category1: + if findWholeWord(word)(rawstring): + foundCategory1 = word + break + + for word in category2: + if findWholeWord(word)(rawstring): + foundCategory2 = word + break + + for word in reject_list: + if findWholeWord(word)(rawstring): + rejectCategory = word + break + + if ((foundCategory1 is not None) and (foundCategory2 is not None)) or ((foundCategory1 is None) and (foundCategory2 is None)) or (rejectCategory is not None): + return False + elif foundCategory1 is not None: + return 'category1' + elif foundCategory2 is not None: + return 'category2' + + +def text_preprocessing(original_text): + # strip white spaces at the beginning and end + processed_text = original_text.strip() + # remove tab spaces + processed_text = re.sub('\t', ' ', processed_text) + # remove new lines/line changes + processed_text = re.sub('\n', ' ', processed_text) + # add a white space after every special character found + processed_text = re.sub("contract-conditions:", "contract-conditions: ", processed_text) + processed_text = re.sub("deposit-conditions:", "deposit-conditions: ", processed_text) + processed_text = re.sub("userchoice:", "userchoice: ", processed_text) + # remove extra whitespaces in between + processed_text = ' '.join(processed_text.split()) + processed_text = re.sub(' +', ' ', processed_text) + clean_text = processed_text + # make everything lowercase + processed_text = processed_text.lower() + + return clean_text,processed_text + + +# TODO - REMOVE SAMPLE TEXT +text_list = [ + "create 500 million rmt#", + + "transfer 200 rmt#", + + "Create Smart Contract with the name India-elections-2019@ of the type one-time-event* using the asset rmt# at the address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1$ with contract-conditions: (1) contractAmount=0.001rmt (2) userChoices=Narendra Modi wins| Narendra Modi loses (3) expiryTime= Wed May 22 2019 21:00:00 GMT+0530", + + "send 0.001 rmt# to india-elections-2019@ to FLO address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1 with the userchoice:'narendra modi wins'", + + "india-elections-2019@ winning-choice:'narendra modi wins'", + + "Create Smart Contract with the name India-elections-2019@ of the type one-time-event* using the asset rmt# at the address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1$ with contract-conditions: (1) contractAmount=0.001rmt (2) expiryTime= Wed May 22 2019 21:00:00 GMT+0530", + + "send 0.001 rmt# to india-elections-2019@ to FLO address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1", + + "Create Smart Contract with the name swap-rupee-bioscope@ of the type continuous-event* at the address oRRCHWouTpMSPuL6yZRwFCuh87ZhuHoL78$ with contract-conditions : (1) subtype = tokenswap (2) accepting_token = rupee# (3) selling_token = bioscope# (4) price = '15' (5) priceType = ‘predetermined’ (6) direction = oneway", + + "Deposit 15 bioscope# to swap-rupee-bioscope@ its FLO address being oRRCHWouTpMSPuL6yZRwFCuh87ZhuHoL78$ with deposit-conditions: (1) expiryTime= Wed Nov 17 2021 21:00:00 GMT+0530 ", + + "Send 15 rupee# to swap-rupee-article@ its FLO address being FJXw6QGVVaZVvqpyF422Aj4FWQ6jm8p2dL$", + + "send 0.001 rmt# to india-elections-2019@ to FLO address F7osBpjDDV1mSSnMNrLudEQQ3cwDJ2dPR1 with the userchoice:'narendra modi wins'" +] + +text_list1 = [ + + 'create usd# as infinite-token', + 'transfer 10 usd#', + + 'Create 100 albumname# as NFT with 2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824 as asset hash', + 'Transfer 10 albumname# nft', + + 'Create 400 rmt#', + 'Transfer 20 rmt#' +] + +text_list2 = [ + '''Create Smart Contract with the name swap-rupee-bioscope@ of the type continuous-event* + at the address stateF=bitcoin_price_source:bitpay:usd_inr_exchange_source:bitpay end-stateF oYzeeUBWRpzRuczW6myh2LHGnXPyR2Bc6k$ with contract-conditions : + (1) subtype = tokenswap + (2) accepting_token = rupee# + (3) selling_token = sreeram# + (4) price = "15" + (5) priceType="predetermined" end-contract-conditions''', + + ''' + Create a smart contract of the name simple-crowd-fund@ of the type one-time-event* using asset bioscope# at the FLO address oQkpZCBcAWc945viKqFmJVbVG4aKY4V3Gz$ with contract-conditions:(1) expiryTime= Tue Sep 13 2022 16:10:00 GMT+0530 (2) payeeAddress=oQotdnMBAP1wZ6Kiofx54S2jNjKGiFLYD7 end-contract-conditions + ''', + + ''' + Create a smart contract of the name simple-crowd-fund@ of the type one-time-event* using asset bioscope# at the FLO address oQkpZCBcAWc945viKqFmJVbVG4aKY4V3Gz$ with contract-conditions:(1) expiryTime= Tue Sep 13 2022 16:10:00 GMT+0530 (2) payeeAddress=oU412TvcMe2ah2xzqFpA95vBJ1RoPZY1LR:10:oVq6QTUeNLh8sapQ6J6EjMQMKHxFCt3uAq:20:oLE79kdHPEZ2bxa3PwtysbJeLo9hvPgizU:60:ocdCT9RAzWVsUncMu24r3HXKXFCXD7gTqh:10 end-contract-conditions + ''', + ''' + Create a smart contract of the name simple-crowd-fund@ of the type one-time-event* using asset bioscope# at the FLO address oQkpZCBcAWc945viKqFmJVbVG4aKY4V3Gz$ with contract-conditions:(1) expiryTime= Tue Sep 13 2022 16:10:00 GMT+0530 (2) payeeAddress=oU412TvcMe2ah2xzqFpA95vBJ1RoPZY1LR end-contract-conditions + ''', + ''' + Create a smart contract of the name all-crowd-fund-7@ of the type one-time-event* using asset bioscope# at the FLO address oYX4GvBYtfTBNyUFRCdtYubu7ZS4gchvrb$ with contract-conditions:(1) expiryTime= Sun Nov 15 2022 12:30:00 GMT+0530 (2) payeeAddress=oQotdnMBAP1wZ6Kiofx54S2jNjKGiFLYD7:10:oMunmikKvxsMSTYzShm2X5tGrYDt9EYPij:20:oRpvvGEVKwWiMnzZ528fPhiA2cZA3HgXY5:30:oWpVCjPDGzaiVfEFHs6QVM56V1uY1HyCJJ:40 (3) minimumsubscriptionamount=1 (5) contractAmount=0.6 end-contract-conditions + ''', + ''' + Create a smart contract of the name all-crowd-fund-7@ of the type one-time-event* using asset bioscope# at the FLO address oYX4GvBYtfTBNyUFRCdtYubu7ZS4gchvrb$ with contract-conditions:(1) expiryTime= Sun Nov 15 2022 12:30:00 GMT+0530 (2) payeeAddress=oQotdnMBAP1wZ6Kiofx54S2jNjKGiFLYD7:0:oMunmikKvxsMSTYzShm2X5tGrYDt9EYPij:30:oRpvvGEVKwWiMnzZ528fPhiA2cZA3HgXY5:30:oWpVCjPDGzaiVfEFHs6QVM56V1uY1HyCJJ:40 (3) minimumsubscriptionamount=1 (4) contractAmount=0.6 end-contract-conditions + ''', + '''send 0.02 bioscope# to twitter-survive@ to FLO address oVbebBNuERWbouDg65zLfdataWEMTnsL8r with the userchoice: survives''', + ''' + Create a smart contract of the name twitter-survive@ of the type one-time-event* using asset bioscope# at the FLO address oVbebBNuERWbouDg65zLfdataWEMTnsL8r$ with contract-conditions:(1) expiryTime= Sun Nov 15 2022 14:55:00 GMT+0530 (2) userchoices= survives | dies (3) minimumsubscriptionamount=0.04 (4) maximumsubscriptionamount=1 (5) contractAmount=0.02 end-contract-conditions + ''', + ''' + create 0 teega# + ''' +] + + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +formatter = logging.Formatter('%(asctime)s:%(name)s:%(message)s') + +file_handler = logging.FileHandler('tracking.log') +file_handler.setLevel(logging.INFO) +file_handler.setFormatter(formatter) + +stream_handler = logging.StreamHandler() +stream_handler.setFormatter(formatter) + +logger.addHandler(file_handler) +logger.addHandler(stream_handler) + +def parse_flodata(text, blockinfo, net): + if net == 'testnet': + is_testnet = True + else: + is_testnet = False + + if text == '': + return outputreturn('noise') + + clean_text, processed_text = text_preprocessing(text) + # System state + print("Processing stateF") + stateF_mapping = isStateF(processed_text) + first_classification = firstclassification_rawstring(processed_text) + parsed_data = None + + if first_classification['categorization'] == 'tokensystem-C': + # Resolving conflict for 'tokensystem-C' + tokenname = first_classification['wordlist'][0][:-1] + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", tokenname): + return outputreturn('noise') + + isNFT = check_word_existence_instring('nft', processed_text) + + isInfinite = check_word_existence_instring('infinite-token', processed_text) + tokenamount = apply_rule1(extractAmount_rule_new, processed_text) + + ## Cannot be NFT and normal token and infinite token. Find what are the conflicts + # if its an NFT then tokenamount has to be integer and infinite keyword should not be present + # if its a normal token then isNFT and isInfinite should be None/False and token amount has to be present + # if its an infinite token then tokenamount should be None and isNFT should be None/False + # The supply of tokenAmount cannot be 0 + + ################################################## + + if (not tokenamount and not isInfinite) or (isNFT and not tokenamount.is_integer() and not isInfinite) or (isInfinite and tokenamount is not False and isNFT is not False) or tokenamount<=0: + return outputreturn('noise') + operation = apply_rule1(selectCategory, processed_text, send_category, create_category) + if operation == 'category1' and tokenamount is not None: + if isNFT: + return outputreturn('nft_transfer',f"{processed_text}", f"{tokenname}", tokenamount, stateF_mapping) + else: + return outputreturn('token_transfer',f"{processed_text}", f"{tokenname}", tokenamount, stateF_mapping) + elif operation == 'category2': + if isInfinite: + return outputreturn('infinite_token_create',f"{processed_text}", f"{tokenname}", stateF_mapping) + else: + if tokenamount is None: + return outputreturn('noise') + if isNFT: + nft_hash = extract_NFT_hash(clean_text) + if nft_hash is False: + return outputreturn('noise') + return outputreturn('nft_create',f"{processed_text}", f"{tokenname}", tokenamount, f"{nft_hash}", stateF_mapping) + else: + return outputreturn('token_incorporation',f"{processed_text}", f"{first_classification['wordlist'][0][:-1]}", tokenamount, stateF_mapping) + else: + return outputreturn('noise') + + if first_classification['categorization'] == 'smart-contract-creation-C': + # Resolving conflict for 'smart-contract-creation-C' + operation = apply_rule1(selectCategory, processed_text, create_category, send_category+deposit_category) + if not operation: + return outputreturn('noise') + + contract_type = extract_special_character_word(first_classification['wordlist'],'*') + if not check_existence_of_keyword(['one-time-event'],[contract_type]): + return outputreturn('noise') + + contract_name = extract_special_character_word(first_classification['wordlist'],'@') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_name): + return outputreturn('noise') + + contract_token = extract_special_character_word(first_classification['wordlist'],'#') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_token): + return outputreturn('noise') + + contract_address = extract_special_character_word(first_classification['wordlist'],'$') + contract_address = find_original_case(contract_address, clean_text) + if not check_flo_address(contract_address, is_testnet): + return outputreturn('noise') + + contract_conditions = extract_contract_conditions(processed_text, contract_type, contract_token, blocktime=blockinfo['time']) + if contract_conditions == False or not resolve_incategory_conflict(contract_conditions,[['userchoices','payeeAddress']]): + return outputreturn('noise') + else: + contractAmount = '' + if 'contractAmount' in contract_conditions.keys(): + contractAmount = contract_conditions['contractAmount'] + try: + if float(contractAmount)<=0: + return outputreturn('noise') + except: + return outputreturn('noise') + minimum_subscription_amount = '' + if 'minimumsubscriptionamount' in contract_conditions.keys(): + minimum_subscription_amount = contract_conditions['minimumsubscriptionamount'] + try: + if float(minimum_subscription_amount)<=0: + return outputreturn('noise') + except: + return outputreturn('noise') + maximum_subscription_amount = '' + if 'maximumsubscriptionamount' in contract_conditions.keys(): + maximum_subscription_amount = contract_conditions['maximumsubscriptionamount'] + try: + if float(maximum_subscription_amount)<=0: + return outputreturn('noise') + except: + return outputreturn('noise') + + if 'userchoices' in contract_conditions.keys(): + return outputreturn('one-time-event-userchoice-smartcontract-incorporation',f"{contract_token}", f"{contract_name}", f"{contract_address}", f"{clean_text}", f"{contractAmount}", f"{minimum_subscription_amount}" , f"{maximum_subscription_amount}", f"{contract_conditions['userchoices']}", f"{contract_conditions['expiryTime']}", stateF_mapping) + elif 'payeeAddress' in contract_conditions.keys(): + contract_conditions['payeeAddress'] = find_word_index_fromstring(clean_text,contract_conditions['payeeAddress']) + # check if colon exists in the payeeAddress string + if ':' in contract_conditions['payeeAddress']: + colon_split = contract_conditions['payeeAddress'].split(':') + if len(colon_split)%2 != 0: + return outputreturn('noise') + split_total = 0 + payeeAddress_split_dictionary = {} + for idx, item in enumerate(colon_split): + if idx%2 == 0: + # check if floid + if not check_flo_address(item, is_testnet): + return outputreturn('noise') + if idx%2 == 1: + # check if number + try: + item = float(item) + if item <= 0: + return outputreturn('noise') + payeeAddress_split_dictionary[colon_split[idx-1]] = item + split_total += item + except: + return outputreturn('noise') + if split_total != 100: + return outputreturn('noise') + else: + contract_conditions['payeeAddress'] = payeeAddress_split_dictionary + return outputreturn('one-time-event-time-smartcontract-incorporation',f"{contract_token}", f"{contract_name}", f"{contract_address}", f"{clean_text}", f"{contractAmount}", f"{minimum_subscription_amount}" , f"{maximum_subscription_amount}", contract_conditions['payeeAddress'], f"{contract_conditions['expiryTime']}", stateF_mapping) + else: + if not check_flo_address(contract_conditions['payeeAddress'], is_testnet): + return outputreturn('noise') + else: + contract_conditions['payeeAddress'] = {f"{contract_conditions['payeeAddress']}":100} + return outputreturn('one-time-event-time-smartcontract-incorporation',f"{contract_token}", f"{contract_name}", f"{contract_address}", f"{clean_text}", f"{contractAmount}", f"{minimum_subscription_amount}" , f"{maximum_subscription_amount}", contract_conditions['payeeAddress'], f"{contract_conditions['expiryTime']}", stateF_mapping) + + if first_classification['categorization'] == 'smart-contract-participation-deposit-C': + # either participation of one-time-event contract or + operation = apply_rule1(select_category_reject, processed_text, send_category, deposit_category, create_category) + if not operation: + return outputreturn('noise') + else: + tokenname = first_classification['wordlist'][0][:-1] + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", tokenname): + return outputreturn('noise') + + contract_name = extract_special_character_word(first_classification['wordlist'],'@') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_name): + return outputreturn('noise') + + contract_address = extract_special_character_word(first_classification['wordlist'],'$') + if contract_address is False: + contract_address = '' + else: + contract_address = find_original_case(contract_address, clean_text) + if not check_flo_address(contract_address, is_testnet): + return outputreturn('noise') + + if operation == 'category1': + tokenamount = apply_rule1(extractAmount_rule_new1, processed_text, 'userchoice:', 'pre') + if not tokenamount: + return outputreturn('noise') + try: + if float(tokenamount)<=0: + return outputreturn('noise') + except: + return outputreturn('noise') + userchoice = extract_userchoice(processed_text) + # todo - do we need more validations for user choice? + if not userchoice: + return outputreturn('noise') + + return outputreturn('one-time-event-userchoice-smartcontract-participation',f"{clean_text}", f"{tokenname}", tokenamount, f"{contract_name}", f"{contract_address}", f"{userchoice}", stateF_mapping) + + elif operation == 'category2': + tokenamount = apply_rule1(extractAmount_rule_new1, processed_text, 'deposit-conditions:', 'pre') + if not tokenamount: + return outputreturn('noise') + try: + if float(tokenamount)<=0: + return outputreturn('noise') + except: + return outputreturn('noise') + deposit_conditions = extract_deposit_conditions(processed_text, blocktime=blockinfo['time']) + if not deposit_conditions: + return outputreturn("noise") + return outputreturn('continuos-event-token-swap-deposit', f"{tokenname}", tokenamount, f"{contract_name}", f"{clean_text}", f"{deposit_conditions['expiryTime']}", stateF_mapping) + + if first_classification['categorization'] == 'smart-contract-participation-ote-ce-C': + # There is no way to properly differentiate between one-time-event-time-trigger participation and token swap participation + # so we merge them in output return + tokenname = first_classification['wordlist'][0][:-1] + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", tokenname): + return outputreturn('noise') + + tokenamount = apply_rule1(extractAmount_rule_new1, processed_text) + if not tokenamount: + return outputreturn('noise') + try: + if float(tokenamount)<=0: + return outputreturn('noise') + except: + return outputreturn('noise') + + contract_name = extract_special_character_word(first_classification['wordlist'],'@') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_name): + return outputreturn('noise') + + contract_address = extract_special_character_word(first_classification['wordlist'],'$') + if contract_address is False: + contract_address = '' + else: + contract_address = find_original_case(contract_address, clean_text) + if not check_flo_address(contract_address, is_testnet): + return outputreturn('noise') + + return outputreturn('smart-contract-one-time-event-continuos-event-participation', f"{clean_text}", f"{tokenname}", tokenamount, f"{contract_name}", f"{contract_address}", stateF_mapping) + + if first_classification['categorization'] == 'userchoice-trigger': + contract_name = extract_special_character_word(first_classification['wordlist'],'@') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_name): + return outputreturn('noise') + + trigger_condition = extract_trigger_condition(processed_text) + if not trigger_condition: + return outputreturn('noise') + return outputreturn('one-time-event-userchoice-smartcontract-trigger', f"{contract_name}", f"{trigger_condition}", stateF_mapping) + + if first_classification['categorization'] == 'smart-contract-creation-ce-tokenswap': + operation = apply_rule1(selectCategory, processed_text, create_category, send_category+deposit_category) + if operation != 'category1': + return outputreturn('noise') + + contract_type = extract_special_character_word(first_classification['wordlist'],'*') + if not check_existence_of_keyword(['continuous-event'],[contract_type]): + return outputreturn('noise') + + contract_name = extract_special_character_word(first_classification['wordlist'],'@') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_name): + return outputreturn('noise') + + contract_token = extract_special_character_word(first_classification['wordlist'],'#') + if not check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_token): + return outputreturn('noise') + + contract_address = extract_special_character_word(first_classification['wordlist'],'$') + contract_address = find_original_case(contract_address, clean_text) + if not check_flo_address(contract_address, is_testnet): + return outputreturn('noise') + + contract_conditions = extract_contract_conditions(processed_text, contract_type, contract_token, blocktime=blockinfo['time']) + if contract_conditions == False: + return outputreturn('noise') + # todo - Add checks for token swap extract contract conditions + try: + assert contract_conditions['subtype'] == 'tokenswap' + assert check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_conditions['accepting_token']) + assert check_regex("^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$", contract_conditions['selling_token']) + if contract_conditions['priceType']=="'determined'" or contract_conditions['priceType']=='"determined"' or contract_conditions['priceType']=="determined" or contract_conditions['priceType']=="'predetermined'" or contract_conditions['priceType']=='"predetermined"' or contract_conditions['priceType']=="predetermined" or contract_conditions['priceType']=="dynamic": + assert float(contract_conditions['price'])>0 + else: + #assert check_flo_address(find_original_case(contract_conditions['priceType'], clean_text), is_testnet) + assert contract_conditions['priceType'] == 'statef' + except AssertionError: + return outputreturn('noise') + return outputreturn('continuos-event-token-swap-incorporation', f"{contract_token}", f"{contract_name}", f"{contract_address}", f"{clean_text}", f"{contract_conditions['subtype']}", f"{contract_conditions['accepting_token']}", f"{contract_conditions['selling_token']}", f"{contract_conditions['priceType']}", f"{contract_conditions['price']}", stateF_mapping) + + return outputreturn('noise') \ No newline at end of file diff --git a/src/api/ranchimallflo_api.py b/src/api/ranchimallflo_api.py new file mode 100644 index 0000000..7e39390 --- /dev/null +++ b/src/api/ranchimallflo_api.py @@ -0,0 +1,2880 @@ +from collections import defaultdict +import sqlite3 +import json +import os +import requests +import sys +import time +from datetime import datetime +from quart import jsonify, make_response, Quart, render_template, request, flash, redirect, url_for, send_file +from quart_cors import cors +import asyncio +from typing import Optional +from config import dbfolder, apiUrl, FLO_DATA_DIR, API_VERIFY, debug_status, HOST, PORT, APP_ADMIN +import parsing +import subprocess +from apscheduler.schedulers.background import BackgroundScheduler +import atexit +import pyflo +from operator import itemgetter +import pdb +import ast +import time + +app = Quart(__name__) +app.clients = set() +app = cors(app, allow_origin="*") + +INTERNAL_ERROR = "Unable to process request, try again later" + +# Global values and configg +internalTransactionTypes = [ 'tokenswapDepositSettlement', 'tokenswapParticipationSettlement', 'smartContractDepositReturn'] + +if net == 'mainnet': + is_testnet = False +elif net == 'testnet': + is_testnet = True + + +# Validation functionss +def check_flo_address(floaddress, is_testnet=False): + return pyflo.is_address_valid(floaddress, testnet=is_testnet) + +def check_integer(value): + return str.isdigit(value) + +# Helper functions +def retryRequest(tempserverlist, apicall): + if len(tempserverlist) != 0: + try: + response = requests.get('{}api/{}'.format(tempserverlist[0], apicall)) + except: + tempserverlist.pop(0) + return retryRequest(tempserverlist, apicall) + else: + if response.status_code == 200: + return json.loads(response.content) + else: + tempserverlist.pop(0) + return retryRequest(tempserverlist, apicall) + else: + print("None of the APIs are responding for the call {}".format(apicall)) + sys.exit(0) + + +def multiRequest(apicall, net): + testserverlist = ['http://0.0.0.0:9000/', 'https://testnet.flocha.in/', 'https://testnet-flosight.duckdns.org/'] + mainserverlist = ['http://0.0.0.0:9001/', 'https://livenet.flocha.in/', 'https://testnet-flosight.duckdns.org/'] + if net == 'mainnet': + return retryRequest(mainserverlist, apicall) + elif net == 'testnet': + return retryRequest(testserverlist, apicall) + + +def blockdetailhelper(blockdetail): + if blockdetail.isdigit(): + blockHash = None + blockHeight = int(blockdetail) + else: + blockHash = str(blockdetail) + blockHeight = None + + # open the latest block database + conn = sqlite3.connect(os.path.join(dbfolder, 'latestCache.db')) + c = conn.cursor() + if blockHash: + c.execute(f"select jsonData from latestBlocks where blockHash='{blockHash}'") + elif blockHeight: + c.execute(f"select jsonData from latestBlocks where blockNumber='{blockHeight}'") + blockJson = c.fetchall() + return blockJson + +def transactiondetailhelper(transactionHash): + # open the latest block database + conn = sqlite3.connect(os.path.join(dbfolder, 'latestCache.db')) + c = conn.cursor() + c.execute(f"SELECT jsonData, parsedFloData, transactionType, db_reference FROM latestTransactions WHERE transactionHash='{transactionHash}'") + transactionJsonData = c.fetchall() + return transactionJsonData + +def update_transaction_confirmations(transactionJson): + url = f"{apiUrl}api/v1/tx/{transactionJson['txid']}" + response = requests.get(url) + if response.status_code == 200: + response_data = response.json() + transactionJson['confirmations'] = response_data['confirmations'] + return transactionJson + +def smartcontract_morph_helper(smart_contracts): + contractList = [] + for idx, contract in enumerate(smart_contracts): + contractDict = {} + contractDict['contractName'] = contract[1] + contractDict['contractAddress'] = contract[2] + contractDict['status'] = contract[3] + contractDict['contractType'] = contract[5] + if contractDict['contractType'] in ['continuous-event', 'continuos-event']: + contractDict['contractSubType'] = 'tokenswap' + accepting_selling_tokens = ast.literal_eval(contract[4]) + contractDict['acceptingToken'] = accepting_selling_tokens[0] + contractDict['sellingToken'] = accepting_selling_tokens[1] + contractStructure = fetchContractStructure(contractDict['contractName'], contractDict['contractAddress']) + if contractStructure['pricetype'] == 'dynamic': + # temp fix + if 'oracle_address' in contractStructure.keys(): + contractDict['oracle_address'] = contractStructure['oracle_address'] + contractDict['price'] = fetch_dynamic_swap_price(contractStructure, {'time': datetime.now().timestamp()}) + else: + contractDict['price'] = contractStructure['price'] + elif contractDict['contractType'] == 'one-time-event': + contractDict['tokenIdentification'] = contract[4] + # pull the contract structure + contractStructure = fetchContractStructure(contractDict['contractName'], contractDict['contractAddress']) + # compare + if 'payeeAddress' in contractStructure.keys(): + contractDict['contractSubType'] = 'time-trigger' + else: + choice_list = [] + for obj_key in contractStructure['exitconditions'].keys(): + choice_list.append(contractStructure['exitconditions'][obj_key]) + contractDict['userChoices'] = choice_list + contractDict['contractSubType'] = 'external-trigger' + contractDict['expiryDate'] = contract[9] + contractDict['closeDate'] = contract[10] + + contractDict['transactionHash'] = contract[6] + contractDict['blockNumber'] = contract[7] + contractDict['incorporationDate'] = contract[8] + contractList.append(contractDict) + return contractList + +def smartContractInfo_output(contractName, contractAddress, contractType, subtype): + if contractType == 'continuos-event' and contractType == 'tokenswap': + pass + elif contractType == 'one-time-event' and contractType == 'userchoice': + pass + elif contractType == 'one-time-event' and contractType == 'timetrigger': + pass + +def return_smart_contracts(connection, contractName=None, contractAddress=None): + # find all the contracts details + if contractName and contractAddress: + connection.execute("SELECT * FROM activecontracts WHERE id IN (SELECT max(id) FROM activecontracts GROUP BY contractName, contractAddress) AND contractName=? AND contractAddress=?", (contractName, contractAddress)) + elif contractName and not contractAddress: + connection.execute("SELECT * FROM activecontracts WHERE id IN (SELECT max(id) FROM activecontracts GROUP BY contractName, contractAddress) AND contractName=?", (contractName,)) + elif not contractName and contractAddress: + connection.execute("SELECT * FROM activecontracts WHERE id IN (SELECT max(id) FROM activecontracts GROUP BY contractName, contractAddress) AND contractAddress=?", (contractAddress,)) + else: + connection.execute("SELECT * FROM activecontracts WHERE id IN (SELECT max(id) FROM activecontracts GROUP BY contractName, contractAddress)") + + smart_contracts = connection.fetchall() + return smart_contracts + +def create_database_connection(type, parameters=None): + if type == 'token': + filelocation = os.path.join(dbfolder, 'tokens', parameters['token_name']) + elif type == 'smart_contract': + contractDbName = '{}-{}.db'.format(parameters['contract_name'].strip(), parameters['contract_address'].strip()) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + elif type == 'system_dbs': + filelocation = os.path.join(dbfolder, 'system.db') + elif type == 'latest_cache': + filelocation = os.path.join(dbfolder, 'latestCache.db') + + conn = sqlite3.connect(filelocation) + c = conn.cursor() + return [conn, c] + +def fetchContractStructure(contractName, contractAddress): + # Make connection to contract database + contractDbName = '{}-{}.db'.format(contractName.strip(),contractAddress.strip()) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + if os.path.isfile(filelocation): + # fetch from contractStructure + conn = sqlite3.connect(filelocation) + c = conn.cursor() + c.execute('SELECT attribute,value FROM contractstructure') + result = c.fetchall() + + contractStructure = {} + conditionDict = {} + counter = 0 + for item in result: + if list(item)[0] == 'exitconditions': + conditionDict[counter] = list(item)[1] + counter = counter + 1 + else: + contractStructure[list(item)[0]] = list(item)[1] + if len(conditionDict) > 0: + contractStructure['exitconditions'] = conditionDict + del counter, conditionDict, c + conn.close() + + if 'contractAmount' in contractStructure: + contractStructure['contractAmount'] = float(contractStructure['contractAmount']) + if 'payeeAddress' in contractStructure: + contractStructure['payeeAddress'] = json.loads(contractStructure['payeeAddress']) + if 'maximumsubscriptionamount' in contractStructure: + contractStructure['maximumsubscriptionamount'] = float(contractStructure['maximumsubscriptionamount']) + if 'minimumsubscriptionamount' in contractStructure: + contractStructure['minimumsubscriptionamount'] = float(contractStructure['minimumsubscriptionamount']) + if 'price' in contractStructure: + contractStructure['price'] = float(contractStructure['price']) + return contractStructure + else: + return 0 + +def fetchContractStatus(contractName, contractAddress): + conn, c = create_database_connection('system_dbs') + # select status from the last instance of activecontracts where match contractName and contractAddress + c.execute(f'SELECT status FROM activecontracts WHERE contractName="{contractName}" AND contractAddress="{contractAddress}" ORDER BY id DESC LIMIT 1') + status = c.fetchall() + if len(status)==0: + return None + else: + return status[0][0] + +def extract_ip_op_addresses(transactionJson): + sender_address = transactionJson['vin'][0]['addresses'][0] + receiver_address = None + for utxo in transactionJson['vout']: + if utxo['scriptPubKey']['addresses'][0] == sender_address: + continue + receiver_address = utxo['scriptPubKey']['addresses'][0] + return sender_address, receiver_address + +def updatePrices(): + prices = {} + # USD -> INR + response = requests.get(f"https://api.exchangerate-api.com/v4/latest/usd") + price = response.json() + prices['USDINR'] = price['rates']['INR'] + + # Blockchain stuff : BTC,FLO -> USD,INR + # BTC->USD | BTC->INR + response = requests.get(f"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,flo&vs_currencies=usd,inr") + price = response.json() + prices['BTCUSD'] = price['bitcoin']['usd'] + prices['BTCINR'] = price['bitcoin']['inr'] + + # FLO->USD | FLO->INR + response = requests.get(f"https://api.coinlore.net/api/ticker/?id=67") + price = response.json() + prices["FLOUSD"] = float(price[0]['price_usd']) + prices["FLOINR"] = float(prices["FLOUSD"]) * float(prices['USDINR']) + + # 3. update latest price data + print('Prices updated at time: %s' % datetime.now()) + print(prices) + + conn = sqlite3.connect('system.db') + c = conn.cursor() + for pair in list(prices.items()): + pair = list(pair) + c.execute(f"UPDATE ratepairs SET price={pair[1]} WHERE ratepair='{pair[0]}'") + conn.commit() + +def fetch_dynamic_swap_price(contractStructure, blockinfo): + oracle_address = contractStructure['oracle_address'] + # fetch transactions from the blockchain where from address : oracle-address... to address: contract address + # find the first contract transaction which adheres to price change format + # {"price-update":{"contract-name": "", "contract-address": "", "price": 3}} + print(f'oracle address is : {oracle_address}') + response = requests.get(f'{apiUrl}api/v1/addr/{oracle_address}') + if response.status_code == 200: + response = response.json() + if 'transactions' not in response.keys(): # API doesn't return 'transactions' key, if 0 txs present on address + return float(contractStructure['price']) + else: + transactions = response['transactions'] + for transaction_hash in transactions: + transaction_response = requests.get(f'{apiUrl}api/v1/tx/{transaction_hash}') + if transaction_response.status_code == 200: + transaction = transaction_response.json() + floData = transaction['floData'] + # If the blocktime of the transaction is < than the current block time + if transaction['time'] < blockinfo['time']: + # Check if flodata is in the format we are looking for + # ie. {"price-update":{"contract-name": "", "contract-address": "", "price": 3}} + # and receiver address should be contractAddress + try: + sender_address, receiver_address = find_sender_receiver(transaction) + assert receiver_address == contractStructure['contractAddress'] + assert sender_address == oracle_address + floData = json.loads(floData) + # Check if the contract name and address are right + assert floData['price-update']['contract-name'] == contractStructure['contractName'] + assert floData['price-update']['contract-address'] == contractStructure['contractAddress'] + return float(floData['price-update']['price']) + except: + continue + else: + continue + else: + print('API error while fetch_dynamic_swap_price') + return None + return float(contractStructure['price']) + else: + print('API error while fetch_dynamic_swap_price') + return None + +def find_sender_receiver(transaction_data): + # Create vinlist and outputlist + vinlist = [] + querylist = [] + + #totalinputval = 0 + #inputadd = '' + + # todo Rule 40 - For each vin, find the feeding address and the fed value. Make an inputlist containing [inputaddress, n value] + for vin in transaction_data["vin"]: + vinlist.append([vin["addr"], float(vin["value"])]) + + totalinputval = float(transaction_data["valueIn"]) + + # todo Rule 41 - Check if all the addresses in a transaction on the input side are the same + for idx, item in enumerate(vinlist): + if idx == 0: + temp = item[0] + continue + if item[0] != temp: + print(f"System has found more than one address as part of vin. Transaction {transaction_data['txid']} is rejected") + return 0 + + inputlist = [vinlist[0][0], totalinputval] + inputadd = vinlist[0][0] + + # todo Rule 42 - If the number of vout is more than 2, reject the transaction + if len(transaction_data["vout"]) > 2: + print(f"System has found more than 2 address as part of vout. Transaction {transaction_data['txid']} is rejected") + return 0 + + # todo Rule 43 - A transaction accepted by the system has two vouts, 1. The FLO address of the receiver + # 2. Flo address of the sender as change address. If the vout address is change address, then the other adddress + # is the recevier address + + outputlist = [] + addresscounter = 0 + inputcounter = 0 + for obj in transaction_data["vout"]: + if obj["scriptPubKey"]["type"] == "pubkeyhash": + addresscounter = addresscounter + 1 + if inputlist[0] == obj["scriptPubKey"]["addresses"][0]: + inputcounter = inputcounter + 1 + continue + outputlist.append([obj["scriptPubKey"]["addresses"][0], obj["value"]]) + + if addresscounter == inputcounter: + outputlist = [inputlist[0]] + elif len(outputlist) != 1: + print(f"Transaction's change is not coming back to the input address. Transaction {transaction_data['txid']} is rejected") + return 0 + else: + outputlist = outputlist[0] + + return inputlist[0], outputlist[0] + +def fetch_contract_status_time_info(contractName, contractAddress): + conn, c = create_database_connection('system_dbs') + c.execute('SELECT status, incorporationDate, expiryDate, closeDate FROM activecontracts WHERE contractName=="{}" AND contractAddress=="{}" ORDER BY id DESC LIMIT 1'.format(contractName, contractAddress)) + contract_status_time_info = c.fetchall() + return contract_status_time_info + +def checkIF_commitee_trigger_tranasaction(transactionDetails): + if transactionDetails[3] == 'trigger': + pass + +def transaction_post_processing(transactionJsonData): + rowarray_list = [] + + for row in transactionJsonData: + transactions_object = {} + parsedFloData = json.loads(row[1]) + transactionDetails = json.loads(row[0]) + + if row[3] in internalTransactionTypes or (row[3]=='trigger' and row[8]!='committee'): + internal_info = {} + internal_info['senderAddress'] = row[4] + internal_info['receiverAddress'] = row[5] + internal_info['tokenAmount'] = row[6] + internal_info['tokenIdentification'] = row[7] + internal_info['contractName'] = parsedFloData['contractName'] + internal_info['transactionTrigger'] = transactionDetails['txid'] + internal_info['time'] = transactionDetails['time'] + internal_info['type'] = row[3] + internal_info['onChain'] = False + transactions_object = internal_info + else: + transactions_object = {**parsedFloData, **transactionDetails} + transactions_object = update_transaction_confirmations(transactions_object) + transactions_object['onChain'] = True + + rowarray_list.append(transactions_object) + + return rowarray_list + +def fetch_token_transactions(token, senderFloAddress=None, destFloAddress=None, limit=None, use_and=False): + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + conn.row_factory = sqlite3.Row + c = conn.cursor() + else: + return jsonify(description="Token doesn't exist"), 404 + + # Build the base SQL query + query = f"SELECT jsonData, parsedFloData, time, transactionType, sourceFloAddress, destFloAddress, transferAmount, '{token}' AS token, '' AS transactionSubType FROM transactionHistory" + + # Build the WHERE clause based on conditions + conditions = [] + parameters = {} + + if senderFloAddress and not destFloAddress: + conditions.append('sourceFloAddress=:sender_flo_address') + parameters['sender_flo_address'] = senderFloAddress + + elif not senderFloAddress and destFloAddress: + conditions.append('destFloAddress=:dest_flo_address') + parameters['dest_flo_address'] = destFloAddress + + elif senderFloAddress and destFloAddress: + if use_and: + conditions.append('sourceFloAddress=:sender_flo_address AND destFloAddress=:dest_flo_address') + else: + conditions.append('sourceFloAddress=:sender_flo_address OR destFloAddress=:dest_flo_address') + parameters['sender_flo_address'] = senderFloAddress + parameters['dest_flo_address'] = destFloAddress + + # Add the WHERE clause if conditions exist + if conditions: + query += ' WHERE {}'.format(' AND '.join(conditions)) + + # Add the LIMIT clause if specified + if limit is not None: + query += ' LIMIT :limit' + parameters['limit'] = limit + + # Execute the query with parameters + c.execute(query, parameters) + transactionJsonData = c.fetchall() + conn.close() + return transaction_post_processing(transactionJsonData) + +def fetch_contract_transactions(contractName, contractAddress, _from=0, to=100): + sc_file = os.path.join(dbfolder, 'smartContracts', '{}-{}.db'.format(contractName, contractAddress)) + conn = sqlite3.connect(sc_file) + c = conn.cursor() + # Find token db names and attach + contractStructure = fetchContractStructure(contractName, contractAddress) + + if contractStructure['contractType'] == 'continuos-event': + token1 = contractStructure['accepting_token'] + token2 = contractStructure['selling_token'] + token1_file = f"{dbfolder}/tokens/{token1}.db" + token2_file = f"{dbfolder}/tokens/{token2}.db" + conn.execute(f"ATTACH DATABASE '{token1_file}' AS token1db") + conn.execute(f"ATTACH DATABASE '{token2_file}' AS token2db") + + transaction_query = f''' + SELECT t1.jsonData, t1.parsedFloData, t1.time, t1.transactionType, t1.sourceFloAddress, t1.destFloAddress, t1.transferAmount, '{token1}' AS token, s.transactionSubType + FROM main.contractTransactionHistory AS s + INNER JOIN token1db.transactionHistory AS t1 + ON t1.transactionHash = s.transactionHash + UNION + SELECT t2.jsonData, t2.parsedFloData, t2.time, t2.transactionType, t2.sourceFloAddress, t2.destFloAddress, t2.transferAmount, '{token2}' AS token, s.transactionSubType + FROM main.contractTransactionHistory AS s + INNER JOIN token2db.transactionHistory AS t2 + ON t2.transactionHash = s.transactionHash + WHERE s.id BETWEEN {_from} AND {to} + ''' + + creation_tx_query = ''' + SELECT jsonData, parsedFloData, time, transactionType, sourceFloAddress, destFloAddress, transferAmount, '' AS token, transactionSubType + FROM contractTransactionHistory + ORDER BY id + LIMIT 1; + ''' + + elif contractStructure['contractType'] == 'one-time-event': + token1 = contractStructure['tokenIdentification'] + token1_file = f"{dbfolder}/tokens/{token1}.db" + conn.execute(f"ATTACH DATABASE '{token1_file}' AS token1db") + + transaction_query = f''' + SELECT t1.jsonData, t1.parsedFloData, t1.time, t1.transactionType, t1.sourceFloAddress, t1.destFloAddress, t1.transferAmount, '{token1}' AS token, s.transactionSubType + FROM main.contractTransactionHistory AS s + INNER JOIN token1db.transactionHistory AS t1 + ON t1.transactionHash = s.transactionHash + WHERE s.id BETWEEN {_from} AND {to} + ''' + + creation_tx_query = ''' + SELECT jsonData, parsedFloData, time, transactionType, sourceFloAddress, destFloAddress, transferAmount, '' AS token, transactionSubType + FROM contractTransactionHistory + ORDER BY id + LIMIT 1; + ''' + + c.execute(transaction_query) + transactionJsonData = c.fetchall() + + c.execute(creation_tx_query) + creation_tx = c.fetchall() + transactionJsonData = creation_tx + transactionJsonData + + return transaction_post_processing(transactionJsonData) + + +def fetch_swap_contract_transactions(contractName, contractAddress, transactionHash=None): + sc_file = os.path.join(dbfolder, 'smartContracts', '{}-{}.db'.format(contractName, contractAddress)) + conn = sqlite3.connect(sc_file) + c = conn.cursor() + # Find token db names and attach + contractStructure = fetchContractStructure(contractName, contractAddress) + token1 = contractStructure['accepting_token'] + token2 = contractStructure['selling_token'] + token1_file = f"{dbfolder}/tokens/{token1}.db" + token2_file = f"{dbfolder}/tokens/{token2}.db" + conn.execute(f"ATTACH DATABASE '{token1_file}' AS token1db") + conn.execute(f"ATTACH DATABASE '{token2_file}' AS token2db") + + # Get data from db + query = f''' + SELECT t1.jsonData, t1.parsedFloData, t1.time, t1.transactionType, t1.sourceFloAddress, t1.destFloAddress, t1.transferAmount, '{token1}' AS token, t1.transactionSubType + FROM main.contractTransactionHistory AS s + INNER JOIN token1db.transactionHistory AS t1 + ON t1.transactionHash = s.transactionHash AND s.transactionHash = '{transactionHash}' + UNION + SELECT t2.jsonData, t2.parsedFloData, t2.time, t2.transactionType, t2.sourceFloAddress, t2.destFloAddress, t2.transferAmount, '{token2}' AS token, t2.transactionSubType + FROM main.contractTransactionHistory AS s + INNER JOIN token2db.transactionHistory AS t2 + ON t2.transactionHash = s.transactionHash AND s.transactionHash = '{transactionHash}' ''' + + '''if transactionHash: + query += f" WHERE s.transactionHash = '{transactionHash}'"''' + + try: + c.execute(query) + except: + pass + + transactionJsonData = c.fetchall() + return transaction_post_processing(transactionJsonData) + +def sort_transactions(transactionJsonData): + transactionJsonData = sorted(transactionJsonData, key=lambda x: x['time'], reverse=True) + return transactionJsonData + +def process_committee_flodata(flodata): + flo_address_list = [] + try: + contract_committee_actions = flodata['token-tracker']['contract-committee'] + except KeyError: + print('Flodata related to contract committee') + else: + # Adding first and removing later to maintain consistency and not to depend on floData for order of execution + for action in contract_committee_actions.keys(): + if action == 'add': + for floid in contract_committee_actions[f'{action}']: + flo_address_list.append(floid) + + for action in contract_committee_actions.keys(): + if action == 'remove': + for floid in contract_committee_actions[f'{action}']: + flo_address_list.remove(floid) + finally: + return flo_address_list + + +def refresh_committee_list(admin_flo_id, api_url, blocktime): + committee_list = [] + latest_param = 'true' + mempool_param = 'false' + init_id = None + + def process_transaction(transaction_info): + if 'isCoinBase' in transaction_info or transaction_info['vin'][0]['addresses'][0] != admin_flo_id or transaction_info['blocktime'] > blocktime: + return + try: + tx_flodata = json.loads(transaction_info['floData']) + committee_list.extend(process_committee_flodata(tx_flodata)) + except: + pass + + def send_api_request(url): + response = requests.get(url, verify=API_VERIFY) + if response.status_code == 200: + return response.json() + else: + print('Response from the Flosight API failed') + sys.exit(0) + + url = f'{api_url}api/v1/address/{admin_flo_id}?details=txs' + response = send_api_request(url) + for transaction_info in response.get('txs', []): + process_transaction(transaction_info) + + while 'incomplete' in response: + url = f'{api_url}api/v1/address/{admin_flo_id}/txs?latest={latest_param}&mempool={mempool_param}&before={init_id}' + response = send_api_request(url) + for transaction_info in response.get('items', []): + process_transaction(transaction_info) + if 'incomplete' in response: + init_id = response['initItem'] + + return committee_list + + +@app.route('/') +async def welcome_msg(): + return jsonify('Welcome to RanchiMall FLO Api v2') + + +@app.route('/api/v1.0/getSystemData', methods=['GET']) +async def systemData(): + try: + + # query for the number of flo addresses in tokenAddress mapping + conn = sqlite3.connect(os.path.join(dbfolder, 'system.db')) + c = conn.cursor() + tokenAddressCount = c.execute('select count(distinct tokenAddress) from tokenAddressMapping').fetchall()[0][0] + tokenCount = c.execute('select count(distinct token) from tokenAddressMapping').fetchall()[0][0] + contractCount = c.execute('select count(distinct contractName) from contractAddressMapping').fetchall()[0][0] + lastscannedblock = int(c.execute("select value from systemData where attribute=='lastblockscanned'").fetchall()[0][0]) + conn.close() + + # query for total number of validated blocks + conn = sqlite3.connect(os.path.join(dbfolder, 'latestCache.db')) + c = conn.cursor() + validatedBlockCount = c.execute('select count(distinct blockNumber) from latestBlocks').fetchall()[0][0] + validatedTransactionCount = c.execute('select count(distinct transactionHash) from latestTransactions').fetchall()[0][0] + conn.close() + return jsonify(systemAddressCount=tokenAddressCount, systemBlockCount=validatedBlockCount, systemTransactionCount=validatedTransactionCount, systemSmartContractCount=contractCount, systemTokenCount=tokenCount, lastscannedblock=lastscannedblock, result='ok') + + except Exception as e: + print("systemData:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v1.0/broadcastTx/') +async def broadcastTx(raw_transaction_hash): + try: + p1 = subprocess.run(['flo-cli',f"-datadir={FLO_DATA_DIR}",'sendrawtransaction',raw_transaction_hash], capture_output=True) + return jsonify(args=p1.args,returncode=p1.returncode,stdout=p1.stdout.decode(),stderr=p1.stderr.decode()) + except Exception as e: + print("broadcastTx:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +# FLO TOKEN APIs +@app.route('/api/v1.0/getTokenList', methods=['GET']) +async def getTokenList(): + try: + filelist = [] + for item in os.listdir(os.path.join(dbfolder, 'tokens')): + if os.path.isfile(os.path.join(dbfolder, 'tokens', item)): + filelist.append(item[:-3]) + return jsonify(tokens=filelist, result='ok') + except Exception as e: + print("getTokenList:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v1.0/getTokenInfo', methods=['GET']) +async def getTokenInfo(): + try: + token = request.args.get('token') + if token is None: + return jsonify(result='error', description='token name hasnt been passed') + + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return jsonify(result='error', description='token doesn\'t exist') + c.execute('SELECT * FROM transactionHistory WHERE id=1') + incorporationRow = c.fetchall()[0] + c.execute('SELECT COUNT (DISTINCT address) FROM activeTable') + numberOf_distinctAddresses = c.fetchall()[0][0] + c.execute('select max(id) from transactionHistory') + numberOf_transactions = c.fetchall()[0][0] + c.execute('select contractName, contractAddress, blockNumber, blockHash, transactionHash from tokenContractAssociation') + associatedContracts = c.fetchall() + conn.close() + + associatedContractList = [] + for item in associatedContracts: + tempdict = {} + item = list(item) + tempdict['contractName'] = item[0] + tempdict['contractAddress'] = item[1] + tempdict['blockNumber'] = item[2] + tempdict['blockHash'] = item[3] + tempdict['transactionHash'] = item[4] + associatedContractList.append(tempdict) + + return jsonify(result='ok', token=token, incorporationAddress=incorporationRow[1], tokenSupply=incorporationRow[3], time=incorporationRow[6], blockchainReference=incorporationRow[7], activeAddress_no=numberOf_distinctAddresses, totalTransactions=numberOf_transactions, associatedSmartContracts=associatedContractList) + + except Exception as e: + print("getTokenInfo:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v1.0/getTokenTransactions', methods=['GET']) +async def getTokenTransactions(): + try: + token = request.args.get('token') + senderFloAddress = request.args.get('senderFloAddress') + destFloAddress = request.args.get('destFloAddress') + limit = request.args.get('limit') + + if token is None: + return jsonify(result='error', description='token name hasnt been passed') + + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + conn.row_factory = sqlite3.Row + c = conn.cursor() + else: + return jsonify(result='error', description='token doesn\'t exist') + + if senderFloAddress and not destFloAddress: + if limit is None: + c.execute('SELECT jsonData, parsedFloData FROM transactionHistory WHERE sourceFloAddress="{}" ORDER BY id DESC'.format(senderFloAddress)) + else: + c.execute('SELECT jsonData, parsedFloData FROM transactionHistory WHERE sourceFloAddress="{}" ORDER BY id DESC LIMIT {}'.format(senderFloAddress, limit)) + elif not senderFloAddress and destFloAddress: + if limit is None: + c.execute('SELECT jsonData, parsedFloData FROM transactionHistory WHERE destFloAddress="{}" ORDER BY id DESC'.format(destFloAddress)) + else: + c.execute('SELECT jsonData, parsedFloData FROM transactionHistory WHERE destFloAddress="{}" ORDER BY id DESC LIMIT {}'.format(destFloAddress, limit)) + elif senderFloAddress and destFloAddress: + if limit is None: + c.execute('SELECT jsonData, parsedFloData FROM transactionHistory WHERE sourceFloAddress="{}" AND destFloAddress="{}" ORDER BY id DESC'.format(senderFloAddress, destFloAddress)) + else: + c.execute('SELECT jsonData, parsedFloData FROM transactionHistory WHERE sourceFloAddress="{}" AND destFloAddress="{}" ORDER BY id DESC LIMIT {}'.format(senderFloAddress, destFloAddress, limit)) + + else: + if limit is None: + c.execute('SELECT jsonData, parsedFloData FROM transactionHistory ORDER BY id DESC') + else: + c.execute('SELECT jsonData, parsedFloData FROM transactionHistory ORDER BY id DESC LIMIT {}'.format(limit)) + transactionJsonData = c.fetchall() + conn.close() + rowarray_list = {} + for row in transactionJsonData: + transactions_object = {} + transactions_object['transactionDetails'] = json.loads(row[0]) + transactions_object['transactionDetails'] = update_transaction_confirmations(transactions_object['transactionDetails']) + transactions_object['parsedFloData'] = json.loads(row[1]) + rowarray_list[transactions_object['transactionDetails']['txid']] = transactions_object + return jsonify(result='ok', token=token, transactions=rowarray_list) + + except Exception as e: + print("getTokenTransactions:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v1.0/getTokenBalances', methods=['GET']) +async def getTokenBalances(): + try: + token = request.args.get('token') + if token is None: + return jsonify(result='error', description='token name hasnt been passed') + + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return jsonify(result='error', description='token doesn\'t exist') + c.execute('SELECT address,SUM(transferBalance) FROM activeTable GROUP BY address') + addressBalances = c.fetchall() + + returnList = {} + + for address in addressBalances: + returnList[address[0]] = address[1] + + return jsonify(result='ok', token=token, balances=returnList) + except Exception as e: + print("getTokenBalances:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + + +# FLO Address APIs +@app.route('/api/v1.0/getFloAddressInfo', methods=['GET']) +async def getFloAddressInfo(): + try: + floAddress = request.args.get('floAddress') + if floAddress is None: + return jsonify(description='floAddress hasn\'t been passed'), 400 + + dblocation = dbfolder + '/system.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute('select token from tokenAddressMapping where tokenAddress="{}"'.format(floAddress)) + tokenNames = c.fetchall() + c.execute(f"select contractName, status, tokenIdentification, contractType, transactionHash, blockNumber, blockHash from activecontracts where contractAddress='{floAddress}'") + incorporatedContracts = c.fetchall() + + if len(tokenNames) != 0: + detailList = {} + for token in tokenNames: + token = token[0] + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + tempdict = {} + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute('SELECT SUM(transferBalance) FROM activeTable WHERE address="{}"'.format(floAddress)) + balance = c.fetchall()[0][0] + tempdict['balance'] = balance + tempdict['token'] = token + detailList[token] = tempdict + else: + # Address is not associated with any token + return jsonify(result='error', description='FLO address is not associated with any tokens') + + if len(incorporatedContracts) != 0: + incorporatedSmartContracts = [] + for contract in incorporatedContracts: + tempdict = {} + tempdict['contractName'] = contract[0] + tempdict['contractAddress'] = floAddress + tempdict['status'] = contract[1] + tempdict['tokenIdentification'] = contract[2] + tempdict['contractType'] = contract[3] + tempdict['transactionHash'] = contract[4] + tempdict['blockNumber'] = contract[5] + tempdict['blockHash'] = contract[6] + incorporatedSmartContracts.append(tempdict) + + return jsonify(result='ok', floAddress=floAddress, floAddressBalances=detailList, incorporatedSmartContracts=incorporatedContracts) + else: + return jsonify(result='ok', floAddress=floAddress, floAddressBalances=detailList, incorporatedSmartContracts=None) + + except Exception as e: + print("getFloAddressInfo:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + +@app.route('/api/v1.0/getFloAddressBalance', methods=['GET']) +async def getAddressBalance(): + try: + floAddress = request.args.get('floAddress') + token = request.args.get('token') + + if floAddress is None: + return jsonify(result='error', description='floAddress hasn\'t been passed') + + if token is None: + dblocation = dbfolder + '/system.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute( + 'select token from tokenAddressMapping where tokenAddress="{}"'.format(floAddress)) + tokenNames = c.fetchall() + + if len(tokenNames) != 0: + detailList = {} + + for token in tokenNames: + token = token[0] + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + tempdict = {} + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute('SELECT SUM(transferBalance) FROM activeTable WHERE address="{}"'.format(floAddress)) + balance = c.fetchall()[0][0] + tempdict['balance'] = balance + tempdict['token'] = token + detailList[token] = tempdict + + return jsonify(result='ok', floAddress=floAddress, floAddressBalances=detailList) + + else: + # Address is not associated with any token + return jsonify(result='error', description='FLO address is not associated with any tokens') + else: + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return jsonify(result='error', description='token doesn\'t exist') + c.execute( + 'SELECT SUM(transferBalance) FROM activeTable WHERE address="{}"'.format(floAddress)) + balance = c.fetchall()[0][0] + conn.close() + return jsonify(result='ok', token=token, floAddress=floAddress, balance=balance) + + except Exception as e: + print("getAddressBalance:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v1.0/getFloAddressTransactions', methods=['GET']) +async def getFloAddressTransactions(): + try: + floAddress = request.args.get('floAddress') + token = request.args.get('token') + limit = request.args.get('limit') + + if floAddress is None: + return jsonify(result='error', description='floAddress has not been passed') + + if token is None: + dblocation = dbfolder + '/system.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute('SELECT token FROM tokenAddressMapping WHERE tokenAddress="{}"'.format(floAddress)) + tokenNames = c.fetchall() + else: + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + tokenNames = [[str(token), ]] + else: + return jsonify(result='error', description='token doesn\'t exist') + + if len(tokenNames) != 0: + allTransactionList = {} + for tokenname in tokenNames: + tokenname = tokenname[0] + dblocation = dbfolder + '/tokens/' + str(tokenname) + '.db' + if os.path.exists(dblocation): + tempdict = {} + conn = sqlite3.connect(dblocation) + c = conn.cursor() + if limit is None: + c.execute('SELECT jsonData, parsedFloData FROM transactionHistory WHERE sourceFloAddress="{}" OR destFloAddress="{}" ORDER BY id DESC'.format(floAddress, floAddress)) + else: + c.execute('SELECT jsonData, parsedFloData FROM transactionHistory WHERE sourceFloAddress="{}" OR destFloAddress="{}" ORDER BY id DESC LIMIT {}'.format(floAddress, floAddress, limit)) + transactionJsonData = c.fetchall() + conn.close() + + for row in transactionJsonData: + transactions_object = {} + transactions_object['transactionDetails'] = json.loads(row[0]) + transactions_object['transactionDetails'] = update_transaction_confirmations(transactions_object['transactionDetails']) + transactions_object['parsedFloData'] = json.loads(row[1]) + allTransactionList[transactions_object['transactionDetails']['txid']] = transactions_object + + if token is None: + return jsonify(result='ok', floAddress=floAddress, transactions=allTransactionList) + else: + return jsonify(result='ok', floAddress=floAddress, transactions=allTransactionList, token=token) + else: + return jsonify(result='error', description='No token transactions present present on this address') + + except Exception as e: + print("getFloAddressTransactions:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +# SMART CONTRACT APIs +@app.route('/api/v1.0/getSmartContractList', methods=['GET']) +async def getContractList(): + try: + contractName = request.args.get('contractName') + contractAddress = request.args.get('contractAddress') + + conn = sqlite3.connect(os.path.join(dbfolder, 'system.db')) + c = conn.cursor() + + contractList = [] + + if contractName and contractAddress: + c.execute('select * from activecontracts where contractName="{}" and contractAddress="{}"'.format(contractName, contractAddress)) + allcontractsDetailList = c.fetchall() + for idx, contract in enumerate(allcontractsDetailList): + contractDict = {} + contractDict['contractName'] = contract[1] + contractDict['contractAddress'] = contract[2] + contractDict['status'] = contract[3] + contractDict['tokenIdentification'] = contract[4] + contractDict['contractType'] = contract[5] + contractDict['transactionHash'] = contract[6] + contractDict['blockNumber'] = contract[7] + contractDict['incorporationDate'] = contract[8] + if contract[9]: + contractDict['expiryDate'] = contract[9] + if contract[10]: + contractDict['closeDate'] = contract[10] + + contractList.append(contractDict) + + elif contractName and not contractAddress: + c.execute('select * from activecontracts where contractName="{}"'.format(contractName)) + allcontractsDetailList = c.fetchall() + for idx, contract in enumerate(allcontractsDetailList): + contractDict = {} + contractDict['contractName'] = contract[1] + contractDict['contractAddress'] = contract[2] + contractDict['status'] = contract[3] + contractDict['tokenIdentification'] = contract[4] + contractDict['contractType'] = contract[5] + contractDict['transactionHash'] = contract[6] + contractDict['blockNumber'] = contract[7] + contractDict['incorporationDate'] = contract[8] + if contract[9]: + contractDict['expiryDate'] = contract[9] + if contract[10]: + contractDict['closeDate'] = contract[10] + + contractList.append(contractDict) + + elif not contractName and contractAddress: + c.execute('select * from activecontracts where contractAddress="{}"'.format(contractAddress)) + allcontractsDetailList = c.fetchall() + for idx, contract in enumerate(allcontractsDetailList): + contractDict = {} + contractDict['contractName'] = contract[1] + contractDict['contractAddress'] = contract[2] + contractDict['status'] = contract[3] + contractDict['tokenIdentification'] = contract[4] + contractDict['contractType'] = contract[5] + contractDict['transactionHash'] = contract[6] + contractDict['blockNumber'] = contract[7] + contractDict['incorporationDate'] = contract[8] + if contract[9]: + contractDict['expiryDate'] = contract[9] + if contract[10]: + contractDict['closeDate'] = contract[10] + + contractList.append(contractDict) + + else: + c.execute('select * from activecontracts') + allcontractsDetailList = c.fetchall() + for idx, contract in enumerate(allcontractsDetailList): + contractDict = {} + contractDict['contractName'] = contract[1] + contractDict['contractAddress'] = contract[2] + contractDict['status'] = contract[3] + contractDict['tokenIdentification'] = contract[4] + contractDict['contractType'] = contract[5] + contractDict['transactionHash'] = contract[6] + contractDict['blockNumber'] = contract[7] + contractDict['incorporationDate'] = contract[8] + if contract[9]: + contractDict['expiryDate'] = contract[9] + if contract[10]: + contractDict['closeDate'] = contract[10] + + contractList.append(contractDict) + + return jsonify(smartContracts=contractList, result='ok') + + + except Exception as e: + print("getContractList:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + +@app.route('/api/v1.0/getSmartContractInfo', methods=['GET']) +async def getContractInfo(): + try: + contractName = request.args.get('contractName') + contractAddress = request.args.get('contractAddress') + + if contractName is None: + return jsonify(result='error', description='Smart Contract\'s name hasn\'t been passed') + + if contractAddress is None: + return jsonify(result='error', description='Smart Contract\'s address hasn\'t been passed') + + contractDbName = '{}-{}.db'.format(contractName.strip(),contractAddress.strip()) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + + if os.path.isfile(filelocation): + # Make db connection and fetch data + conn = sqlite3.connect(filelocation) + c = conn.cursor() + c.execute('SELECT attribute,value FROM contractstructure') + result = c.fetchall() + + contractStructure = {} + conditionDict = {} + counter = 0 + for item in result: + if list(item)[0] == 'exitconditions': + conditionDict[counter] = list(item)[1] + counter = counter + 1 + else: + contractStructure[list(item)[0]] = list(item)[1] + if len(conditionDict) > 0: + contractStructure['exitconditions'] = conditionDict + del counter, conditionDict + + returnval = contractStructure + returnval['userChoice'] = contractStructure['exitconditions'] + returnval.pop('exitconditions') + + c.execute('select count(participantAddress) from contractparticipants') + noOfParticipants = c.fetchall()[0][0] + returnval['numberOfParticipants'] = noOfParticipants + + c.execute('select sum(tokenAmount) from contractparticipants') + totalAmount = c.fetchall()[0][0] + returnval['tokenAmountDeposited'] = totalAmount + conn.close() + + conn = sqlite3.connect(os.path.join(dbfolder, 'system.db')) + c = conn.cursor() + c.execute('select status, incorporationDate, expiryDate, closeDate from activecontracts where contractName=="{}" and contractAddress=="{}"'.format(contractName.strip(), contractAddress.strip())) + results = c.fetchall() + + if len(results) == 1: + for result in results: + returnval['status'] = result[0] + returnval['incorporationDate'] = result[1] + if result[2]: + returnval['expiryDate'] = result[2] + if result[3]: + returnval['closeDate'] = result[3] + + if returnval['status'] == 'closed': + conn = sqlite3.connect(filelocation) + c = conn.cursor() + if returnval['contractType'] == 'one-time-event': + # pull out trigger information + # check if the trigger was succesful or failed + c.execute( + f"select transactionType, transactionSubType from contractTransactionHistory where transactionType='trigger'") + triggerntype = c.fetchall() + + if len(triggerntype) == 1: + triggerntype = list(triggerntype[0]) + + returnval['triggerType'] = triggerntype[1] + + if 'userChoice' in returnval: + # Contract is of the type external trigger + if triggerntype[0] == 'trigger' and triggerntype[1] is None: + # this is a normal trigger + + # find the winning condition + c.execute('select userchoice from contractparticipants where winningAmount is not null limit 1') + returnval['winningChoice'] = c.fetchall()[0][0] + # find the winning participants + c.execute( + 'select participantAddress, winningAmount from contractparticipants where winningAmount is not null') + winnerparticipants = c.fetchall() + + returnval['numberOfWinners'] = len(winnerparticipants) + + else: + return jsonify(result='error', description='There is more than 1 trigger in the database for the smart contract. Please check your code, this shouldnt happen') + + return jsonify(result='ok', contractName=contractName, contractAddress=contractAddress, contractInfo=returnval) + + else: + return jsonify(result='error', details='Smart Contract with the given name doesn\'t exist') + + except Exception as e: + print("getContractInfo:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v1.0/getSmartContractParticipants', methods=['GET']) +async def getcontractparticipants(): + try: + contractName = request.args.get('contractName') + contractAddress = request.args.get('contractAddress') + + if contractName is None: + return jsonify(result='error', description='Smart Contract\'s name hasn\'t been passed') + + if contractAddress is None: + return jsonify(result='error', description='Smart Contract\'s address hasn\'t been passed') + + contractName = contractName.strip() + contractAddress = contractAddress.strip() + filelocation = os.path.join(dbfolder, 'smartContracts', '{}-{}.db'.format(contractName, contractAddress)) + + if os.path.isfile(filelocation): + # Make db connection and fetch data + contractStructure = fetchContractStructure(contractName, contractAddress) + conn, c = create_database_connection('smart_contract', {'contract_name': contractName, 'contract_address': contractAddress}) + + if 'exitconditions' in contractStructure: + # contract is of the type external trigger + # check if the contract has been closed + c.execute('select * from contractTransactionHistory where transactionType="trigger"') + trigger = c.fetchall() + + if len(trigger) == 1: + c.execute('select value from contractstructure where attribute="tokenIdentification"') + token = c.fetchall() + token = token[0][0] + c.execute('SELECT id,participantAddress, tokenAmount, userChoice, transactionHash, winningAmount FROM contractparticipants') + result = c.fetchall() + conn.close() + returnval = {} + for row in result: + returnval[row[1]] = {'participantFloAddress': row[1], 'tokenAmount': row[2], 'userChoice': row[3], + 'transactionHash': row[4], 'winningAmount': row[5], 'tokenIdentification': token} + + elif len(trigger) == 0: + c.execute('SELECT id,participantAddress, tokenAmount, userChoice, transactionHash FROM contractparticipants') + result = c.fetchall() + conn.close() + returnval = {} + for row in result: + returnval[row[1]] = {'participantFloAddress': row[1], 'tokenAmount': row[2], 'userChoice': row[3], 'transactionHash': row[4]} + + else: + return jsonify(result='error', description='More than 1 trigger present. This is unusual, please check your code') + + elif 'payeeAddress' in contractStructure: + # contract is of the type internal trigger + c.execute( + 'SELECT id,participantAddress, tokenAmount, userChoice, transactionHash FROM contractparticipants') + result = c.fetchall() + conn.close() + returnval = {} + for row in result: + returnval[row[1]] = {'participantFloAddress': row[1], 'tokenAmount': row[2], 'userChoice': row[3], + 'transactionHash': row[4]} + + elif contractStructure['contractType'] == 'continuos-event' and contractStructure['subtype'] == 'tokenswap': + c.execute('SELECT * FROM contractparticipants') + contract_participants = c.fetchall() + returnval = {} + for row in contract_participants: + returnval[row[1]] = { + 'participantFloAddress': row[1], + 'participationAmount': row[2], + 'swapPrice': float(row[3]), + 'transactionHash': row[4], + 'blockNumber': row[5], + 'blockHash': row[6], + 'swapAmount': row[7] + } + conn.close() + + return jsonify(result='ok', contractName=contractName, contractAddress=contractAddress, participantInfo=returnval) + else: + return jsonify(result='error', description='Smart Contract with the given name doesn\'t exist') + + except Exception as e: + print("getcontractparticipants:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v1.0/getParticipantDetails', methods=['GET']) +async def getParticipantDetails(): + + try: + floAddress = request.args.get('floAddress') + contractName = request.args.get('contractName') + contractAddress = request.args.get('contractAddress') + + if floAddress is None: + return jsonify(result='error', description='FLO address hasn\'t been passed') + dblocation = os.path.join(dbfolder, 'system.db') + + if (contractName and contractAddress is None) or (contractName is None and contractAddress): + return jsonify(result='error', description='pass both, contractName and contractAddress as url parameters') + + #if os.path.isfile(dblocation) and os.path.isfile(contract_db): + if os.path.isfile(dblocation): + # Make db connection and fetch data + conn = sqlite3.connect(dblocation) + c = conn.cursor() + + if contractName is not None: + c.execute(f'SELECT * FROM contractAddressMapping WHERE address="{floAddress}" AND addressType="participant" AND contractName="{contractName}" AND contractAddress="{contractAddress}"') + else: + c.execute(f'SELECT * FROM contractAddressMapping WHERE address="{floAddress}" AND addressType="participant"') + participant_address_contracts = c.fetchall() + + if len(participant_address_contracts) != 0: + participationDetailsList = [] + for contract in participant_address_contracts: + detailsDict = {} + contract_db = os.path.join(dbfolder, 'smartContracts', f"{contract[3]}-{contract[4]}.db") + # Make db connection and fetch contract structure + conn = sqlite3.connect(contract_db) + c = conn.cursor() + + # Get details of the type of Smart Contract + c.execute('SELECT attribute,value FROM contractstructure') + result = c.fetchall() + + contractStructure = {} + conditionDict = {} + counter = 0 + for item in result: + if list(item)[0] == 'exitconditions': + conditionDict[counter] = list(item)[1] + counter = counter + 1 + else: + contractStructure[list(item)[0]] = list(item)[1] + if len(conditionDict) > 0: + contractStructure['exitconditions'] = conditionDict + del counter, conditionDict + + if contractStructure['contractType']=='continuos-event' and contractStructure['subtype']=='tokenswap': + # normal result + swap details + # what is a api detail + c.execute('SELECT * FROM contractparticipants WHERE participantAddress=?',(floAddress,)) + participant_details = c.fetchall() + + if len(participant_details) > 0: + participationList = [] + for participation in participant_details: + c.execute("SELECT value FROM contractstructure WHERE attribute='selling_token'") + structure = c.fetchall() + detailsDict['participationAddress'] = floAddress + detailsDict['participationAmount'] = participation[2] + detailsDict['receivedAmount'] = float(participation[3]) + detailsDict['participationToken'] = contractStructure['accepting_token'] + detailsDict['receivedToken'] = contractStructure['selling_token'] + detailsDict['swapPrice_received_to_participation'] = float(participation[7]) + detailsDict['transactionHash'] = participation[4] + detailsDict['blockNumber'] = participation[5] + detailsDict['blockHash'] = participation[6] + participationList.append(detailsDict) + + participationDetailsList.append(participationList) + + elif contractStructure['contractType']=='one-time-event' and 'payeeAddress' in contractStructure.keys(): + # normal results + conn = sqlite3.connect(dblocation) + c = conn.cursor() + detailsDict = {} + detailsDict['contractName'] = contract[3] + detailsDict['contractAddress'] = contract[4] + detailsDict['tokenAmount'] = contract[5] + detailsDict['transactionHash'] = contract[6] + + c.execute(f"select status, tokenIdentification, contractType, blockNumber, blockHash, incorporationDate, expiryDate, closeDate from activecontracts where contractName='{detailsDict['contractName']}' and contractAddress='{detailsDict['contractAddress']}'") + temp = c.fetchall() + detailsDict['status'] = temp[0][0] + detailsDict['tokenIdentification'] = temp[0][1] + detailsDict['contractType'] = temp[0][2] + detailsDict['blockNumber'] = temp[0][3] + detailsDict['blockHash'] = temp[0][4] + detailsDict['incorporationDate'] = temp[0][5] + if temp[0][6]: + detailsDict['expiryDate'] = temp[0][6] + if temp[0][7]: + detailsDict['closeDate'] = temp[0][7] + + # check if the contract has been closed + contractDbName = '{}-{}.db'.format(detailsDict['contractName'].strip(), detailsDict['contractAddress'].strip()) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + if os.path.isfile(filelocation): + # Make db connection and fetch data + conn = sqlite3.connect(filelocation) + c = conn.cursor() + c.execute('SELECT attribute,value FROM contractstructure') + result = c.fetchall() + contractStructure = {} + conditionDict = {} + counter = 0 + for item in result: + if list(item)[0] == 'exitconditions': + conditionDict[counter] = list(item)[1] + counter = counter + 1 + else: + contractStructure[list(item)[0]] = list(item)[1] + if len(conditionDict) > 0: + contractStructure['exitconditions'] = conditionDict + del counter, conditionDict + + if 'payeeAddress' in contractStructure: + # contract is of the type external trigger + # check if the contract has been closed + c.execute(f"SELECT tokenAmount FROM contractparticipants where participantAddress='{floAddress}'") + result = c.fetchall() + conn.close() + detailsDict['tokenAmount'] = result[0][0] + + elif contractStructure['contractType']=='one-time-event' and 'exitconditions' in contractStructure.keys(): + # normal results + winning/losing details + conn = sqlite3.connect(dblocation) + c = conn.cursor() + detailsDict = {} + detailsDict['contractName'] = contract[3] + detailsDict['contractAddress'] = contract[4] + detailsDict['tokenAmount'] = contract[5] + detailsDict['transactionHash'] = contract[6] + + c.execute(f"select status, tokenIdentification, contractType, blockNumber, blockHash, incorporationDate, expiryDate, closeDate from activecontracts where contractName='{detailsDict['contractName']}' and contractAddress='{detailsDict['contractAddress']}'") + temp = c.fetchall() + detailsDict['status'] = temp[0][0] + detailsDict['tokenIdentification'] = temp[0][1] + detailsDict['contractType'] = temp[0][2] + detailsDict['blockNumber'] = temp[0][3] + detailsDict['blockHash'] = temp[0][4] + detailsDict['incorporationDate'] = temp[0][5] + if temp[0][6]: + detailsDict['expiryDate'] = temp[0][6] + if temp[0][7]: + detailsDict['closeDate'] = temp[0][7] + + # check if the contract has been closed + contractDbName = '{}-{}.db'.format(detailsDict['contractName'].strip(), detailsDict['contractAddress'].strip()) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + if os.path.isfile(filelocation): + # Make db connection and fetch data + conn = sqlite3.connect(filelocation) + c = conn.cursor() + c.execute('SELECT attribute,value FROM contractstructure') + result = c.fetchall() + contractStructure = {} + conditionDict = {} + counter = 0 + for item in result: + if list(item)[0] == 'exitconditions': + conditionDict[counter] = list(item)[1] + counter = counter + 1 + else: + contractStructure[list(item)[0]] = list(item)[1] + if len(conditionDict) > 0: + contractStructure['exitconditions'] = conditionDict + del counter, conditionDict + + if 'exitconditions' in contractStructure: + # contract is of the type external trigger + # check if the contract has been closed + c.execute('select * from contractTransactionHistory where transactionType="trigger"') + trigger = c.fetchall() + + if detailsDict['status'] == 'closed': + c.execute(f"SELECT userChoice, winningAmount FROM contractparticipants where participantAddress='{floAddress}'") + result = c.fetchall() + conn.close() + detailsDict['userChoice'] = result[0][0] + detailsDict['winningAmount'] = result[0][1] + else: + c.execute(f"SELECT userChoice FROM contractparticipants where participantAddress='{floAddress}'") + result = c.fetchall() + conn.close() + detailsDict['userChoice'] = result[0][0] + + participationDetailsList.append(detailsDict) + + return jsonify(result='ok', floAddress=floAddress, type='participant', participatedContracts=participationDetailsList) + + else: + return jsonify(result='error', description='Address hasn\'t participated in any other contract') + else: + return jsonify(result='error', description='System error. System db is missing') + + except Exception as e: + print("getLatestBlockDetails:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v1.0/getSmartContractTransactions', methods=['GET']) +async def getsmartcontracttransactions(): + try: + contractName = request.args.get('contractName') + contractAddress = request.args.get('contractAddress') + + if contractName is None: + return jsonify(result='error', description='Smart Contract\'s name hasn\'t been passed') + + if contractAddress is None: + return jsonify(result='error', description='Smart Contract\'s address hasn\'t been passed') + + contractDbName = '{}-{}.db'.format(contractName.strip(), contractAddress.strip()) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + + if os.path.isfile(filelocation): + # Make db connection and fetch data + conn = sqlite3.connect(filelocation) + c = conn.cursor() + c.execute('select jsonData, parsedFloData from contractTransactionHistory') + result = c.fetchall() + conn.close() + returnval = {} + + for item in result: + transactions_object = {} + transactions_object['transactionDetails'] = json.loads(item[0]) + transactions_object['transactionDetails'] = update_transaction_confirmations(transactions_object['transactionDetails']) + transactions_object['parsedFloData'] = json.loads(item[1]) + returnval[transactions_object['transactionDetails']['txid']] = transactions_object + + return jsonify(result='ok', contractName=contractName, contractAddress=contractAddress, contractTransactions=returnval) + + else: + return jsonify(result='error', description='Smart Contract with the given name doesn\'t exist') + + except Exception as e: + print("getParticipantDetails:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v1.0/getBlockDetails/', methods=['GET']) +async def getblockdetails(blockdetail): + try: + blockJson = blockdetailhelper(blockdetail) + if len(blockJson) != 0: + blockJson = json.loads(blockJson[0][0]) + return jsonify(result='ok', blockDetails=blockJson) + else: + return jsonify(result='error', description='Block doesn\'t exist in database') + except Exception as e: + print("getblockdetails:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v1.0/getTransactionDetails/', methods=['GET']) +async def gettransactiondetails(transactionHash): + try: + transactionJsonData = transactiondetailhelper(transactionHash) + if len(transactionJsonData) != 0: + transactionJson = json.loads(transactionJsonData[0][0]) + transactionJson = update_transaction_confirmations(transactionJson) + parseResult = json.loads(transactionJsonData[0][1]) + + return jsonify(parsedFloData=parseResult, transactionDetails=transactionJson, transactionHash=transactionHash, result='ok') + else: + return jsonify(result='error', description='Transaction doesn\'t exist in database') + except Exception as e: + print("gettransactiondetails:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v1.0/getLatestTransactionDetails', methods=['GET']) +async def getLatestTransactionDetails(): + try: + + numberOfLatestBlocks = request.args.get('numberOfLatestBlocks') + + dblocation = dbfolder + '/latestCache.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return 'Latest transactions db doesn\'t exist. This is unusual, please report on https://github.com/ranchimall/ranchimallflo-api' + + if numberOfLatestBlocks is not None: + c.execute('SELECT * FROM latestTransactions WHERE blockNumber IN (SELECT DISTINCT blockNumber FROM latestTransactions ORDER BY blockNumber DESC LIMIT {}) ORDER BY id ASC;'.format(int(numberOfLatestBlocks))) + latestTransactions = c.fetchall() + c.close() + tempdict = {} + for idx, item in enumerate(latestTransactions): + item = list(item) + tx_parsed_details = {} + tx_parsed_details['transactionDetails'] = json.loads(item[3]) + tx_parsed_details['transactionDetails'] = update_transaction_confirmations(tx_parsed_details['transactionDetails']) + tx_parsed_details['parsedFloData'] = json.loads(item[5]) + tx_parsed_details['parsedFloData']['transactionType'] = item[4] + tx_parsed_details['transactionDetails']['blockheight'] = int(item[2]) + tempdict[json.loads(item[3])['txid']] = tx_parsed_details + else: + c.execute('''SELECT * FROM latestTransactions WHERE blockNumber IN (SELECT DISTINCT blockNumber FROM latestTransactions ORDER BY blockNumber DESC) ORDER BY id ASC;''') + latestTransactions = c.fetchall() + c.close() + tempdict = {} + for idx, item in enumerate(latestTransactions): + item = list(item) + tx_parsed_details = {} + tx_parsed_details['transactionDetails'] = json.loads(item[3]) + tx_parsed_details['transactionDetails'] = update_transaction_confirmations(tx_parsed_details['transactionDetails']) + tx_parsed_details['parsedFloData'] = json.loads(item[5]) + tx_parsed_details['parsedFloData']['transactionType'] = item[4] + tx_parsed_details['transactionDetails']['blockheight'] = int(item[2]) + tempdict[json.loads(item[3])['txid']] = tx_parsed_details + return jsonify(result='ok', latestTransactions=tempdict) + except Exception as e: + print("getLatestTransactionDetails:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + + +@app.route('/api/v1.0/getLatestBlockDetails', methods=['GET']) +async def getLatestBlockDetails(): + try: + limit = request.args.get('limit') + dblocation = dbfolder + '/latestCache.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return 'Latest transactions db doesn\'t exist. This is unusual, please report on https://github.com/ranchimall/ranchimallflo-api' + + if limit is None: + c.execute('''SELECT * FROM ( SELECT * FROM latestBlocks ORDER BY blockNumber DESC LIMIT 4) ORDER BY id ASC;''') + else: + int(limit) + c.execute('SELECT * FROM ( SELECT * FROM latestBlocks ORDER BY blockNumber DESC LIMIT {}) ORDER BY id ASC;'.format(limit)) + latestBlocks = c.fetchall() + c.close() + tempdict = {} + for idx, item in enumerate(latestBlocks): + tempdict[json.loads(item[3])['hash']] = json.loads(item[3]) + return jsonify(result='ok', latestBlocks=tempdict) + + except Exception as e: + print("getsmartcontracttransactions:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v1.0/getBlockTransactions/', methods=['GET']) +async def getblocktransactions(blockdetail): + try: + blockJson = blockdetailhelper(blockdetail) + if len(blockJson) != 0: + blockJson = json.loads(blockJson[0][0]) + blocktxlist = blockJson['tx'] + blocktxs = {} + for i in range(len(blocktxlist)): + temptx = transactiondetailhelper(blocktxlist[i]) + transactionJson = json.loads(temptx[0][0]) + transactionJson = update_transaction_confirmations(transactionJson) + parseResult = json.loads(temptx[0][1]) + blocktxs[blocktxlist[i]] = { + "parsedFloData" : parseResult, + "transactionDetails" : transactionJson + } + return jsonify(result='ok', transactions=blocktxs, blockKeyword=blockdetail) + else: + return jsonify(result='error', description='Block doesn\'t exist in database') + + + except Exception as e: + print("getblocktransactions:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + +@app.route('/api/v1.0/categoriseString/') +async def categoriseString(urlstring): + try: + # check if the hash is of a transaction + response = requests.get('{}api/v1/tx/{}'.format(apiUrl, urlstring)) + if response.status_code == 200: + return jsonify(type='transaction') + else: + response = requests.get('{}api/v1/block/{}'.format(apiUrl, urlstring)) + if response.status_code == 200: + return jsonify(type='block') + else: + # check urlstring is a token name + tokenfolder = os.path.join(dbfolder, 'tokens') + onlyfiles = [f[:-3] + for f in os.listdir(tokenfolder) if os.path.isfile(os.path.join(tokenfolder, f))] + + if urlstring.lower() in onlyfiles: + return jsonify(type='token') + else: + contractfolder = os.path.join(dbfolder, 'system.db') + conn = sqlite3.connect(contractfolder) + conn.row_factory = lambda cursor, row: row[0] + c = conn.cursor() + contractList = c.execute('select contractname from activeContracts').fetchall() + + if urlstring.lower() in contractList: + return jsonify(type='smartContract') + else: + return jsonify(type='noise') + + except Exception as e: + print("categoriseString:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v1.0/getTokenSmartContractList', methods=['GET']) +async def getTokenSmartContractList(): + try: + # list of tokens + filelist = [] + for item in os.listdir(os.path.join(dbfolder, 'tokens')): + if os.path.isfile(os.path.join(dbfolder, 'tokens', item)): + filelist.append(item[:-3]) + + # list of smart contracts + conn = sqlite3.connect(os.path.join(dbfolder, 'system.db')) + c = conn.cursor() + contractList = [] + c.execute('SELECT * FROM activecontracts') + allcontractsDetailList = c.fetchall() + for idx, contract in enumerate(allcontractsDetailList): + contractDict = {} + contractDict['contractName'] = contract[1] + contractDict['contractAddress'] = contract[2] + contractDict['status'] = contract[3] + contractDict['tokenIdentification'] = contract[4] + contractDict['contractType'] = contract[5] + contractDict['transactionHash'] = contract[6] + contractDict['blockNumber'] = contract[7] + contractDict['blockHash'] = contract[8] + contractDict['incorporationDate'] = contract[9] + if contract[10]: + contractDict['expiryDate'] = contract[10] + if contract[11]: + contractDict['closeDate'] = contract[11] + contractList.append(contractDict) + + return jsonify(tokens=filelist, smartContracts=contractList, result='ok') + except Exception as e: + print("getTokenSmartContractList:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + + +################### +### VERSION 2 ### +################### + +@app.route('/api/v2/info', methods=['GET']) +async def info(): + try: + # query for the number of flo addresses in tokenAddress mapping + conn = sqlite3.connect(os.path.join(dbfolder, 'system.db')) + c = conn.cursor() + tokenAddressCount = c.execute('SELECT COUNT(distinct tokenAddress) FROM tokenAddressMapping').fetchall()[0][0] + tokenCount = c.execute('SELECT COUNT(distinct token) FROM tokenAddressMapping').fetchall()[0][0] + contractCount = c.execute('SELECT COUNT(distinct contractName) FROM contractAddressMapping').fetchall()[0][0] + lastscannedblock = int(c.execute("SELECT value FROM systemData WHERE attribute=='lastblockscanned'").fetchall()[0][0]) + conn.close() + + # query for total number of validated blocks + conn = sqlite3.connect(os.path.join(dbfolder, 'latestCache.db')) + c = conn.cursor() + validatedBlockCount = c.execute('SELECT COUNT(distinct blockNumber) FROM latestBlocks').fetchall()[0][0] + validatedTransactionCount = c.execute('SELECT COUNT(distinct transactionHash) FROM latestTransactions').fetchall()[0][0] + conn.close() + + return jsonify(systemAddressCount=tokenAddressCount, systemBlockCount=validatedBlockCount, systemTransactionCount=validatedTransactionCount, systemSmartContractCount=contractCount, systemTokenCount=tokenCount, lastscannedblock=lastscannedblock), 200 + + except Exception as e: + print("info:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v2/broadcastTx/') +async def broadcastTx_v2(raw_transaction_hash): + try: + p1 = subprocess.run(['flo-cli',f"-datadir={FLO_DATA_DIR}",'sendrawtransaction',raw_transaction_hash], capture_output=True) + return jsonify(args=p1.args,returncode=p1.returncode,stdout=p1.stdout.decode(),stderr=p1.stderr.decode()), 200 + + + except Exception as e: + print("broadcastTx_v2:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + +# FLO TOKEN APIs +@app.route('/api/v2/tokenList', methods=['GET']) +async def tokenList(): + try: + filelist = [] + for item in os.listdir(os.path.join(dbfolder, 'tokens')): + if os.path.isfile(os.path.join(dbfolder, 'tokens', item)): + filelist.append(item[:-3]) + return jsonify(tokens=filelist), 200 + except Exception as e: + print("tokenList:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + + +@app.route('/api/v2/tokenInfo/', methods=['GET']) +async def tokenInfo(token): + try: + if token is None: + return jsonify(description='Token name hasnt been passed'), 400 + + # todo : input validation + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return jsonify(description="Token doesn't exist"), 404 + c.execute('SELECT * FROM transactionHistory WHERE id=1') + incorporationRow = c.fetchall()[0] + c.execute('SELECT COUNT (DISTINCT address) FROM activeTable') + numberOf_distinctAddresses = c.fetchall()[0][0] + c.execute('SELECT MAX(id) FROM transactionHistory') + numberOf_transactions = c.fetchall()[0][0] + c.execute('SELECT contractName, contractAddress, blockNumber, blockHash, transactionHash FROM tokenContractAssociation') + associatedContracts = c.fetchall() + conn.close() + + associatedContractList = [] + for item in associatedContracts: + tempdict = {} + item = list(item) + tempdict['contractName'] = item[0] + tempdict['contractAddress'] = item[1] + tempdict['blockNumber'] = item[2] + tempdict['blockHash'] = item[3] + tempdict['transactionHash'] = item[4] + associatedContractList.append(tempdict) + + return jsonify(token=token, incorporationAddress=incorporationRow[1], tokenSupply=incorporationRow[3], time=incorporationRow[6], blockchainReference=incorporationRow[7], activeAddress_no=numberOf_distinctAddresses, totalTransactions=numberOf_transactions, associatedSmartContracts=associatedContractList), 200 + + except Exception as e: + print("tokenInfo:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v2/tokenTransactions/', methods=['GET']) +async def tokenTransactions(token): + try: + if token is None: + return jsonify(description='Token name hasnt been passed'), 400 + + # Input validations + senderFloAddress = request.args.get('senderFloAddress') + if senderFloAddress is not None and not check_flo_address(senderFloAddress, is_testnet): + return jsonify(description='senderFloAddress validation failed'), 400 + destFloAddress = request.args.get('destFloAddress') + if destFloAddress is not None and not check_flo_address(destFloAddress, is_testnet): + return jsonify(description='destFloAddress validation failed'), 400 + limit = request.args.get('limit') + if limit is not None and not check_integer(limit): + return jsonify(description='limit validation failed'), 400 + use_AND = request.args.get('use_AND') + if use_AND is not None and use_AND not in [True, False]: + return jsonify(description='use_AND validation failed'), 400 + + _from = int(request.args.get('_from', 1)) # Get page number, default is 1 + to = int(request.args.get('to', 100)) # Get limit, default is 10 + + if _from<1: + return jsonify(description='_from validation failed'), 400 + if to<1: + return jsonify(description='to validation failed'), 400 + + filelocation = os.path.join(dbfolder, 'tokens', f'{token}.db') + + if os.path.isfile(filelocation): + transactionJsonData = fetch_token_transactions(token, senderFloAddress, destFloAddress, limit, use_AND) + sortedFormattedTransactions = sort_transactions(transactionJsonData) + return jsonify(token=token, transactions=sortedFormattedTransactions), 200 + else: + return jsonify(description='Token with the given name doesn\'t exist'), 404 + + except Exception as e: + print("tokenTransactions:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v2/tokenBalances/', methods=['GET']) +async def tokenBalances(token): + try: + if token is None: + return jsonify(description='Token name hasnt been passed'), 400 + + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return jsonify(description="Token doesn't exist"), 404 + c.execute('SELECT address,SUM(transferBalance) FROM activeTable GROUP BY address') + addressBalances = c.fetchall() + returnList = {} + for address in addressBalances: + returnList[address[0]] = address[1] + + return jsonify(token=token, balances=returnList), 200 + + + except Exception as e: + print("tokenBalances:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + +# FLO Address APIs +@app.route('/api/v2/floAddressInfo/', methods=['GET']) +async def floAddressInfo(floAddress): + try: + if floAddress is None: + return jsonify(description='floAddress hasn\'t been passed'), 400 + # input validation + if not check_flo_address(floAddress, is_testnet): + return jsonify(description='floAddress validation failed'), 400 + + dblocation = dbfolder + '/system.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute(f'SELECT token FROM tokenAddressMapping WHERE tokenAddress="{floAddress}"') + tokenNames = c.fetchall() + c.execute(f"SELECT contractName, status, tokenIdentification, contractType, transactionHash, blockNumber, blockHash FROM activecontracts WHERE contractAddress='{floAddress}'") + incorporatedContracts = c.fetchall() + detailList = None + if len(tokenNames) != 0: + detailList = {} + for token in tokenNames: + token = token[0] + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + tempdict = {} + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute(f'SELECT SUM(transferBalance) FROM activeTable WHERE address="{floAddress}"') + balance = c.fetchall()[0][0] + tempdict['balance'] = balance + tempdict['token'] = token + detailList[token] = tempdict + #else: + # # Address is not associated with any token + # return jsonify(description='FLO address is not associated with any tokens'), 404 + + incorporatedSmartContracts = None + if len(incorporatedContracts) > 0: + incorporatedSmartContracts = [] + for contract in incorporatedContracts: + tempdict = {} + tempdict['contractName'] = contract[0] + tempdict['contractAddress'] = floAddress + tempdict['status'] = contract[1] + tempdict['tokenIdentification'] = contract[2] + tempdict['contractType'] = contract[3] + tempdict['transactionHash'] = contract[4] + tempdict['blockNumber'] = contract[5] + tempdict['blockHash'] = contract[6] + incorporatedSmartContracts.append(tempdict) + + return jsonify(floAddress=floAddress, floAddressBalances=detailList, incorporatedSmartContracts=incorporatedSmartContracts), 200 + + except Exception as e: + print("floAddressInfo:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v2/floAddressBalance/', methods=['GET']) +async def floAddressBalance(floAddress): + try: + if floAddress is None: + return jsonify(description='floAddress hasn\'t been passed'), 400 + # input validation + if not check_flo_address(floAddress, is_testnet): + return jsonify(description='floAddress validation failed'), 400 + + token = request.args.get('token') + if token is None: + dblocation = dbfolder + '/system.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute(f'SELECT token FROM tokenAddressMapping WHERE tokenAddress="{floAddress}"') + tokenNames = c.fetchall() + + if len(tokenNames) != 0: + detailList = {} + for token in tokenNames: + token = token[0] + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + tempdict = {} + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute(f'SELECT SUM(transferBalance) FROM activeTable WHERE address="{floAddress}"') + balance = c.fetchall()[0][0] + tempdict['balance'] = balance + tempdict['token'] = token + detailList[token] = tempdict + return jsonify(floAddress=floAddress, floAddressBalances=detailList), 200 + else: + # Address is not associated with any token + return jsonify(floAddress=floAddress, floAddressBalances={}), 200 + else: + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return jsonify(description="Token doesn't exist"), 404 + c.execute(f'SELECT SUM(transferBalance) FROM activeTable WHERE address="{floAddress}"') + balance = c.fetchall()[0][0] + conn.close() + return jsonify(floAddress=floAddress, token=token, balance=balance), 200 + + except Exception as e: + print("floAddressBalance:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v2/floAddressTransactions/', methods=['GET']) +async def floAddressTransactions(floAddress): + try: + if floAddress is None: + return jsonify(description='floAddress has not been passed'), 400 + if not check_flo_address(floAddress, is_testnet): + return jsonify(description='floAddress validation failed'), 400 + limit = request.args.get('limit') + if limit is not None and not check_integer(limit): + return jsonify(description='limit validation failed'), 400 + + token = request.args.get('token') + if token is None: + dblocation = dbfolder + '/system.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + c.execute('SELECT token FROM tokenAddressMapping WHERE tokenAddress="{}"'.format(floAddress)) + tokenNames = c.fetchall() + else: + dblocation = dbfolder + '/tokens/' + str(token) + '.db' + if os.path.exists(dblocation): + tokenNames = [[str(token), ]] + else: + return jsonify(description="Token doesn't exist"), 404 + + if len(tokenNames) != 0: + allTransactionList = [] + for tokenname in tokenNames: + tokenname = tokenname[0] + transactionJsonData = fetch_token_transactions(tokenname, senderFloAddress=floAddress, destFloAddress=floAddress, limit=limit) + allTransactionList = allTransactionList + transactionJsonData + + sortedFormattedTransactions = sort_transactions(allTransactionList) + if token is None: + return jsonify(floAddress=floAddress, transactions=sortedFormattedTransactions), 200 + else: + return jsonify(floAddress=floAddress, transactions=sortedFormattedTransactions, token=token), 200 + else: + return jsonify(floAddress=floAddress, transactions=[], token=token), 200 + + except Exception as e: + print("floAddressTransactions:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +# SMART CONTRACT APIs +@app.route('/api/v2/smartContractList', methods=['GET']) +async def getContractList_v2(): + try: + contractName = request.args.get('contractName') + if contractName is not None: + contractName = contractName.strip().lower() + + # todo - Add validation for contractAddress and contractName to prevent SQL injection attacks + contractAddress = request.args.get('contractAddress') + if contractAddress is not None: + contractAddress = contractAddress.strip() + if not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + + contractList = [] + conn = sqlite3.connect(os.path.join(dbfolder, 'system.db')) + c = conn.cursor() + smart_contracts = return_smart_contracts(c, contractName, contractAddress) + smart_contracts_morphed = smartcontract_morph_helper(smart_contracts) + conn.close() + + committeeAddressList = refresh_committee_list(APP_ADMIN, apiUrl, int(time.time())) + + return jsonify(smartContracts=smart_contracts_morphed, smartContractCommittee=committeeAddressList), 200 + + + except Exception as e: + print("getContractList_v2:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + +@app.route('/api/v2/smartContractInfo', methods=['GET']) +async def getContractInfo_v2(): + try: + contractName = request.args.get('contractName') + contractAddress = request.args.get('contractAddress') + + if contractName is None: + return jsonify(description='Smart Contract\'s name hasn\'t been passed'), 400 + contractName = contractName.strip().lower() + + if contractAddress is None: + return jsonify(description='Smart Contract\'s address hasn\'t been passed'), 400 + contractAddress = contractAddress.strip() + if not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + + contractStructure = fetchContractStructure(contractName, contractAddress) + if contractStructure: + returnval = contractStructure + # Categorize into what type of contract it is right now + if contractStructure['contractType'] == 'continuos-event' and contractStructure['subtype'] == 'tokenswap': + conn, c = create_database_connection('smart_contract', {'contract_name': contractName, 'contract_address': contractAddress}) + + c.execute('SELECT COUNT(participantAddress), SUM(tokenAmount), SUM(winningAmount) FROM contractparticipants') + participation_details = c.fetchall() + + c.execute('SELECT depositAmount FROM contractdeposits') + deposit_details = c.fetchall() + + returnval['numberOfParticipants'] = participation_details[0][0] + returnval['totalParticipationAmount'] = participation_details[0][1] + returnval['totalHonorAmount'] = participation_details[0][2] + c.execute('SELECT COUNT(DISTINCT transactionHash) FROM contractdeposits') + returnval['numberOfDeposits'] = c.fetchall()[0][0] + c.execute('SELECT SUM(depositBalance) AS totalDepositBalance FROM contractdeposits c1 WHERE id = ( SELECT MAX(id) FROM contractdeposits c2 WHERE c1.transactionHash = c2.transactionHash);') + returnval['currentDepositBalance'] = c.fetchall()[0][0] + # todo - add code to token tracker to save continuos event subtype KEY as contractSubtype as part of contractStructure and remove the following line + returnval['contractSubtype'] = 'tokenswap' + returnval['priceType'] = returnval['pricetype'] + if returnval['pricetype'] not in ['predetermined']: + returnval['price'] = fetch_dynamic_swap_price(contractStructure, {'time': datetime.now().timestamp()}) + returnval['acceptingToken'] = returnval['accepting_token'] + returnval['sellingToken'] = returnval['selling_token'] + + elif contractStructure['contractType'] == 'one-time-event' and 'exitconditions' in contractStructure.keys(): + choice_list = [] + for obj_key in contractStructure['exitconditions'].keys(): + choice_list.append(contractStructure['exitconditions'][obj_key]) + returnval['userChoices'] = choice_list + returnval.pop('exitconditions') + + contract_status_time_info = fetch_contract_status_time_info(contractName, contractAddress) + if len(contract_status_time_info) == 1: + for status_time_info in contract_status_time_info: + returnval['status'] = status_time_info[0] + returnval['incorporationDate'] = status_time_info[1] + if status_time_info[2]: + returnval['expiryDate'] = status_time_info[2] + if status_time_info[3]: + returnval['closeDate'] = status_time_info[3] + # todo - add code to token tracker to save one-time-event subtype as part of contractStructure and remove the following line + returnval['contractSubtype'] = 'external-trigger' + elif contractStructure['contractType'] == 'one-time-event' and 'payeeAddress' in contractStructure.keys(): + contract_status_time_info = fetch_contract_status_time_info(contractName, contractAddress) + if len(contract_status_time_info) == 1: + for status_time_info in contract_status_time_info: + returnval['status'] = status_time_info[0] + returnval['incorporationDate'] = status_time_info[1] + if status_time_info[2]: + returnval['expiryDate'] = status_time_info[2] + if status_time_info[3]: + returnval['closeDate'] = status_time_info[3] + returnval['contractSubtype'] = 'time-trigger' + + return jsonify(contractName=contractName, contractAddress=contractAddress, contractInfo=returnval), 200 + else: + return jsonify(details="Smart Contract with the given name doesn't exist"), 404 + + except Exception as e: + print("getContractInfo_v2:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v2/smartContractParticipants', methods=['GET']) +async def getcontractparticipants_v2(): + try: + contractName = request.args.get('contractName') + if contractName is None: + return jsonify(description='Smart Contract\'s name hasn\'t been passed'), 400 + contractName = contractName.strip().lower() + + contractAddress = request.args.get('contractAddress') + if contractAddress is None: + return jsonify(description='Smart Contract\'s address hasn\'t been passed'), 400 + contractAddress = contractAddress.strip() + if not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + + filelocation = os.path.join(dbfolder, 'smartContracts', '{}-{}.db'.format(contractName, contractAddress)) + if os.path.isfile(filelocation): + # Make db connection and fetch data + contractStructure = fetchContractStructure(contractName, contractAddress) + contractStatus = fetchContractStatus(contractName, contractAddress) + conn, c = create_database_connection('smart_contract', {'contract_name': contractName, 'contract_address': contractAddress}) + if 'exitconditions' in contractStructure: + # contract is of the type external trigger + # check if the contract has been closed + if contractStatus == 'closed': + token = contractStructure['tokenIdentification'] + c.execute('SELECT id, participantAddress, tokenAmount, userChoice, transactionHash, winningAmount FROM contractparticipants') + result = c.fetchall() + returnval = [] + for row in result: + # Check value of winning amount + c.execute(f'SELECT winningAmount FROM contractwinners WHERE referenceTxHash="{row[4]}"') + participant_winningAmount = c.fetchall() + if participant_winningAmount != []: + participant_winningAmount = participant_winningAmount[0][0] + else: + participant_winningAmount = 0 + participation = {'participantFloAddress': row[1], 'tokenAmount': row[2], 'userChoice': row[3], 'transactionHash': row[4], 'winningAmount': participant_winningAmount, 'tokenIdentification': token} + returnval.append(participation) + else: + c.execute('SELECT id, participantAddress, tokenAmount, userChoice, transactionHash FROM contractparticipants') + result = c.fetchall() + conn.close() + returnval = [] + for row in result: + participation = {'participantFloAddress': row[1], 'tokenAmount': row[2], 'userChoice': row[3], 'transactionHash': row[4]} + returnval.append(participation) + return jsonify(contractName=contractName, contractAddress=contractAddress, contractType=contractStructure['contractType'], contractSubtype='external-trigger', participantInfo=returnval), 200 + elif 'payeeAddress' in contractStructure: + # contract is of the type internal trigger + c.execute('SELECT id, participantAddress, tokenAmount, userChoice, transactionHash FROM contractparticipants') + result = c.fetchall() + conn.close() + returnval = [] + for row in result: + participation = {'participantFloAddress': row[1], 'tokenAmount': row[2], 'transactionHash': row[4]} + returnval.append(participation) + return jsonify(contractName=contractName, contractAddress=contractAddress, contractType=contractStructure['contractType'], contractSubtype='time-trigger', participantInfo=returnval), 200 + elif contractStructure['contractType'] == 'continuos-event' and contractStructure['subtype'] == 'tokenswap': + c.execute('SELECT * FROM contractparticipants') + contract_participants = c.fetchall() + returnval = [] + for row in contract_participants: + participation = { + 'participantFloAddress': row[1], + 'participationAmount': row[2], + 'swapPrice': float(row[3]), + 'transactionHash': row[4], + 'blockNumber': row[5], + 'blockHash': row[6], + 'swapAmount': row[7] + } + returnval.append(participation) + conn.close() + return jsonify(contractName=contractName, contractAddress=contractAddress, contractType=contractStructure['contractType'], contractSubtype=contractStructure['subtype'], participantInfo=returnval), 200 + else: + return jsonify(description='Smart Contract with the given name doesn\'t exist'), 404 + + except Exception as e: + print("getcontractparticipants_v2:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v2/participantDetails/', methods=['GET']) +async def participantDetails(floAddress): + try: + if floAddress is None: + return jsonify(description='FLO address hasn\'t been passed'), 400 + if not check_flo_address(floAddress, is_testnet): + return jsonify(description='floAddress validation failed'), 400 + + contractName = request.args.get('contractName') + contractAddress = request.args.get('contractAddress') + + if contractAddress is not None and not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + contractAddress = contractAddress.strip() + + if (contractName and contractAddress is None) or (contractName is None and contractAddress): + return jsonify(description='pass both, contractName and contractAddress as url parameters'), 400 + contractName = contractName.strip().lower() + + systemdb_location = os.path.join(dbfolder, 'system.db') + if os.path.isfile(systemdb_location): + # Make db connection and fetch data + systemdb_conn = sqlite3.connect(systemdb_location) + c = systemdb_conn.cursor() + if contractName is not None: + c.execute(f'SELECT * FROM contractAddressMapping WHERE address="{floAddress}" AND addressType="participant" AND contractName="{contractName}" AND contractAddress="{contractAddress}"') + else: + c.execute(f'SELECT * FROM contractAddressMapping WHERE address="{floAddress}" AND addressType="participant"') + participant_address_contracts = c.fetchall() + + if len(participant_address_contracts) != 0: + participationDetailsList = [] + for contract in participant_address_contracts: + detailsDict = {} + contract_db = os.path.join(dbfolder, 'smartContracts', f"{contract[3]}-{contract[4]}.db") + # Make db connection and fetch contract structure + contractdb_conn = sqlite3.connect(contract_db) + contract_c = contractdb_conn.cursor() + # Get details of the type of Smart Contract + contract_c.execute('SELECT attribute,value FROM contractstructure') + result = contract_c.fetchall() + + contractStructure = fetchContractStructure(contract[3], contract[4]) + contractDbName = '{}-{}.db'.format(contract[3], contract[4]) + + if contractStructure['contractType']=='continuos-event' and contractStructure['subtype']=='tokenswap': + # normal result + swap details + # what is a api detail + contract_c.execute('SELECT * FROM contractparticipants WHERE participantAddress=?',(floAddress,)) + participant_details = contract_c.fetchall() + if len(participant_details) > 0: + participationList = [] + for participation in participant_details: + detailsDict['participationAddress'] = floAddress + detailsDict['participationAmount'] = participation[2] + detailsDict['receivedAmount'] = float(participation[3]) + detailsDict['participationToken'] = contractStructure['accepting_token'] + detailsDict['receivedToken'] = contractStructure['selling_token'] + detailsDict['swapPrice_received_to_participation'] = float(participation[7]) + detailsDict['transactionHash'] = participation[4] + detailsDict['blockNumber'] = participation[5] + detailsDict['blockHash'] = participation[6] + participationList.append(detailsDict) + participationDetailsList.append(participationList) + + elif contractStructure['contractType']=='one-time-event' and 'payeeAddress' in contractStructure.keys(): + # normal results + detailsDict = {} + detailsDict['contractName'] = contract[3] + detailsDict['contractAddress'] = contract[4] + detailsDict['tokenAmount'] = contract[5] + detailsDict['transactionHash'] = contract[6] + + c.execute(f"SELECT status, tokenIdentification, contractType, blockNumber, blockHash, incorporationDate, expiryDate, closeDate FROM activecontracts WHERE contractName='{detailsDict['contractName']}' AND contractAddress='{detailsDict['contractAddress']}'") + temp = c.fetchall() + detailsDict['status'] = temp[0][0] + detailsDict['tokenIdentification'] = temp[0][1] + detailsDict['contractType'] = temp[0][2] + detailsDict['blockNumber'] = temp[0][3] + detailsDict['blockHash'] = temp[0][4] + detailsDict['incorporationDate'] = temp[0][5] + if temp[0][6]: + detailsDict['expiryDate'] = temp[0][6] + if temp[0][7]: + detailsDict['closeDate'] = temp[0][7] + + # check if the contract has been closed + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + if os.path.isfile(filelocation): + if 'payeeAddress' in contractStructure: + # contract is of the type external trigger + # check if the contract has been closed + contract_c.execute(f"SELECT tokenAmount FROM contractparticipants WHERE participantAddress='{floAddress}'") + result = contract_c.fetchall() + detailsDict['tokenAmount'] = result[0][0] + + elif contractStructure['contractType']=='one-time-event' and 'exitconditions' in contractStructure.keys(): + # normal results + winning/losing details + detailsDict = {} + detailsDict['contractName'] = contract[3] + detailsDict['contractAddress'] = contract[4] + detailsDict['tokenAmount'] = contract[5] + detailsDict['transactionHash'] = contract[6] + + c.execute(f"SELECT status, tokenIdentification, contractType, blockNumber, blockHash, incorporationDate, expiryDate, closeDate FROM activecontracts WHERE contractName='{detailsDict['contractName']}' AND contractAddress='{detailsDict['contractAddress']}'") + temp = c.fetchall() + detailsDict['status'] = temp[0][0] + detailsDict['tokenIdentification'] = temp[0][1] + detailsDict['contractType'] = temp[0][2] + detailsDict['blockNumber'] = temp[0][3] + detailsDict['blockHash'] = temp[0][4] + detailsDict['incorporationDate'] = temp[0][5] + if temp[0][6]: + detailsDict['expiryDate'] = temp[0][6] + if temp[0][7]: + detailsDict['closeDate'] = temp[0][7] + + # check if the contract has been closed + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + if os.path.isfile(filelocation): + # Make db connection and fetch data + contract_c.execute('SELECT attribute,value FROM contractstructure') + result = contract_c.fetchall() + contractStructure = {} + conditionDict = {} + counter = 0 + for item in result: + if list(item)[0] == 'exitconditions': + conditionDict[counter] = list(item)[1] + counter = counter + 1 + else: + contractStructure[list(item)[0]] = list(item)[1] + if len(conditionDict) > 0: + contractStructure['exitconditions'] = conditionDict + del counter, conditionDict + + if 'exitconditions' in contractStructure: + # contract is of the type external trigger + # check if the contract has been closed + if detailsDict['status'] == 'closed': + contract_c.execute(f"SELECT userChoice, winningAmount FROM contractparticipants WHERE participantAddress='{floAddress}'") + result = contract_c.fetchall() + detailsDict['userChoice'] = result[0][0] + detailsDict['winningAmount'] = result[0][1] + else: + contract_c.execute(f"SELECT userChoice FROM contractparticipants WHERE participantAddress='{floAddress}'") + result = contract_c.fetchall() + detailsDict['userChoice'] = result[0][0] + participationDetailsList.append(detailsDict) + + return jsonify(floAddress=floAddress, type='participant', participatedContracts=participationDetailsList), 200 + else: + return jsonify(description="Address hasn't participated in any other contract"), 404 + else: + return jsonify(description='System error. System.db is missing. This is unusual, please report on https://github.com/ranchimall/ranchimallflo-api'), 500 + + except Exception as e: + print("participantDetails:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v2/smartContractTransactions', methods=['GET']) +async def smartcontracttransactions(): + try: + contractName = request.args.get('contractName') + if contractName is None: + return jsonify(description='Smart Contract\'s name hasn\'t been passed'), 400 + contractName = contractName.strip().lower() + contractAddress = request.args.get('contractAddress') + if contractAddress is None: + return jsonify(description='Smart Contract\'s address hasn\'t been passed'), 400 + contractAddress = contractAddress.strip() + if not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + + _from = int(request.args.get('_from', 1)) # Get page number, default is 1 + to = int(request.args.get('to', 100)) # Get limit, default is 10 + + if _from<1: + return jsonify(description='_from validation failed'), 400 + if to<1: + return jsonify(description='to validation failed'), 400 + + contractDbName = '{}-{}.db'.format(contractName, contractAddress) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + + if os.path.isfile(filelocation): + # Make db connection and fetch data + transactionJsonData = fetch_contract_transactions(contractName, contractAddress, _from, to) + transactionJsonData = sort_transactions(transactionJsonData) + return jsonify(contractName=contractName, contractAddress=contractAddress, contractTransactions=transactionJsonData), 200 + else: + return jsonify(description='Smart Contract with the given name doesn\'t exist'), 404 + + except Exception as e: + print("smartcontracttransactions:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +# todo - add options to only ask for active/consumed/returned deposits +@app.route('/api/v2/smartContractDeposits', methods=['GET']) +async def smartcontractdeposits(): + try: + # todo - put validation for transactionHash + contractName = request.args.get('contractName') + if contractName is None: + return jsonify(description='Smart Contract\'s name hasn\'t been passed'), 400 + contractName = contractName.strip().lower() + + contractAddress = request.args.get('contractAddress') + if contractAddress is None: + return jsonify(description='Smart Contract\'s address hasn\'t been passed'), 400 + contractAddress = contractAddress.strip() + if not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + + contractDbName = '{}-{}.db'.format(contractName, contractAddress) + filelocation = os.path.join(dbfolder, 'smartContracts', contractDbName) + if os.path.isfile(filelocation): + # active deposits + conn = sqlite3.connect(filelocation) + c = conn.cursor() + c.execute('''SELECT depositorAddress, transactionHash, status, depositBalance FROM contractdeposits + WHERE (transactionHash, id) IN (SELECT transactionHash, MAX(id) FROM contractdeposits GROUP BY transactionHash) + ORDER BY id DESC; ''') + + distinct_deposits = c.fetchall() + deposit_info = [] + for a_deposit in distinct_deposits: + #c.execute(f"SELECT depositBalance FROM contractdeposits WHERE (transactionHash, id) IN (SELECT transactionHash, MIN(id) FROM contractdeposits GROUP BY transactionHash );") + c.execute(f"SELECT depositBalance, unix_expiryTime FROM contractdeposits WHERE transactionHash=='{a_deposit[1]}' ORDER BY id LIMIT 1") + original_deposit_balance = c.fetchall() + obj = { + 'depositorAddress': a_deposit[0], + 'transactionHash': a_deposit[1], + 'status': a_deposit[2], + 'originalBalance': original_deposit_balance[0][0], + 'currentBalance': a_deposit[3], + 'time': original_deposit_balance[0][1] + } + deposit_info.append(obj) + c.execute('SELECT SUM(depositBalance) AS totalDepositBalance FROM contractdeposits c1 WHERE id = ( SELECT MAX(id) FROM contractdeposits c2 WHERE c1.transactionHash = c2.transactionHash);') + currentDepositBalance = c.fetchall()[0][0] + return jsonify(currentDepositBalance=currentDepositBalance, depositInfo=deposit_info), 200 + else: + return jsonify(description='Smart Contract with the given name doesn\'t exist'), 404 + + + except Exception as e: + print("smartcontractdeposits:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + +@app.route('/api/v2/blockDetails/', methods=['GET']) +async def blockdetails(blockHash): + try: + # todo - validate blockHash + blockJson = blockdetailhelper(blockHash) + if len(blockJson) != 0: + blockJson = json.loads(blockJson[0][0]) + return jsonify(blockDetails=blockJson), 200 + else: + return jsonify(description='Block doesn\'t exist in database'), 404 + except Exception as e: + print("blockdetails:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + + +@app.route('/api/v2/transactionDetails/', methods=['GET']) +async def transactiondetails1(transactionHash): + try: + # todo - validate transactionHash + transactionJsonData = transactiondetailhelper(transactionHash) + + if len(transactionJsonData) != 0: + transactionJson = json.loads(transactionJsonData[0][0]) + transactionJson = update_transaction_confirmations(transactionJson) + parseResult = json.loads(transactionJsonData[0][1]) + operation = transactionJsonData[0][2] + db_reference = transactionJsonData[0][3] + sender_address, receiver_address = extract_ip_op_addresses(transactionJson) + + mergeTx = {**parseResult, **transactionJson} + # TODO (CRITICAL): Write conditions to include and filter on chain and offchain transactions + mergeTx['onChain'] = True + + operationDetails = {} + if operation == 'smartContractDeposit': + # open the db reference and check if there is a deposit return + conn = sqlite3.connect(f"{dbfolder}/smartContracts/{db_reference}.db") + c = conn.cursor() + c.execute("SELECT depositAmount, blockNumber FROM contractdeposits WHERE status='deposit-return' AND transactionHash=?",(transactionJson['txid'],)) + returned_deposit_tx = c.fetchall() + if len(returned_deposit_tx) == 1: + operationDetails['returned_depositAmount'] = returned_deposit_tx[0][0] + operationDetails['returned_blockNumber'] = returned_deposit_tx[0][1] + c.execute("SELECT depositAmount, blockNumber FROM contractdeposits WHERE status='deposit-honor' AND transactionHash=?",(transactionJson['txid'],)) + deposit_honors = c.fetchall() + operationDetails['depositHonors'] = {} + operationDetails['depositHonors']['list'] = [] + operationDetails['depositHonors']['count'] = len(deposit_honors) + for deposit_honor in deposit_honors: + operationDetails['depositHonors']['list'].append({'honor_amount':deposit_honor[0],'blockNumber':deposit_honor[1]}) + + c.execute("SELECT depositBalance FROM contractdeposits WHERE id=(SELECT max(id) FROM contractdeposits WHERE transactionHash=?)",(transactionJson['txid'],)) + depositBalance = c.fetchall() + operationDetails['depositBalance'] = depositBalance[0][0] + operationDetails['consumedAmount'] = parseResult['depositAmount'] - operationDetails['depositBalance'] + + elif operation == 'tokenswap-participation': + conn = sqlite3.connect(f"{dbfolder}/smartContracts/{db_reference}.db") + c = conn.cursor() + c.execute('SELECT tokenAmount, winningAmount, userChoice FROM contractparticipants WHERE transactionHash=?',(transactionJson['txid'],)) + swap_amounts = c.fetchall() + c.execute("SELECT value FROM contractstructure WHERE attribute='selling_token'") + structure = c.fetchall() + operationDetails['participationAmount'] = swap_amounts[0][0] + operationDetails['receivedAmount'] = swap_amounts[0][1] + operationDetails['participationToken'] = parseResult['tokenIdentification'] + operationDetails['receivedToken'] = structure[0][0] + operationDetails['swapPrice_received_to_participation'] = float(swap_amounts[0][2]) + + elif operation == 'smartContractPays': + # Find what happened because of the trigger + # Find who + conn = sqlite3.connect(f"{dbfolder}/smartContracts/{db_reference}.db") + c = conn.cursor() + c.execute('SELECT participantAddress, tokenAmount, userChoice, winningAmount FROM contractparticipants WHERE winningAmount IS NOT NULL') + winner_participants = c.fetchall() + if len(winner_participants) != 0: + operationDetails['total_winners'] = len(winner_participants) + operationDetails['winning_choice'] = winner_participants[0][2] + operationDetails['winner_list'] = [] + for participant in winner_participants: + winner_details = {} + winner_details['participantAddress'] = participant[0] + winner_details['participationAmount'] = participant[1] + winner_details['winningAmount'] = participant[3] + operationDetails['winner_list'].append(winner_details) + + elif operation == 'ote-externaltrigger-participation': + # Find if this guy has won + conn = sqlite3.connect(f"{dbfolder}/smartContracts/{db_reference}.db") + c = conn.cursor() + c.execute('SELECT winningAmount FROM contractparticipants WHERE transactionHash=?',(transactionHash,)) + winningAmount = c.fetchall() + if winningAmount[0][0] is not None: + operationDetails['winningAmount'] = winningAmount[0][0] + + elif operation == 'tokenswapParticipation': + contractName, contractAddress = db_reference.rsplit('-',1) + conn = sqlite3.connect(f"{dbfolder}/smartContracts/{db_reference}.db") + c = conn.cursor() + txhash_txs = fetch_swap_contract_transactions(contractName, contractAddress, transactionHash) + mergeTx['subTransactions'] = [] + for transaction in txhash_txs: + if transaction['onChain'] == False: + mergeTx['subTransactions'].append(transaction) + + mergeTx['operation'] = operation + mergeTx['operationDetails'] = operationDetails + return jsonify(mergeTx), 200 + else: + return jsonify(description='Transaction doesn\'t exist in database'), 404 + + except Exception as e: + print("transactiondetails1:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v2/latestTransactionDetails', methods=['GET']) +async def latestTransactionDetails(): + try: + limit = request.args.get('limit') + if limit is not None and not check_integer(limit): + return jsonify(description='limit validation failed'), 400 + + dblocation = dbfolder + '/latestCache.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return jsonify(description='Latest transactions db doesn\'t exist. This is unusual, please report on https://github.com/ranchimall/ranchimallflo-api'), 500 + + if limit is not None: + c.execute('SELECT * FROM latestTransactions WHERE blockNumber IN (SELECT DISTINCT blockNumber FROM latestTransactions ORDER BY blockNumber DESC LIMIT {}) ORDER BY id DESC;'.format(int(limit))) + else: + c.execute('''SELECT * FROM latestTransactions WHERE blockNumber IN (SELECT DISTINCT blockNumber FROM latestTransactions ORDER BY blockNumber DESC) ORDER BY id DESC;''') + latestTransactions = c.fetchall() + c.close() + tx_list = [] + for idx, item in enumerate(latestTransactions): + item = list(item) + tx_parsed_details = {} + tx_parsed_details['transactionDetails'] = json.loads(item[3]) + tx_parsed_details['transactionDetails'] = update_transaction_confirmations(tx_parsed_details['transactionDetails']) + tx_parsed_details['parsedFloData'] = json.loads(item[5]) + tx_parsed_details['parsedFloData']['transactionType'] = item[4] + tx_parsed_details['transactionDetails']['blockheight'] = int(item[2]) + tx_parsed_details = {**tx_parsed_details['transactionDetails'], **tx_parsed_details['parsedFloData']} + # TODO (CRITICAL): Write conditions to include and filter on chain and offchain transactions + tx_parsed_details['onChain'] = True + tx_list.append(tx_parsed_details) + return jsonify(latestTransactions=tx_list), 200 + except Exception as e: + print("latestTransactionDetails:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + + +@app.route('/api/v2/latestBlockDetails', methods=['GET']) +async def latestBlockDetails(): + try: + limit = request.args.get('limit') + if limit is not None and not check_integer(limit): + return jsonify(description='limit validation failed'), 400 + + dblocation = dbfolder + '/latestCache.db' + if os.path.exists(dblocation): + conn = sqlite3.connect(dblocation) + c = conn.cursor() + else: + return jsonify(description='Latest transactions db doesn\'t exist. This is unusual, please report on https://github.com/ranchimall/ranchimallflo-api'), 404 + + if limit is None: + c.execute('''SELECT jsonData FROM ( SELECT * FROM latestBlocks ORDER BY blockNumber DESC LIMIT 4) ORDER BY id DESC;''') + else: + c.execute(f'SELECT jsonData FROM ( SELECT * FROM latestBlocks ORDER BY blockNumber DESC LIMIT {limit}) ORDER BY id DESC;') + latestBlocks = c.fetchall() + c.close() + + templst = [] + for idx, item in enumerate(latestBlocks): + templst.append(json.loads(item[0])) + + return jsonify(latestBlocks=templst), 200 + + except Exception as e: + print("latestBlockDetails:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v2/blockTransactions/', methods=['GET']) +async def blocktransactions(blockHash): + try: + blockJson = blockdetailhelper(blockHash) + if len(blockJson) != 0: + blockJson = json.loads(blockJson[0][0]) + blocktxlist = blockJson['txs'] + blocktxs = [] + for i in range(len(blocktxlist)): + temptx = transactiondetailhelper(blocktxlist[i]['txid']) + transactionJson = json.loads(temptx[0][0]) + parseResult = json.loads(temptx[0][1]) + blocktxs.append({**parseResult , **transactionJson}) + + # TODO (CRITICAL): Write conditions to include and filter on chain and offchain transactions + #blocktxs['onChain'] = True + return jsonify(transactions=blocktxs, blockKeyword=blockHash), 200 + else: + return jsonify(description='Block doesn\'t exist in database'), 404 + + + except Exception as e: + print("blocktransactions:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + +@app.route('/api/v2/categoriseString/') +async def categoriseString_v2(urlstring): + try: + # check if the hash is of a transaction + response = requests.get('{}api/v1/tx/{}'.format(apiUrl, urlstring)) + if response.status_code == 200: + return jsonify(type='transaction'), 200 + else: + response = requests.get('{}api/v1/block/{}'.format(apiUrl, urlstring)) + if response.status_code == 200: + return jsonify(type='block'), 200 + else: + # check urlstring is a token name + tokenfolder = os.path.join(dbfolder, 'tokens') + onlyfiles = [f[:-3] + for f in os.listdir(tokenfolder) if os.path.isfile(os.path.join(tokenfolder, f))] + if urlstring.lower() in onlyfiles: + return jsonify(type='token'), 200 + else: + systemdb = os.path.join(dbfolder, 'system.db') + conn = sqlite3.connect(systemdb) + conn.row_factory = lambda cursor, row: row[0] + c = conn.cursor() + contractList = c.execute('select contractname from activeContracts').fetchall() + + if urlstring.lower() in contractList: + return jsonify(type='smartContract'), 200 + else: + return jsonify(type='noise'), 200 + + except Exception as e: + print("categoriseString_v2:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + +@app.route('/api/v2/tokenSmartContractList', methods=['GET']) +async def tokenSmartContractList(): + try: + # list of tokens + filelist = [] + for item in os.listdir(os.path.join(dbfolder, 'tokens')): + if os.path.isfile(os.path.join(dbfolder, 'tokens', item)): + filelist.append(item[:-3]) + + # list of smart contracts + contractName = request.args.get('contractName') + if contractName is not None: + contractName = contractName.strip().lower() + + # todo - Add validation for contractAddress and contractName to prevent SQL injection attacks + contractAddress = request.args.get('contractAddress') + if contractAddress is not None: + contractAddress = contractAddress.strip() + if not check_flo_address(contractAddress, is_testnet): + return jsonify(description='contractAddress validation failed'), 400 + conn = sqlite3.connect(os.path.join(dbfolder, 'system.db')) + c = conn.cursor() + smart_contracts = return_smart_contracts(c, contractName, contractAddress) + smart_contracts_morphed = smartcontract_morph_helper(smart_contracts) + conn.close() + + committeeAddressList = refresh_committee_list(APP_ADMIN, apiUrl, int(time.time())) + return jsonify(tokens=filelist, smartContracts=smart_contracts_morphed, smartContractCommittee=committeeAddressList), 200 + + + except Exception as e: + print("tokenSmartContractList:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + +class ServerSentEvent: + def __init__( + self, + data: str, + *, + event: Optional[str] = None, + id: Optional[int] = None, + retry: Optional[int] = None, + ) -> None: + self.data = data + self.event = event + self.id = id + self.retry = retry + + def encode(self) -> bytes: + message = f"data: {self.data}" + if self.event is not None: + message = f"{message}\nevent: {self.event}" + if self.id is not None: + message = f"{message}\nid: {self.id}" + if self.retry is not None: + message = f"{message}\nretry: {self.retry}" + message = f"{message}\r\n\r\n" + return message.encode('utf-8') + +@app.route('/sse') +async def sse(): + queue = asyncio.Queue() + app.clients.add(queue) + + async def send_events(): + while True: + try: + data = await queue.get() + event = ServerSentEvent(data) + yield event.encode() + except asyncio.CancelledError as error: + app.clients.remove(queue) + + response = await make_response( + send_events(), + { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Transfer-Encoding': 'chunked', + }, + ) + response.timeout = None + return response + +@app.route('/api/v2/prices', methods=['GET']) +async def priceData(): + try: + # read system.db for price data + conn = sqlite3.connect('system.db') + c = conn.cursor() + ratepairs = c.execute('select ratepair, price from ratepairs') + ratepairs = ratepairs.fetchall() + prices = {} + for ratepair in ratepairs: + ratepair = list(ratepair) + prices[ratepair[0]] = ratepair[1] + return jsonify(prices=prices), 200 + except Exception as e: + print("priceData:", e) + return jsonify(result='error', description=INTERNAL_ERROR) + + + +####################### +####################### + +# if system.db isn't present, initialize it +if not os.path.isfile(f"system.db"): + # create an empty db + conn = sqlite3.connect('system.db') + c = conn.cursor() + c.execute('''CREATE TABLE ratepairs (id integer primary key, ratepair text, price real)''') + c.execute("INSERT INTO ratepairs(ratepair, price) VALUES ('BTCBTC', 1)") + c.execute("INSERT INTO ratepairs(ratepair, price) VALUES ('BTCUSD', -1)") + c.execute("INSERT INTO ratepairs(ratepair, price) VALUES ('BTCINR', -1)") + c.execute("INSERT INTO ratepairs(ratepair, price) VALUES ('FLOUSD', -1)") + c.execute("INSERT INTO ratepairs(ratepair, price) VALUES ('FLOINR', -1)") + c.execute("INSERT INTO ratepairs(ratepair, price) VALUES ('USDINR', -1)") + conn.commit() + conn.close() + + # update the prices once + updatePrices() + +# assign a scheduler for updating prices in the background +scheduler = BackgroundScheduler() +scheduler.add_job(func=updatePrices, trigger="interval", seconds=600) +scheduler.start() +# Shut down the scheduler when exiting the app +atexit.register(lambda: scheduler.shutdown()) + +if __name__ == "__main__": + app.run(debug=debug_status, host=HOST, port=PORT) \ No newline at end of file diff --git a/src/api/requirements.txt b/src/api/requirements.txt new file mode 100644 index 0000000..2c9ea24 --- /dev/null +++ b/src/api/requirements.txt @@ -0,0 +1,31 @@ +aiofiles +APScheduler==3.9.1 +arrow==0.15.2 +blinker==1.4 +certifi==2019.9.11 +cffi +chardet==3.0.4 +Click==7.0 +h11==0.9.0 +h2==3.1.1 +hpack==3.0.0 +Hypercorn==0.8.2 +hyperframe==5.2.0 +idna==2.8 +itsdangerous==1.1.0 +Jinja2==2.10.1 +MarkupSafe==1.1.1 +multidict==4.5.2 +priority==1.3.0 +pyflo-lib==2.0.9 +pycparser==2.19 +python-dateutil==2.8.0 +Quart==0.10.0 +Quart-CORS==0.2.0 +requests==2.22.0 +six==1.12.0 +sortedcontainers==2.1.0 +toml==0.10.0 +typing-extensions==3.7.4 +urllib3==1.25.3 +wsproto==0.15.0 diff --git a/src/api/static/broadcast.js b/src/api/static/broadcast.js new file mode 100644 index 0000000..1b237e0 --- /dev/null +++ b/src/api/static/broadcast.js @@ -0,0 +1,24 @@ +document.addEventListener('DOMContentLoaded', function() { + var es = new EventSource('/sse'); + es.onmessage = function (event) { + var messages_dom = document.getElementsByTagName('ul')[0]; + var message_dom = document.createElement('li'); + var content_dom = document.createTextNode('Received: ' + event.data); + message_dom.appendChild(content_dom); + messages_dom.appendChild(message_dom); + }; + + document.getElementById('send').onclick = function() { + fetch('/', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify ({ + message: document.getElementsByName("message")[0].value, + }), + }); + document.getElementsByName("message")[0].value = ""; + }; +}); diff --git a/src/api/templates/index.html b/src/api/templates/index.html new file mode 100644 index 0000000..34b7db8 --- /dev/null +++ b/src/api/templates/index.html @@ -0,0 +1,12 @@ + + + + SSSE example + + + + +
    + + + diff --git a/src/api/wsgi.py b/src/api/wsgi.py new file mode 100644 index 0000000..025a73f --- /dev/null +++ b/src/api/wsgi.py @@ -0,0 +1,4 @@ +from ranchimallflo_api import app + +if __name__ == "__main__": + app.run()