diff --git a/.gitignore b/.gitignore index 4b2cb76c..ca39a24b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,8 @@ develop-eggs lib lib64 venv* +PRs +test_out # Installer logs pip-log.txt diff --git a/src/Bondage/bondage.py b/src/Bondage/bondage.py deleted file mode 100644 index 00573184..00000000 --- a/src/Bondage/bondage.py +++ /dev/null @@ -1,104 +0,0 @@ -from BaseContract.base_contract import BaseContract -#from Registry.registry import Registry (in hopes this will get ported soon...) -from portedFiles.types import (BondageArgs, BondArgs, UnbondArgs, DelegateBondArgs, BondFilter, Filter, txid, NetworkProviderOptions, Constants, NumType, TransactionCallback) -from web3._utils import (utf8ToHex, toHex) - -class ZapBondage(BaseConstract): - - def __init__(self, NetworkProviderOptions): - super().__init__(self, NetworkProviderOptions or {}, { artifactName: 'Bondage'}) - - async def bond(self, bond_args: BondArgs , callback: TransactionCallback): - assert(dots and dots > 0, 'Dots to bond must be greater than 0.') - dots = toHex(bond_args['dots']) - #getEndpointBroker() is part of the Registry code (currently missing) - broker = await self.contract.getEndpointBroker(bond_args['provider'], utf8ToHex(bond_args['endpoint'])) - if broker != const.NULL_ADDRESS: - if bond_args['frm'] != broker: - raise Error('Broker address {broker} needs to call delegate bonding'.format(broker)) - promiEvent = self.contract.bond( - bond_args['provider'], - utf8ToHex(bond_args['endpoint']), - dots) - .send({ bond_args['frm'], bond_args['gas'], bond_args['gasPrice'] }) - if callback: - #Add events - pass - return promiEvent - - async def delegateBond(self, delegate_bond_args: DelegateBondArgs, callback: TransactionCallback): - assert(dots and dots > 0, 'Dots to bond must be greater than 0.') - dots = toHex(delegate_bond_args['dots']) - broker = await self.contract.getEndpointBroker(delegate_bond_args['provider'], utf8ToHex(delegate_bond_args['endpoint'])) - if broker != const.NULL_ADDRESS: - if delegate_bond_args['frm'] != broker: - raise Error('Broker address {broker} needs to call delegate bonding for this endpoint'.format(broker)) - promiEvent = self.contract.delegateBond( - delegate_bond_args['subscriber'], - delegate_bond_args['provider'], - utf8ToHex(delegate_bond_args['endpoint']), - dots) - .send({ delegate_bond_args['frm'], delegate_bond_args['gas'], delegate_bond_args['gasPrice'] }) - if callback: - #Add events - pass - return promiEvent - - async def unbond(self, unbond_args: UnbondArgs , callback: TransactionCallback): - assert(dots and dots > 0, 'Dots to unbond must be greater than 0.') - dots = toHex(unbond_args['dots']) - #getEndpointBroker() is part of the Registry code (currently missing) - broker = await self.contract.getEndpointBroker(unbond_args['provider'], utf8ToHex(unbond_args['endpoint'])) - if broker != const.NULL_ADDRESS: - if unbond_args['frm'] != broker: - raise Error('Broker address {broker} needs to call unbonding for this endpoint'.format(broker)) - promiEvent = self.contract.unbond( - unbond_args['provider'], - utf8ToHex(unbond_args['endpoint']), - dots) - .send({ unbond_args['frm'], unbond_args['gas'], unbond_args['gasPrice'] }) - if callback: - #Add events - pass - return promiEvent - - - #GETTERS - async def getBoundDots(self, bondage_args: BondageArgs): - return await self.contract.getBoundDots(bondage_args['subscriber'], bondage_args['provider'],utf8ToHex(bondage_args['endpoint'])) - - async def getDotsLimit(self, bondage_args: BondageArgs): - return await self.contract.dotLimit(bondage_args['provider'], utf8ToHex(bondage_args['endpoint'])) - - async def getDotsIssued(self, bondage_args: BondageArgs): - return await self.contract.getDotsIssued(bondage_args['provider'], utf8ToHex(bondage_args['endpoint'])) - - async def getBrokerAddress(self, bondage_args: BondageArgs): - return await self.contract.getEndpointBroker(bondage_args['provider'], utf8ToHex(bondage_args['endpoint'])) - - async def getZapBound(self, bondage_args: BondageArgs): - return await self.contract.getZapBound(bondage_args['provider'], utf8ToHex(bondage_args['endpoint'])) - - async def getNumEscrow(self, bondage_args: BondageArgs): - return await self.contract.getNumEscrow(bondage_args['subscriber'], bondage_args['provider'], bondage_args['endpoint']) - - - #EVENTS (Still not sure on these, I'm unaware of what the .events method is referring to) - def listen(filters: BondFilter = {}, callback: TransactionCallback): - self.contract.events.allEvents(filters, { 'fromBlock': 0, 'toBlock': 'latest' }, callback) - - def listenBound(filters: BondFilter = {}, callback: TransactionCallback): - self.contract.events.Bound(filters, { 'toBlock': 'latest' }, callback) - - def listenBound(filters: BondFilter = {}, callback: TransactionCallback): - self.contract.events.Unbond(filters, {'toBlock': 'latest' }, callback) - - def listenEscrowed(filters: BondFilter = {}, callback: TransactionCallback): - self.contract.events.Escrowed(filters, { 'toBlock': 'latest' }, callback) - - def listenReleased(filters: BondFilter = {}, callback: TransactionCallback): - self.contract.events.Released(filters, {'toBlock': 'latest' }, callback) - - def listenReturned(filters: BondFilter = {}, callback: TransactionCallback): - self.contract.events.Returned(filters, { 'toBlock': 'latest' }, callback) - diff --git a/src/ZapToken/Zaptoken.py b/src/ZapToken/Zaptoken.py deleted file mode 100644 index 2f24b2b9..00000000 --- a/src/ZapToken/Zaptoken.py +++ /dev/null @@ -1,46 +0,0 @@ -from base_contract import BaseContract -from BaseContract import utils -from portedFiles.types import (TransferType, address, txid, NetworkProviderOptions, TransactionCallback, NumType) -#from web3._utils. import (toHex) - -class ZapToken(BaseConstract): - - def __init__(self, NetworkProviderOptions): - super().__init__(NetworkProviderOptions or {}, { artifactName: 'ZAP_Token'}) - - async def balanceOf(self, address: address): - return await self.contract.balanceof(adddress) - - # async def send({to, amount,from, gasPrice, gas = Util.DEFAULT_GAS } : TransferType, cb = TransactionCallback): - # amount = toHex(amount) - # promiEvent = self.contract.transfer(to, amount),send({from, gas, gasPrice}) - # if cb: - # #Event - # pass - # return promiEvent - - # async def allocate({to, amount,from, gasPrice, gas = Util.DEFAULT_GAS } : TransferType, cb = TransactionCallback): - # amount = toHex(amount) - # promiEvent = self.contract.allocate(to, amount),send({from, gas, gasPrice}) - # if cb: - # #Event - # pass - # return promiEvent - - # async def approve({to, amount,from, gasPrice, gas = Util.DEFAULT_GAS } : TransferType, cb = TransactionCallback): - # amount = toHex(amount) - # _success = self.contract.approve(to,ammount).send({from, gas, gasPrice}) - # if cd: - # #Event - # pass - # success = await _success - - # if (not success): - # raise Exception('Failed to approve Bondage transfer') - - # return success - - - - - diff --git a/src/arbiter/__init__.py b/src/arbiter/__init__.py new file mode 100644 index 00000000..5bbe5d63 --- /dev/null +++ b/src/arbiter/__init__.py @@ -0,0 +1,6 @@ +class ZapArbiter: + """docstring for ZapArbiter""" + + def __init__(self, arg): + super(ZapArbiter, self).__init__() + self.arg = arg diff --git a/src/Artifacts/contracts/Arbiter.json b/src/artifacts/contracts/Arbiter.json similarity index 99% rename from src/Artifacts/contracts/Arbiter.json rename to src/artifacts/contracts/Arbiter.json index 36448f38..561d738d 100644 --- a/src/Artifacts/contracts/Arbiter.json +++ b/src/artifacts/contracts/Arbiter.json @@ -419,7 +419,7 @@ "transactionHash": "0xd891f92a9be7d2cb45ff46bbdfdf1cf20cc9957a7f3137905e105abf24e01bc1" }, "31337": { - "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + "address": "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0" } } } \ No newline at end of file diff --git a/src/Artifacts/contracts/Bondage.json b/src/artifacts/contracts/Bondage.json similarity index 99% rename from src/Artifacts/contracts/Bondage.json rename to src/artifacts/contracts/Bondage.json index 3c6efc5e..d3c22075 100644 --- a/src/Artifacts/contracts/Bondage.json +++ b/src/artifacts/contracts/Bondage.json @@ -749,7 +749,7 @@ "transactionHash": "0xbf5c11d55267fc39e8d3c7cb1b8aac179c2141ec7dd26a98d03017cd4676b79e" }, "31337": { - "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + "address": "0x8a791620dd6260079bf849dc5567adc3f2fdc318" } } } \ No newline at end of file diff --git a/src/Artifacts/contracts/Client1.json b/src/artifacts/contracts/Client1.json similarity index 100% rename from src/Artifacts/contracts/Client1.json rename to src/artifacts/contracts/Client1.json diff --git a/src/Artifacts/contracts/Client2.json b/src/artifacts/contracts/Client2.json similarity index 100% rename from src/Artifacts/contracts/Client2.json rename to src/artifacts/contracts/Client2.json diff --git a/src/Artifacts/contracts/Client3.json b/src/artifacts/contracts/Client3.json similarity index 100% rename from src/Artifacts/contracts/Client3.json rename to src/artifacts/contracts/Client3.json diff --git a/src/Artifacts/contracts/Client4.json b/src/artifacts/contracts/Client4.json similarity index 100% rename from src/Artifacts/contracts/Client4.json rename to src/artifacts/contracts/Client4.json diff --git a/src/Artifacts/contracts/ClientBytes32Array.json b/src/artifacts/contracts/ClientBytes32Array.json similarity index 100% rename from src/Artifacts/contracts/ClientBytes32Array.json rename to src/artifacts/contracts/ClientBytes32Array.json diff --git a/src/Artifacts/contracts/ClientIntArray.json b/src/artifacts/contracts/ClientIntArray.json similarity index 100% rename from src/Artifacts/contracts/ClientIntArray.json rename to src/artifacts/contracts/ClientIntArray.json diff --git a/src/Artifacts/contracts/CurrentCost.json b/src/artifacts/contracts/CurrentCost.json similarity index 100% rename from src/Artifacts/contracts/CurrentCost.json rename to src/artifacts/contracts/CurrentCost.json diff --git a/src/Artifacts/contracts/Dispatch.json b/src/artifacts/contracts/Dispatch.json similarity index 99% rename from src/Artifacts/contracts/Dispatch.json rename to src/artifacts/contracts/Dispatch.json index 30c0ea92..3c10d742 100644 --- a/src/Artifacts/contracts/Dispatch.json +++ b/src/artifacts/contracts/Dispatch.json @@ -771,7 +771,7 @@ "transactionHash": "0x9cbc488a7a4f2a217261e5f456f0118386a0a4b689e6c8ba8c601bda9341dd64" }, "31337": { - "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + "address": "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707" } } } \ No newline at end of file diff --git a/src/Artifacts/contracts/Piecewise.json b/src/artifacts/contracts/Piecewise.json similarity index 100% rename from src/Artifacts/contracts/Piecewise.json rename to src/artifacts/contracts/Piecewise.json diff --git a/src/Artifacts/contracts/Registry.json b/src/artifacts/contracts/Registry.json similarity index 100% rename from src/Artifacts/contracts/Registry.json rename to src/artifacts/contracts/Registry.json diff --git a/src/Artifacts/contracts/RegistryInterface.json b/src/artifacts/contracts/RegistryInterface.json similarity index 100% rename from src/Artifacts/contracts/RegistryInterface.json rename to src/artifacts/contracts/RegistryInterface.json diff --git a/src/Artifacts/contracts/TokenDotFactory.json b/src/artifacts/contracts/TokenDotFactory.json similarity index 100% rename from src/Artifacts/contracts/TokenDotFactory.json rename to src/artifacts/contracts/TokenDotFactory.json diff --git a/src/Artifacts/contracts/TokenFactory.json b/src/artifacts/contracts/TokenFactory.json similarity index 100% rename from src/Artifacts/contracts/TokenFactory.json rename to src/artifacts/contracts/TokenFactory.json diff --git a/src/Artifacts/contracts/ZapCoordinator.json b/src/artifacts/contracts/ZapCoordinator.json similarity index 100% rename from src/Artifacts/contracts/ZapCoordinator.json rename to src/artifacts/contracts/ZapCoordinator.json diff --git a/src/Artifacts/contracts/ZapCoordinatorInterface.json b/src/artifacts/contracts/ZapCoordinatorInterface.json similarity index 100% rename from src/Artifacts/contracts/ZapCoordinatorInterface.json rename to src/artifacts/contracts/ZapCoordinatorInterface.json diff --git a/src/Artifacts/contracts/ZapToken.json b/src/artifacts/contracts/ZapToken.json similarity index 97% rename from src/Artifacts/contracts/ZapToken.json rename to src/artifacts/contracts/ZapToken.json index c6eb2c93..8e285a3b 100644 --- a/src/Artifacts/contracts/ZapToken.json +++ b/src/artifacts/contracts/ZapToken.json @@ -422,6 +422,12 @@ "links": {}, "address": "0x11616e60d1b62633e02f044f155c9e03ff843974", "transactionHash": "0x995123b07bdd859f0edfef77c6f2842b8b32f57c586d591a6f1fff258b2c11f8" + }, + "31337": { + "events": {}, + "links": {}, + "address": "0x5fbdb2315678afecb367f032d93f642f64180aa3", + "transactionHash": "0xfa432264f28f3e8f3f151b9f834e1f6096d74b397bd5254cb883ef949aa5ad97" } } } \ No newline at end of file diff --git a/src/Artifacts/src/index.py b/src/artifacts/src/__init__.py similarity index 100% rename from src/Artifacts/src/index.py rename to src/artifacts/src/__init__.py diff --git a/src/Artifacts/src/tests/__init__.py b/src/artifacts/src/tests/__init__.py similarity index 100% rename from src/Artifacts/src/tests/__init__.py rename to src/artifacts/src/tests/__init__.py diff --git a/src/Artifacts/src/tests/test_artifacts.py b/src/artifacts/src/tests/test_artifacts.py similarity index 100% rename from src/Artifacts/src/tests/test_artifacts.py rename to src/artifacts/src/tests/test_artifacts.py diff --git a/src/BaseContract/base_contract.py b/src/base_contract/__init__.py similarity index 98% rename from src/BaseContract/base_contract.py rename to src/base_contract/__init__.py index fde613e6..7c866876 100644 --- a/src/BaseContract/base_contract.py +++ b/src/base_contract/__init__.py @@ -1,4 +1,4 @@ -import utils +# import utils from web3 import Web3 class BaseContract: diff --git a/src/BaseContract/test/__init__.py b/src/base_contract/test/__init__.py similarity index 100% rename from src/BaseContract/test/__init__.py rename to src/base_contract/test/__init__.py diff --git a/src/BaseContract/test/test_base_contract.py b/src/base_contract/test/test_base_contract.py similarity index 100% rename from src/BaseContract/test/test_base_contract.py rename to src/base_contract/test/test_base_contract.py diff --git a/src/BaseContract/utils.py b/src/base_contract/utils.py similarity index 100% rename from src/BaseContract/utils.py rename to src/base_contract/utils.py diff --git a/src/bondage/__init__.py b/src/bondage/__init__.py new file mode 100644 index 00000000..b5ea6f6f --- /dev/null +++ b/src/bondage/__init__.py @@ -0,0 +1,6 @@ +class ZapBondage: + """docstring for ZapBondage""" + + def __init__(self, arg): + super(ZapBondage, self).__init__() + self.arg = arg diff --git a/src/dispatch/__init__.py b/src/dispatch/__init__.py new file mode 100644 index 00000000..36ba3836 --- /dev/null +++ b/src/dispatch/__init__.py @@ -0,0 +1,6 @@ +class ZapDispatch: + """docstring for ZapDispatch""" + + def __init__(self, arg): + super(ZapDispatch, self).__init__() + self.arg = arg diff --git a/src/registry/__init__.py b/src/registry/__init__.py new file mode 100644 index 00000000..ad88956e --- /dev/null +++ b/src/registry/__init__.py @@ -0,0 +1,359 @@ +from web3 import Web3 +from asyncio import sleep +from typing import Optional, List + +from base_contract import BaseContract +from zap_token.curve import Curve +from zaptypes import ( + Filter, address, + NetworkProviderOptions, const, TransactionCallback, txid +) + + +class ZapRegistry(BaseContract): + """This contract manages Providers and Curve registration + + NetworkProviderOptions -- Dictionary object containing options for + BaseContract init + + NetworkProviderOptions has the following keyword arguments: + + arifactsDir -- Directory where contract ABIs are located + networkId -- Select which network the contract is located + options : (mainnet, testnet, private) + networkProvider -- Ethereum network provider (e.g. Infura or web3) + + Example: + ZapRegistry({"networkId": 42, "networkProvider": "web3"}) + """ + + def __init__(self, options: NetworkProviderOptions = {}): + options["artifact_name"] = "REGISTRY" + BaseContract.__init__(self, **options) + + """ + Registry storage calls for all providers + """ + + # async def task(func, action, args1=[], args2=[]): + # item = func(*args1).action(*args2) + # sleep(5) + + # if item: + # return item + # return + + async def get_all_providers(self) -> list: + """Get all providers in Registry Contract. + + returns a list of oracles once async is fulfilled + """ + await sleep(3) + return self.contract.functions.getAllOracles().call() + + async def get_provider_address_by_index(self, index: int) -> str: + """ Look up provider's address by its index in registry storage + + returns address of indexed provider once async is fulfilled + """ + await sleep(0.6) + return self.contract.functions.getOracleAddress(index).call() + + """ + Provider specific calls + """ + + async def initiate_provider(self, public_key: str, title: str, + From: address, cb: TransactionCallback = None, + gas=const.DEFAULT_GAS) -> txid: + """ + Initiates a brand endpoint in the Registry contract, + creating an Oracle entry if need be. + + Arguments: + public_key -- a public identifier for this oracle + title -- describes what data this oracle provides + from -- Ethereum address of the account that is + initializing this provider + gas -- Sets the fas limit for this transaction. + Defaults to 4 * 10**5 + + """ + + try: + await sleep(3) + tx_hash: txid = self.contract.functions.initiateProvider( + public_key, Web3.toBytes(text=title)).transact( + {"from": From, "gas": gas}) + if cb: + cb(None, tx_hash) + + return tx_hash.hex() + except ValueError as e: + print(e) + + async def get_provider_publickey(self, provider: address) -> int: + """ Get a provider's public key from the registry contract. + + provider -- The address of this provider + + returns the public key number. + """ + await sleep(0.8) + return self.contract.functions.getProviderPublicKey(provider).call() + + async def get_provider_title(self, provider: address) -> str: + """ Get a provider's title from the Registry contract. + + address -- The address of this provider. + + return a future that will eventually resolve into a title string + """ + await sleep(1) + title = Web3.toText(self.contract.functions.getProviderTitle( + provider).call()) + + return title + + async def set_provider_title(self, From: address, title: str, + cb: TransactionCallback = None, + gas=const.DEFAULT_GAS) -> txid: + """ Set the new provider's title + + Arguments: + From -- The address of this provider + title -- The new title of this provider + cb -- Callback for transactionHash event + + """ + try: + await sleep(1.4) + tx_hash: txid = self.contract.functions.setProviderTitle( + Web3.toBytes(text=title)).transact( + {"from": From, "gas": gas}) + cb(None, tx_hash) + + return tx_hash.hex() + except Exception as e: + return(str(e)) + + async def is_provider_initiated(self, provider: address) -> bool: + """ Gets whether this provider has already been created. + + provider -- Gets whether this provider has already been created. + + returns a future that will eventually resolve a true/false value. + """ + await sleep(0.13) + return self.contract.functions.isProviderInitiated(provider).call() + + async def set_provider_param(self, key: int, value: int, + From: address, gas=const.DEFAULT_GAS) -> txid: + await sleep(1.2) + return self.contract.functions.setProviderParameter( + Web3.toBytes(key), Web3.toBytes(value)).transact( + {"from": From, "gas": gas}).hex() + + async def get_provider_param(self, provider: address, key: int) -> bytes: + """ Get a parameter from a provider + + provider -- The address of the provider + key -- The key you're getting + + returns a future that will be resolved with the value of the keys + """ + await sleep(0.82) + return self.contract.functions.getProviderParameter(provider, Web3.toBytes(key)).call() + + async def get_all_provider_params(self, provider: address) -> List[bytes]: + """ Get all the parameters of a provider + + provider -- The address of the provider + + returns a future that will be resolved with all the keys + """ + await sleep(0.89) + return await self.contract.functions.getAllProviderParams(provider).call() + + async def get_provider_endpoints(self, provider: address) -> List[str]: + """ Get the endpoints of a given provider + + provider -- The address of this provider + + returns a Future that will be eventually resolved with + the list of endpoints of the provider. + """ + await sleep(0.58) + endpoints = self.contract.functions.getProviderEndpoints( + provider).call() + endpoints = [Web3.toHex(endpoint) for endpoint in endpoints] + valid_endpoints = [e for e in endpoints if e != ''] + + return valid_endpoints + + """ + Provider's specific endpoint calls + """ + + async def initiate_provider_curve(self, end_point, term, + From, gasPrice, + cb: TransactionCallback = None, + broker=const.NULL_ADDRESS, + gas=const.DEFAULT_GAS) -> txid: + """""" + await sleep(0.247) + + try: + tx_hash: txid = self.contract.functions.initiateProviderCurve( + Web3.toBytes(text=end_point), + term, broker).transact( + {"from": From, "gas": gas, "gasPrice": int(gasPrice)}) + if cb: + cb(None, tx_hash) + return tx_hash.hex() + except ValueError as e: + print(str(e)) + + async def clear_endpoint(self, endpoint, From, gasPrice, + cb: TransactionCallback = None, + gas=const.DEFAULT_GAS) -> txid: + try: + await sleep(1.6) + tx_hash: txid = self.contract.functions.clearEndpoint( + Web3.toBytes(text=endpoint)).transact( + {"from": From, "gas": gas, "gasPrice": gasPrice}) + if cb: + cb(None, tx_hash) + return tx_hash.hex() + except Exception as e: + print(e) + + async def get_provider_curve(self, provider: address, endpoint: str): + await sleep(0.8) + terms: list = self.contract.functions.getProviderCurve( + provider, Web3.toBytes(text=endpoint)).call() + + return Curve([int(t) for t in terms]) + + def encode_params(self, endpoint_params: list = [], ): + pars = endpoint_params + hex_params =\ + [el if el.find('0x') == 0 else Web3.toHex(text=el) for el in pars] + bytes_params =\ + [bytearray(Web3.toBytes(hexstr=hex_p)) for hex_p in hex_params] + params = [] + + from math import ceil + for element in bytes_params: + if len(element) <= 32: + params.append(Web3.toHex(element)) + continue + chunks_len = ceil((len(element) + 2) / 32) + param_bytes_w_len = [0, chunks_len].extend(element) + for i in range(0, chunks_len): + start = i * 32 + end = start + 32 + params.append(Web3.toHex(param_bytes_w_len[start:end])) + return params + + def decode_params(self, raw_params: List[str] = []): + bytes_params = [bytearray(Web3.toBytes(hexstr=el)) + + for el in raw_params] + params = [] + i = 0 + length = len(bytes_params) + + while i < length: + is_start_o_chunks =\ + bytes_params[i][0] == 0 and\ + bytes_params[i][1] > 1 and\ + len(bytes_params[i]) == 32 + + if not is_start_o_chunks: + params.append(Web3.toHex(bytes_params[i])) + i += 1 + continue + + chunks_len = bytes_params[i][1] + end = i + chunks_len + + raw_bytes = bytes_params[i][2:] + i += 1 + + while i < end: + raw_bytes = raw_bytes.extend(bytes_params[i]) + i += 1 + + params.append(Web3.toHex(raw_bytes)) + + try: + return [Web3.toText(hexstr=raw_hex) for raw_hex in params] + except Exception as e: + print(e) + + async def set_endpoint_params(self, endpoint: str, From: address, + gasPrice: int, + cb: Optional[TransactionCallback] = None, + endpoint_params: Optional[List[str]] = [], + gas: Optional[int] = const.DEFAULT_GAS + ) -> txid: + """ Initialize endpoint params for an endpoint. + Can only be called by the owner of this oracle. + + :param str endpoint: Data endpoint of the provider + :param List[str] endpoint_params: The parameters that this endpoint + accepts as query arguments + :param address from: The address of the owner of this oracle + :param int gas: Sets the gas limit for this + transaction (optional) + :param cb: Callback for transactionHash event + + :returns Future(txid) Returns a Promise that will eventually + resolve into a transaction hash + """ + params = self.encode_params(endpoint_params) + + try: + await sleep(0.53) + tx_hash = self.contract.functions.setEndpointParams( + Web3.toBytes(text=endpoint), params).transact( + {"from": From, "gas": gas}) + if cb: + cb(None, tx_hash) + return tx_hash.hex() + except Exception as e: + print(e) + + async def get_endpoint_broker(self, provider: address, + endpoint: str) -> str: + await sleep(0.27) + return self.contract.functions.getEndpointBroker( + provider, + Web3.toBytes(text=endpoint)).call() + + async def is_endpoint_set(self, provider: address, endpoint: str): + await sleep(0.2) + unset: bool = self.contract.functions.getCurveUnset( + provider, + Web3.toBytes(text=endpoint)).call() + + return not unset + + """ Events + """ + from typing import Callable + + async def listen(self, callback: Callable[..., None]): + sleep(3) + self.contract.events.allEvents(callback) + + async def listen_new_provider(self, callback: TransactionCallback, + filters: Filter = {}): + sleep(2) + self.contract.events.NewProvider(filters, callback) + + async def listen_new_curve(self, callback: TransactionCallback, + filters: Filter): + sleep(2) + self.contract.events.NewCurve(filters, callback) diff --git a/src/subscriber/__init__.py b/src/subscriber/__init__.py new file mode 100644 index 00000000..8903ea2c --- /dev/null +++ b/src/subscriber/__init__.py @@ -0,0 +1,356 @@ +from asyncio import sleep + +from registry import ZapRegistry +from dispatch import ZapDispatch +from bondage import ZapBondage +from arbiter import ZapArbiter +from zap_token import ZapToken + +from typing import Any, List, Optional + +from zaptypes import ( + NetworkProviderOptions, const, + TransactionCallback, txid, + address +) + + +class ZapSubscriber: + """ Represents an offchain Subscriber. + + Also provides an interface to the appropriate smart contracts. + """ + + def __init__(self, owner: str, + options: NetworkProviderOptions): + assert owner, "owner address is required" + self.subscriber_owner = owner + self.zap_token = ZapToken(**options) + self.zap_dispatch = ZapDispatch(**options) + self.zap_bondage = ZapBondage(**options) + self.zap_arbiter = ZapArbiter(**options) + self.zap_registry = ZapRegistry(**options) + + async def get_zap_balance(self) -> int: + """ Gets the Zap balance of the current ZapSubscriber. + + :return a Coroutine that will resolve into the Zap Balance in wei. + """ + balance: int = await\ + self.zap_token.balance_of(self.subscriber_owner) + return balance + + async def get_zap_allowance(self) -> int: + """ Gets the Zap allowance of the current ZapSubscriber to Bondage. + + :return a Coroutine that will resolve into the Zap Allowance in wei + """ + owner = self.zap_bondage.contract.address + + await sleep(1.5) + allowance: int = self.zap_token.contract.functions.allowance( + self.subscriber_owner, owner).call() + + return allowance + + async def approve_to_bond(self, zap_num: int, + From: address, gas_price: int, + gas: Optional[int] = const.DEFAULT_GAS) -> txid: + """ Approve number of zap to a provider + + :param provider: Provider's address + + :param zap_num: Number of Zap to approve + + :param gas: Number of gas limit + """ + return await self.zap_token.approve( + self.zap_bondage.contract.address, + zap_num, + From, gas_price, gas) + + async def bond(self, provider: address, endpoint: str, dots: int, + From: address, gas_price: int, + gas: Optional[int] = const.DEFAULT_GAS, + cb: Optional[TransactionCallback] = None) -> txid: + """ Bonds `zap_num` amount of Zap to the given provider's endpoint, + yielding dots that enable this subscriber to send queries. + + :param provider: Provider's address + + :param endpoint: Endpoint that this client wants to query from + + :param dots: Amount of dots to bond + + :param cb: Callback for transactionHash event + + :returns + a Coroutine that will eventually resolve + into a transaction hash + """ + await sleep(1.5) + approved: str =\ + self.zap_token.contract.functions.allowance( + self.subscriber_owner, + self.zap_bondage.contract.address).call() + + required: int = await\ + self.zap_bondage.calc_zap_for_dots( + provider, endpoint, dots) + + zap_balance = await self.get_zap_balance() + + assert (approved >= required),\ + "You don\'t have enough ZAP approved." + assert (zap_balance >= required),\ + "Balance insufficient." + + bonded = await self.zap_bondage.bond( + provider, endpoint, + dots, + From, gas_price, gas, + cb=cb) + + return bonded + + async def delegate_bond(self, provider: address, subscriber: address, + endpoint: str, dots: int, + From: address, gas_price: int, + gas: Optional[int] = const.DEFAULT_GAS, + cb: Optional[TransactionCallback] = None) -> txid: + """ Delegate bond zapNum amount of Zap to provider's endpoint. + + Thereby yielding dots that enable the given subscriber + to send queries. + + :param provider: Provider's address + + :param subscriber: + subscriber's address that will bond with provider's endpoint + + :param endpoint: Endpoint that this client wants to query from + + :param dots: Amount of dots to bond + + :param cb: Callback for transactionHash event + + :return Coroutine that will resolve into a txid + """ + await sleep(1.5) + approved: int =\ + self.zap_token.contract.functions.allowance( + self.subscriber_owner, + self.zap_bondage.contract.address).call() + + required: int = await\ + self.zap_bondage.calc_zap_for_dots(provider, endpoint, dots) + zap_balance = await self.get_zap_balance() + + assert approved >= required,\ + "You don\'t have enough ZAP approved." + assert (zap_balance >= required),\ + "Balance insufficient." + + bonded = await self.zap_bondage.delegate_bond( + subscriber, endpoint, + dots, provider, From, + gas_price, gas, + cb=cb) + + return bonded + + async def unbond(self, provider: address, endpoint: address, dots: int, + From: address, gas_price: int, + gas: Optional[int] = const.DEFAULT_GAS, + cb: Optional[TransactionCallback] = None) -> txid: + """ Unbonds a given number of dots from a given oracle. + + Returns Zap to this subscriber based on the bonding curve. + + :param provider: Oracle's address + + :param endpoint: Endpoint that the client has already bonded to + + :param dots: + Number of dots to unbond (redeem) from + this provider and endpoint + + :param cb: Callback for transactionHash event + + :returns a Coroutine that will resolve into a transaction hash + """ + bound_dots = await\ + self.zap_bondage.get_bound_dots( + self.subscriber_owner, + provider, + endpoint) + + assert (bound_dots >= dots),\ + "dots to unbond is less than requested" + + return await self.zap_bondage.unbond( + provider, endpoint, dots, + From, gas_price, gas, cb) + + async def subscribe(self, provider: address, endpoint: address, + endpoint_params: List[str], dots: int, + From: address, gas_price: int, + gas: Optional[int] = const.DEFAULT_GAS, + cb: Optional[TransactionCallback] = None) -> str: + """ Initializes a temporal subscription to an oracle. + + Defined in terms of # of blocks. + + :param provider: Oracle's address + + :param endpoint: Endpoint that the client will query from + + :param endpointParams: The parameters passed to the oracle + + :param dots: + Number of dots to subscribe for, determining the number of + blocks this temporal subscription will last for + + :returns a Coroutine that will resolve into a transaction hash + """ + provider_pubkey = await\ + self.zap_registry.get_provider_publickey(provider) + zap_required = await\ + self.zap_bondage.calc_zap_for_dots(provider, endpoint, dots) + zap_balance = await\ + self.get_zap_balance() + + if zap_balance < zap_required: + raise Exception( + f"Insufficient balance; Requires {zap_required} Zap for {dots} dots") + + bound_dots = await\ + self.zap_bondage.get_bound_dots( + self.subscriber_owner, + provider, + endpoint) + + if bound_dots < dots: + raise Exception( + f"Insufficient bound dots; Please bond {dots} dots to subscribe") + blocks = dots + sub = await\ + self.zap_arbiter.initiate_subscription( + provider, endpoint, endpoint_params, + provider_pubkey, blocks, + From, gas_price, gas, cb) + + return sub + + async def query_data(self, provider: address, query: str, + endpoint: str, endpoint_params: List[str], + From: address, gas_price: int, + gas: Optional[int] = const.DEFAULT_GAS, + cb: Optional[TransactionCallback] = None) -> txid: + """ Queries data from a subscriber to a given provider's endpoint. + + Parses a query string and endpoint parameters which will be + processed by the oracle. + + :param provider: Oracle's address + + :param query: Query string given to be handled by provider + + :param endpoint: + Data endpoint of provider, determines how query is handled + + :param endpointParams: + Parameters passed to data provider's endpoint + + :returns Coroutine Transaction hash + """ + bound_dots = await\ + self.zap_bondage.get_bound_dots( + self.subscriber_owner, + provider, endpoint) + + if not bound_dots: + raise Exception("Insufficient balance of bound dots to query") + + return\ + await self.zap_dispatch.query_data( + provider, query, endpoint, + endpoint_params, + From, gas_price, gas, cb) + + async def cancel_query(self, query_id, + From: address, gas_price: int, + gas: Optional[int] = const.DEFAULT_GAS, + cb: Optional[TransactionCallback] = None) -> txid: + """ Cancel a query_id + + :param queryId: string representation of the query id + """ + try: + await self.zap_dispatch.cancel_query( + query_id, + From, gas_price, gas, cb) + except Exception as e: + raise e + + async def get_num_escrow(self, provider: address, endpoint: str) -> int: + """ Get Number of dots in escrow + + :param provider + + :param endpoint + + :returns Number of escrow dots + """ + return\ + await self.zap_bondage.get_num_escrow( + provider, + self.subscriber_owner, + endpoint) + + async def get_bound_dots(self, provider: address, endpoint: str) -> int: + """ Gets the number of dots that are bounded to a provider's endpoint + for the current subscriber. + + :param provider: Address of the data provider + + :param endpoint: Data endpoint of the provider + + :returns + a Coroutine that resolves into the number of bound dots + to this provider's endpoint + """ + return\ + await self.zap_bondage.get_bound_dots( + self.subscriber_owner, + provider, endpoint) + + async def listen_to_offchain_response( + self, + cb: Optional[TransactionCallback] = None, + Filter: Optional[dict] = {}) -> Any: + """ Listen to all Offchain responses events + + :param filter + + :param callback + """ + if not Filter["subscriber"]: + Filter.update({"subscriber": self.subscriber_owner}) + + self.zap_dispatch.listen_offchain_response(Filter, cb) + + """ Helpers + """ + async def hasEnoughZap(self, zap_required: int) -> bool: + """ Compares Zap Balance of Subscriber to a given amount. + + :param zap_required: Number of zap to check for + + :returns + a Coroutine that will resolve into a boolean + true if there is enough Zap + """ + balance: int = await\ + self.zap_token.balance_of(self.subscriber_owner) + return balance >= zap_required diff --git a/src/zap_token/__init__.py b/src/zap_token/__init__.py new file mode 100644 index 00000000..9e4698c7 --- /dev/null +++ b/src/zap_token/__init__.py @@ -0,0 +1,9 @@ +from .curve import Curve + + +class ZapToken: + """docstring for ZapToken""" + + def __init__(self, arg): + super(ZapToken, self).__init__() + self.arg = arg diff --git a/src/ZapToken/Curve/curve.py b/src/zap_token/curve/__init__.py similarity index 100% rename from src/ZapToken/Curve/curve.py rename to src/zap_token/curve/__init__.py diff --git a/src/ZapToken/Curve/regex b/src/zap_token/curve/regex similarity index 100% rename from src/ZapToken/Curve/regex rename to src/zap_token/curve/regex diff --git a/src/ZapToken/Curve/types.py b/src/zap_token/curve/types.py similarity index 100% rename from src/ZapToken/Curve/types.py rename to src/zap_token/curve/types.py diff --git a/src/ZapToken/test/zaptoken_test.py b/src/zap_token/test/zaptoken_test.py similarity index 100% rename from src/ZapToken/test/zaptoken_test.py rename to src/zap_token/test/zaptoken_test.py diff --git a/src/zaptypes/__init__.py b/src/zaptypes/__init__.py new file mode 100644 index 00000000..794d5a43 --- /dev/null +++ b/src/zaptypes/__init__.py @@ -0,0 +1,220 @@ +from typing import TypedDict, Callable, Any, NewType +from collections import namedtuple + +address = NewType("address", str) +txid = NewType("txid", str) +NumType = NewType("NumType", float) # need confirmation on this datatype + +""" + Python has a builtin called the TypedDict that + is structured and can be accessed in a similar way + to a TypeScript interface + + Py also lacks constants, but named tuples behaves + the same way +""" + + +class defaultTx(TypedDict, total=False): + From: str + gas: float + gasPrice: float + + +class Filter(TypedDict, total=False): + fromBlock: float + toBlock: float + provider: str + subscriber: str + terminator: str + endpoint: str + ID: float + + +class listenEvent(TypedDict, total=False): + filtr: Filter + callback: Callable[..., Any] # accepts a function, takes any # of args + + +class Artifact(TypedDict, total=False): + contract_name: str + abi: dict + networks: dict = {"networkId": {"address": str}} + + +class BaseContractType(TypedDict, total=False): + """ Base Contract """ + artifactsDir: str + artifactName: str + networkId: int + networkProvider: Any or None + contract: Any + coordinator: str + address: str + web3: Any + + +class NetworkProviderOptions(TypedDict, total=False): + artifactsDir: str + networkId: int + networkProvider: Any + coordinator: str + address: str + web3: Any + + +class TransferType(defaultTx, TypedDict): + to: str + amount: float + + +Constants = namedtuple("Constants", ["DEFAULT_GAS", "NULL_ADDRESS"]) +const = Constants(int(4e5), "0x0000000000000000000000000000000000000000") +""" + accessed by const.DEFAULT_GAS and const.NULL_ADDRESS +""" + + +class SubscriptionInit(defaultTx, TypedDict, total=False): + provider: str + endpoint: str + endpoint_params: list + blocks: NumType + pubkey: NumType + + +class SubscriptionEnd(defaultTx, TypedDict, total=False): + provider: str + + +class SubscriptionType(TypedDict): + provider: str + subscriber: str + endpoint: str + + +############# BONDAGE ############# + +class TokenBondType(defaultTx, TypedDict): + endpoint: str + dots: NumType + + +class BondType(defaultTx, TypedDict, total=False): + subscriber: str + provider: str + endpoint: str + dots: NumType + + +class DelegateBondType(BondType, TypedDict): + subscriber: str + + +class UnbondType(defaultTx, TypedDict): + provider: str + endpoint: str + dots: NumType + + +class SubscribeType(defaultTx, TypedDict): + provider: str + endpoint: str + dots: NumType + endpoint_params: list # need confirmation on this data type + + +class SubscriberHandler(TypedDict, total=False): + """ + Callable types accepts a function, takes any # of args + """ + handleResponse: Callable[..., Any] + handleUnsubscription: Callable[..., Any] + handleSubscription: Callable[..., Any] + + +class ApproveType(defaultTx, TypedDict): + provider: str + zapNum: float + + +class BondArgs(defaultTx, TypedDict): + provider: str + endpoint: str + dots: NumType + + +class UnbondArgs(defaultTx, TypedDict): + provider: str + endpoint: str + dots: NumType + + +class DelegateBondArgs(defaultTx, TypedDict): + provider: str + endpoint: str + dots: NumType + subscriber: str + + +class BondageArgs(TypedDict, total=False): + subscriber: str + provider: str + endpoint: str + dots: NumType + zapNum: NumType + + +class CalcBondRateType(TypedDict): + provider: str + endpoint: str + zapNum: NumType + + +class BondFilter(Filter, TypedDict, total=False): + numDots: NumType + numZap: NumType + + +############# TOKEN DOT FACTORY ############# + +# accepts a function, takes error and hash as args +TransactionCallback = NewType( + "TransactionCallback", Callable[[str, str], None]) + + +############# PROVIDER ############# + +class InitProvider(defaultTx, TypedDict): + public_key: str + title: str + + +class InitCurve(defaultTx, TypedDict, total=False): + endpoint: str + term: list + broker: address + + +class NextEndpoint(TypedDict): + provider: address + endpoint: str + + +class EndpointParams(defaultTx, TypedDict): + endpoint: str + endpoint_params: list + + +class SetProviderParams(defaultTx, TypedDict): + key: str + value: str + + +class SetProviderTitle(defaultTx, TypedDict): + From: address + title: str + + +class Endpoint(defaultTx, TypedDict): + endpoint: str diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..f9f034f7 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +w3_playground.py \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..c5cfa7cc --- /dev/null +++ b/tests/README.md @@ -0,0 +1,28 @@ +# How to test + +## Prerequisites +```bash +# go to zappy root +cd path/to/zappy_root/ +# activate virtual environment +source ./venv/bin/activate +# install pytest with pip +pip3 install pytest +# install anyio to run async tests and web3 test pack +pip3 install anyio web3[tester] +``` +## Usage +```bash +pytest -v +``` +The `-v` option will give you a detailed output of tests. + +For extended usage documentation, take a look at the [pytest docs](https://docs.pytest.org/en/stable/usage.html). + +## Asynchronous I/O Tests + +Async I/O testing is done with the [Anyio library](https://anyio.readthedocs.io/en/latest/index.html). + +Anyio provides a flexible and scalable library that adds support for pytest async tests. + +The Python builtin `asyncio` library is supported on install. [Anyio also supports curio and trio as backends](https://anyio.readthedocs.io/en/latest/testing.html?highlight=pytest#specifying-the-backends-to-run-on). diff --git a/tests/Registry/.gitignore b/tests/Registry/.gitignore new file mode 100644 index 00000000..61a58b02 --- /dev/null +++ b/tests/Registry/.gitignore @@ -0,0 +1 @@ +test.sh \ No newline at end of file diff --git a/tests/Registry/conftest.py b/tests/Registry/conftest.py new file mode 100644 index 00000000..42d8db6b --- /dev/null +++ b/tests/Registry/conftest.py @@ -0,0 +1,190 @@ +from pytest import MonkeyPatch, fixture + +from web3 import Web3 +# from Types.types import const, txid + +from os.path import join, realpath +from sys import path +path.insert(0, realpath(join(__file__, "../../../src/"))) + +from Artifacts.src.index import Artifacts +from ZapToken.Curve.curve import Curve + +w3 = Web3(Web3.HTTPProvider("http://localhost:8545")) +abi = Artifacts["REGISTRY"] +coor_artifact = Artifacts["ZAPCOORDINATOR"] +_address = abi["networks"]["31337"]["address"] + + +""" Arrange/SetUp Section +""" + + +@fixture(scope="module") +def reg_contract(): + _reg_contract = w3.eth.contract(address=_address, + abi=abi["abi"]) + return _reg_contract + + +@fixture(scope="module") +def coor(): + _coor = w3.eth.contract( + abi=coor_artifact['abi'], + address=coor_artifact['networks']["31337"]['address']) + return _coor + + +@fixture(scope="module") +def funcs(reg_contract): + _funcs = reg_contract.functions.__dict__ + del _funcs['_functions'] + del _funcs['abi'] + return _funcs + + +@fixture(scope="module") +def coor_funcs(coor): + _coor_funcs = coor.functions.__dict__ + del _coor_funcs['_functions'] + del _coor_funcs['abi'] + return _coor_funcs + + +def _ZapRegistry(reg_contract): + """ WIP: Returns an object representation of ZapRegistry for testing. + + This ZapRegistry object has a mocked BaseContract + used for its init phase. + + This mocked BaseContract has fixture data, including its contract. + + Contracts (and/or their functions) are asynchronous mock objects. + Almost everything else is base of Magic mock objects. + + """ + + mp = MonkeyPatch() + # same as setting the return_value of a mock obj + + class MockBaseContract: + def __init__(self, artifact_name, artifact_dir=None, network_id=None, + network_provider=None, coordinator=None, + address=None, web3=None): + + self.name = artifact_name + self.artifact = Artifacts[artifact_name] + coor_artifact = Artifacts["ZAPCOORDINATOR"] + self.provider = web3 or w3 + self.network_id = network_id or "1" + self.coordinator = self.provider.eth.contract( + abi=coor_artifact['abi'], + address=coor_artifact['networks'][self.network_id]['address']) + + self.address = self.artifact["networks"][self.network_id]["address"] + self.contract = reg_contract + + mp.setattr("Registry.registry.BaseContract", MockBaseContract) + from Registry.registry import ZapRegistry + + try: + return(ZapRegistry({"network_id": "31337"})) + except Exception as e: + raise e + + +@fixture(scope="module") +def Zap_Registry(): + """ yield a ZapRegistry object + + This object stays in pytest cache for the lifespan of the test. + It is not necessary to have a SetUp class method/function. + + references: + https://docs.pytest.org/en/stable/xunit_setup.html + https://docs.pytest.org/en/stable/fixture.html#what-fixtures-are + https://docs.pytest.org/en/stable/fixture.html#\ + fixtures-can-be-requested-more-than-once-per-test-return-values-are-cached + """ + zap_reg_obj = _ZapRegistry + yield zap_reg_obj + del zap_reg_obj + + +@fixture(scope="module") +def functions(reg_contract): + return reg_contract.functions + + +@fixture(scope="module") +def oracle(functions): + return functions.getOracleAddress(0).call() + + +@fixture(scope="module") +def account(): + return w3.eth.accounts[11] + + +@fixture(scope="module") +def curve_values(): + return [3, 0, 0, 3, 100000] + + +@fixture(scope="module") +def testZapProvider(curve_values): + _testZapProvider = { + "pubkey": 111, + "title": 'testProvider', + "endpoint_params": ['p1', 'p2'], + "endpoint": 'testEndpoint', + "query": 'btcPrice', + "curve": Curve(curve_values), + "broker": '0x0000000000000000000000000000000000000000' + } + + return _testZapProvider + + +@fixture(scope="module") +def endpoint(testZapProvider): + return testZapProvider["endpoint"] + + +@fixture(scope="module") +def endpoint_params(testZapProvider): + return testZapProvider["endpoint_params"] + + +@fixture(scope="module") +def broker(testZapProvider): + return testZapProvider["broker"] + + +@fixture(scope="module") +def pubkey(testZapProvider): + return testZapProvider["pubkey"] + + +@fixture(scope="module") +def title(testZapProvider): + return testZapProvider["title"] + + +@fixture(scope="module") +def anyio_backend(): + """ Ensures anyio uses the default, pytest-asyncio plugin + for running async tests + """ + return 'asyncio' + + +@fixture(scope="class") +def instance(Zap_Registry, reg_contract): + """ SetUp: ZapRegistry instance. + + This instance is cached and can be reused for + the remainder of the test. + """ + instance = Zap_Registry(reg_contract) + return instance diff --git a/tests/Registry/test_registry.py b/tests/Registry/test_registry.py new file mode 100644 index 00000000..83edc9e7 --- /dev/null +++ b/tests/Registry/test_registry.py @@ -0,0 +1,240 @@ +""" Tests the setting up, transactions, and calls of the Zap Registry contract. + + This is an integration test that uses the Zap Hardhat testing environment + to test the output and side effects of Zap Registry. + + Web3 is initialized and used to make calls and transactions to Hardhat and + receive events from the testing environment. + + Uses Pytest's fixture decorators to facilitate the setup of this test. + Fixtures are ran once the first time they're requested and their return + values are cached for the remainder of the test. +""" +from pytest import fixture, mark + +from web3 import Web3 + +from os.path import join, realpath +from sys import path +path.insert(0, realpath(join(__file__, "../../../src/"))) + +from ZapToken.Curve.curve import Curve +from Artifacts.src.index import Artifacts + + +""" pytest section +""" + + +class TestRegistry: + """ Tests the initialization and functionality of Zap's Registry Contract + """ + + def test_init(self, instance, funcs, coor_funcs): + """ Test if ZapRegistry is initialized + + Tests is the static attributes are all present and are valid + """ + contract = instance.contract + coor = instance.coordinator + functions_dict = contract.functions.__dict__ + coordinator_dict = coor.functions.__dict__ + + assert instance + assert instance.name == "REGISTRY" + assert instance.artifact == Artifacts["REGISTRY"] + assert isinstance(instance.artifact, dict) + assert isinstance(instance.provider, Web3) + assert instance.provider == instance.contract.web3 + assert instance.network_id == "31337" + assert instance.address == "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + assert instance.address == instance.contract.address + + for func, coor_func in zip(funcs, coor_funcs): + assert func in functions_dict + assert coor_func in coordinator_dict + + # @mark.skip(reason="Provider already initialized.") + @mark.anyio + async def test_initiate_provider(self, instance, pubkey, account, title): + opt = {"public_key": pubkey, "title": title, + "From": account, "gas": 4 * 10**5} + tx = await instance.initiate_provider(**opt) + + assert isinstance(tx, str) + + @mark.anyio + async def test_get_provider_publickey(self, instance, pubkey, account): + pubkey_instance = await instance.get_provider_publickey(account) + + assert pubkey_instance + assert isinstance(pubkey_instance, int) + assert pubkey_instance == pubkey + + @mark.anyio + async def test_get_provider_title(self, instance, account): + title_instance = await instance.get_provider_title(account) + + assert title_instance + assert isinstance(title_instance, str) + # assert title_instance == + + @mark.anyio + async def test_is_provider_initiated(self, instance, account): + isInit = await instance.is_provider_initiated(account) + + assert isInit is True + assert isinstance(isInit, bool) + + @mark.anyio + async def test_set_provider_title(self, instance, account): + title_test = "REGISTRY" + + tx = await instance.set_provider_title(account, title_test) + + assert tx + print(tx) + assert isinstance(tx, str) + + @mark.anyio + async def test_set_provider_param(self, instance, pubkey, account): + tx = await instance.set_provider_param(pubkey, 11, account) + + assert tx + assert isinstance(tx, str) + + @mark.anyio + async def test_get_provider_param(self, instance, account, pubkey): + param = await instance.get_provider_param( + account, Web3.toBytes(pubkey)) + + assert param + print(param) + assert isinstance(param, bytes) + + @mark.anyio + async def test_get_all_providers(self, instance): + provs = await instance.get_all_providers() + assert provs + assert isinstance(provs, list) + + @mark.anyio + async def get_all_provider_params(self, instance, address, pubkey): + params = await instance.get_provider_param( + address, pubkey) + + assert params + print(params) + assert isinstance(params, list) + + # @mark.skip(reason="Possible error with python types") + @mark.anyio + async def test_initiate_provider_curve(self, instance, account, + endpoint, curve_values): + term = curve_values + opts = {"end_point": endpoint, "term": term, + "From": account, "gasPrice": int(5e4)} + + curve = await instance.initiate_provider_curve(**opts) + + assert curve + assert isinstance(curve, str) + + @mark.anyio + async def test_get_provider_address_by_index(self, instance): + addy = await instance.get_provider_address_by_index(0) + assert addy + assert isinstance(addy, str) + + @mark.anyio + async def test_get_provider_endpoints(self, instance, account): + e = await instance.get_provider_endpoints(account) + + assert e + print(e) + assert isinstance(e, list) + + return e + + @mark.anyio + async def test_get_provider_curve(self, instance, + account, endpoint): + e = endpoint + curve = await instance.get_provider_curve(account, e) + + assert curve + print(curve.values) + assert isinstance(curve, Curve) + + def decode_params(self, instance, params): + return(instance.decode_params(params)) + + def encode_params(self, instance, params): + return(instance.encode_params(params)) + + @mark.anyio + async def test_set_endpoint_params(self, instance, account, + endpoint, endpoint_params): + tx = await instance.set_endpoint_params( + endpoint, account, + int(5e4), + endpoint_params=endpoint_params) + + assert tx + assert isinstance(tx, str) + + @mark.anyio + async def test_get_endpoint_broker(self, instance, broker): + broker = await instance.get_endpoint_broker(broker, "123") + + assert broker + assert isinstance(broker, str) + + @mark.anyio + async def test_is_endpoint_set(self, instance, account, endpoint): + isSet = await instance.is_endpoint_set(account, endpoint) + + assert isSet is True + assert isinstance(isSet, bool) + + @mark.anyio + async def test_clear_endpoint(self, instance, + endpoint, + account): + e = endpoint + tx = await instance.clear_endpoint( + e, account, int(5e3)) + + assert tx + print(tx) + assert isinstance(tx, str) + + # @mark.anyio + # async def test_(self, instance): + # get = await instance.something() + + # assert get + # print(get) + # assert isinstance(get, bytes) + + +# async def task(co_mock): +# """ Will handle awaitables from contract calls/tx +# """ +# await co_mock + +""" python "direct calls" section +""" + +# import pprint +# from asyncio import run +# provs = asyncio.run(_ZapRegistry().get_all_providers()) +# zap_reg_obj = _ZapRegistry() +# provs = zap_reg_obj.get_all_providers() + +# print(run(task(zap_reg_obj.contract.functions.getAllOracles()))) + +# pprint(zap_reg_obj.contract.functions.getAllOracles.__dict__) + +# def test_init(): +# assert isinstance(_ZapRegistry(), ZapRegistry) diff --git a/src/ZapToken/Curve/__init__.py b/tests/Subscriber/__init__.py similarity index 100% rename from src/ZapToken/Curve/__init__.py rename to tests/Subscriber/__init__.py diff --git a/tests/Subscriber/conftest.py b/tests/Subscriber/conftest.py new file mode 100644 index 00000000..81cbbd6b --- /dev/null +++ b/tests/Subscriber/conftest.py @@ -0,0 +1,214 @@ +from pytest import MonkeyPatch, fixture +from unittest.mock import Mock + +from web3 import Web3 + +from os.path import join, realpath +from sys import path +path.insert(0, realpath(join(__file__, "../../../src/"))) + +from artifacts.src import Artifacts +from zap_token import Curve +from zaptypes import const + + +_w3 = Web3(Web3.HTTPProvider("http://localhost:8545")) +abi_dir = {"registry": Artifacts["REGISTRY"], + "dispatch": Artifacts["DISPATCH"], + "bondage": Artifacts["BONDAGE"], + "arbiter": Artifacts["ARBITER"], + "zap_token": Artifacts["ZAP_TOKEN"] + } + + +@fixture(scope="module") +def w3(): + return _w3 + + +# @fixture(scope="module") +# def zt(): +# zt_abi = Artifacts["ZAP_TOKEN"] +# zt_address =\ +# _w3.toChecksumAddress( +# zt_abi["networks"]["development"]["address"]) + +# return (zt_abi, zt_address) + + +coor_artifact = Artifacts["ZAPCOORDINATOR"] +_address_dir = {contract: + Web3.toChecksumAddress( + abi["networks"]["31337"]["address"]) + for contract, abi in abi_dir.items()} + +""" Arrange/SetUp Section +""" + + +@fixture(scope="module") +def contracts(): + _contracts = {name: _w3.eth.contract(address=address, + abi=abi_dir[name]["abi"]) + for name, address in _address_dir.items()} + return _contracts + + +@fixture(scope="module") +def coor(): + _coor = _w3.eth.contract( + abi=coor_artifact['abi'], + address=coor_artifact['networks']["31337"]['address']) + return _coor + + +@fixture(scope="module") +def funcs(contracts): + _funcs = {} + for name, contract in contracts.items(): + _funcs[name] = contract.functions.__dict__ + del _funcs[name]['_functions'] + del _funcs[name]['abi'] + return _funcs + + +@fixture(scope="module") +def coor_funcs(coor): + _coor_funcs = coor.functions.__dict__ + del _coor_funcs['_functions'] + del _coor_funcs['abi'] + return _coor_funcs + + +@fixture(scope="module") +def accounts(): + return _w3.eth.accounts + + +@fixture(scope="class") +def owner(accounts): + return accounts[0] + + +@fixture(scope="class") +def subscriber(accounts): + return accounts[1] + + +@fixture(scope="class") +def oracle(accounts): + return accounts[2] + + +@fixture(scope="class") +def broker(accounts): + return accounts[3] + + +def _ZapSubscriber(contracts, funcs, subscriber): + + mp = MonkeyPatch() + + """ Mocked Contract classes + + These mocked objects have defined constants within this body + and will have variables to test various correct and erroneous + test cases within the test module. + + constants: + ContractClass.contact: the Solidty contract + """ + + # ZapBondage + mock_Bondage = Mock() + + # ZapToken + mock_ZapToken = Mock() + # print(mock_ZapToken.contract) + # mock_ZapToken.contract = + + mock_Dispatch = Mock() # ZapDispatch + mock_Arbiter = Mock() # ZapArbiter + mock_Registry = Mock() # ZapRegistry + + mp.setattr("subscriber.ZapBondage", mock_Bondage) + mp.setattr("subscriber.ZapToken", mock_ZapToken) + mp.setattr("subscriber.ZapDispatch", mock_Dispatch) + mp.setattr("subscriber.ZapArbiter", mock_Arbiter) + mp.setattr("subscriber.ZapRegistry", mock_Registry) + + # reminder: might need to run the Mock.configure_mock(**attrs) + # before running subscriber tests + + from subscriber import ZapSubscriber + + try: + return( + ZapSubscriber( + subscriber, + {"network_id.return_value": "31337"})) + except Exception as e: + raise e + + +@fixture(scope="module") +def Zap_Subscriber(): + zap_sub_obj = _ZapSubscriber + yield zap_sub_obj + del zap_sub_obj + + +@fixture(scope="module") +def hardhat_provider(): + return "http://localhost:8545" + + +@fixture(scope="module") +def provider(accounts): + return {"pubkey": 102, + "title": '0x426f72676573697573', + "address": accounts[2], + "endpoint_params": ['param1', 'param2'], + "endpoint": 'Wiles', + "query": 'btcPrice', + "curve": Curve([3, 0, 0, 1, 1222]), + "broker": '0x0000000000000000000000000000000000000000' + } + + +@fixture(scope="module") +def function(): + pass + + +@fixture(scope="module") +def anyio_backend(): + """ Ensures anyio uses the default, pytest-asyncio plugin + for running async tests + """ + return 'asyncio' + + +@fixture(scope="class") +def instance(Zap_Subscriber, contracts, funcs, subscriber): + return Zap_Subscriber(contracts, funcs, subscriber) + + +# @fixture(scope="module") +# def function(): +# return None + + +# @fixture(scope="module") +# def function(): +# return None + + +# @fixture(scope="module") +# def function(): +# return None + + +# @fixture(scope="module") +# def function(): +# return None diff --git a/tests/Subscriber/encode_params/__init__.py b/tests/Subscriber/encode_params/__init__.py new file mode 100644 index 00000000..c5238bc8 --- /dev/null +++ b/tests/Subscriber/encode_params/__init__.py @@ -0,0 +1,25 @@ +from web3 import Web3 + + +def encode_params(self, endpoint_params: list = [], ): + """ needed for + """ + pars = endpoint_params + hex_params =\ + [el if el.find('0x') == 0 else Web3.toHex(text=el) for el in pars] + bytes_params =\ + [Web3.toBytes(hexstr=hex_p) for hex_p in hex_params] + params = [] + + from math import ceil + for element in bytes_params: + if len(element) <= 32: + params.append(Web3.toHex(element)) + continue + chunks_len = ceil((len(element) + 2) / 32) + param_bytes_w_len = [0, chunks_len].extend(element) + for i in range(0, chunks_len): + start = i * 32 + end = start + 32 + params.append(Web3.toHex(param_bytes_w_len[start:end])) + return params diff --git a/tests/Subscriber/test_subscriber.py b/tests/Subscriber/test_subscriber.py new file mode 100644 index 00000000..84f4b82b --- /dev/null +++ b/tests/Subscriber/test_subscriber.py @@ -0,0 +1,339 @@ +from unittest.mock import AsyncMock +from pytest import mark, fixture + +from web3 import Web3 +from . import encode_params + +from os.path import join, realpath +from sys import path +path.insert(0, realpath(join(__file__, "../../../src/"))) + +from zaptypes import const + + +""" pytest section +""" + +class TestSubscriber: + + query_id = "" + + global tx_meta + tx_meta = "" + + def test_init(self, instance): + assert instance + + """ contract patching + """ + + @fixture(scope="class", autouse=True) + def patch_contracts(self, instance, contracts, oracle): + instance.zap_registry.contract = contracts["registry"] + instance.zap_token.contract = contracts["zap_token"] + instance.zap_bondage.contract = contracts["bondage"] + instance.zap_dispatch.contract = contracts["dispatch"] + + @fixture(scope="class", autouse=True) + def patch_all_contract_functions(self, instance, subscriber, w3): + """ Patches all contract functions before tests + """ + + global tx_meta + + tx_meta = {"From": instance.subscriber_owner, + "gas_price": w3.eth.gas_price} + + """ ZapToken Functions + """ + + # ZapToken.balance_of + def side_effect(acc): + return\ + instance.zap_token.contract.functions.balanceOf( + acc).call() + + instance.zap_token.balance_of = AsyncMock() + instance.zap_token.balance_of.side_effect = side_effect + + """ Bondage Function + """ + + # Bondage.calc_zap_for_dots + def side_effect(prov, ep, dots): + return instance.zap_bondage.contract.functions.calcZapForDots( + prov, Web3.toBytes(text=ep), dots).call() + + instance.zap_bondage.calc_zap_for_dots = AsyncMock() + instance.zap_bondage.calc_zap_for_dots.side_effect = side_effect + + # Bondage.get_bound_dots + def side_effect(sub, prov, ep): + return instance.zap_bondage.contract.functions.getBoundDots( + sub, prov, Web3.toBytes(text=ep)).call() + + instance.zap_bondage.get_bound_dots = AsyncMock() + instance.zap_bondage.get_bound_dots.side_effect = side_effect + + # Bondage.bond + def side_effect(prov, ep, dts, From, gas_price, gas, cb=None): + return instance.zap_bondage.contract.functions.bond( + prov, Web3.toBytes(text=ep), int(dts)).transact( + {"from": subscriber, "gas": const.DEFAULT_GAS, + "gasPrice": w3.eth.gas_price}) + + instance.zap_bondage.bond = AsyncMock() + instance.zap_bondage.bond.side_effect = side_effect + + # Bondage.unbond + def side_effect(prov, ep, dts, From, gas_price, gas, cb=None): + return instance.zap_bondage.contract.functions.unbond( + prov, Web3.toBytes(text=ep), int(dts)).transact( + {"from": subscriber, "gas": const.DEFAULT_GAS, + "gasPrice": w3.eth.gas_price}) + + instance.zap_bondage.unbond = AsyncMock() + instance.zap_bondage.unbond.side_effect = side_effect + + @fixture(scope="class", autouse=True) + def prepare_tokens(self, instance, + subscriber, owner, broker, w3): + bondage_owner = instance.zap_bondage.contract.address + instance.zap_token.contract.functions.allocate( + owner, 1500000000000000000000000000000).transact( + {"from": owner, "gas": const.DEFAULT_GAS, + "gasPrice": w3.eth.gas_price}) + instance.zap_token.contract.functions.allocate( + subscriber, 50000000000000000000000000000).transact( + {"from": owner, "gas": const.DEFAULT_GAS, + "gasPrice": w3.eth.gas_price}) + + instance.zap_token.contract.functions.allocate( + broker, 50000000000000000000000000000).transact( + {"from": owner, "gas": const.DEFAULT_GAS, + "gasPrice": w3.eth.gas_price}) + + instance.zap_token.contract.functions.approve( + bondage_owner, 1000000000000000000000000000000).transact( + {"from": subscriber, "gas": const.DEFAULT_GAS, + "gasPrice": w3.eth.gas_price}) + + instance.zap_token.contract.functions.approve( + bondage_owner, 1000000000000000000000000000000).transact( + {"from": broker, "gas": const.DEFAULT_GAS, + "gasPrice": w3.eth.gas_price}) + + + @mark.anyio + async def test_get_zap_balance(self, instance): + + balance = await instance.get_zap_balance() + + assert isinstance(balance, int) + print("Subscriber Balance: ", balance) + + @mark.anyio + async def test_get_zap_allowance(self, instance, contracts): + allowance = await instance.get_zap_allowance() + + assert isinstance(allowance, int) + print("Subscriber allowance: ", allowance) + + @mark.skip("Already approving on SetUp") + @mark.anyio + async def test_approve_to_bond(self, instance, oracle, + w3, provider): + def side_effect(to, amount): + tx_hash = instance.zap_token.contract.functions.approve( + to, amount).transact().hex() + receipt = w3.eth.getTransactionReceipt(tx_hash) + log = instance.zap_token.contract.events.Approval( + ).processReceipt(receipt) + try: + return not log[0]["errors"] + except KeyError: + return True + + instance.zap_token.approve = AsyncMock() + instance.zap_token.approve.side_effect = side_effect + + approve = await\ + instance.approve_to_bond(1000000000) + + assert approve + + @mark.anyio + async def test_bond(self, instance, provider, + subscriber, oracle, broker): + + await\ + instance.bond( + oracle, + provider["endpoint"], + 999, **tx_meta) + + bonded = await instance.zap_bondage.get_bound_dots( + subscriber, + oracle, + provider["endpoint"]) + + assert bonded + assert bonded == 999 + + await\ + instance.unbond( + oracle, + provider["endpoint"], + 999, **tx_meta) + + @mark.anyio + async def test_subscribe(self, instance, oracle, provider, + subscriber, contracts, w3): + + def side_effect(prov): + return\ + instance.zap_registry.contract.functions.getProviderPublicKey( + prov).call() + + instance.zap_registry.get_provider_publickey = AsyncMock() + instance.zap_registry.get_provider_publickey.side_effect =\ + side_effect + + instance.zap_arbiter.contract = contracts["arbiter"] + + def side_effect(provider, endpoint, + endpoint_params, pubkey, + blocks, From, gas_price, gas, cb): + endpoint_params =\ + [Web3.toBytes(text=param) + if not isinstance(param, bytes) + else param + for param in endpoint_params] + return\ + instance.zap_arbiter.contract.functions.initiateSubscription( + provider, Web3.toBytes(text=endpoint), + endpoint_params, pubkey, blocks).transact( + {"from": subscriber, "gas": const.DEFAULT_GAS, + "gasPrice": w3.eth.gas_price}).hex() + + instance.zap_arbiter.initiate_subscription = AsyncMock() + instance.zap_arbiter.initiate_subscription.side_effect =\ + side_effect + + bounded = await instance.zap_bondage.get_bound_dots( + subscriber, oracle, provider["endpoint"]) + + if bounded == 0: + await instance.bond( + oracle, + provider["endpoint"], + 100, **tx_meta) + + sub = await instance.subscribe(oracle, provider["endpoint"], + provider["endpoint_params"], 100, + **tx_meta) + assert isinstance(sub, str) + assert sub + + @mark.anyio + async def test_query_data(self, instance, w3, + oracle, provider, subscriber): + + global query_id + + def side_effect(prov, q, ep, ep_params, From, gas_price, gas, cb=None): + return\ + instance.zap_dispatch.contract.functions.query( + prov, q, Web3.toBytes(text=ep), ep_params).transact( + {"from": subscriber, "gas": const.DEFAULT_GAS, + "gasPrice": w3.eth.gas_price}) + + instance.zap_dispatch.query_data = AsyncMock() + instance.zap_dispatch.query_data.side_effect = side_effect + + params = encode_params.encode_params(provider["endpoint_params"]) + + bounded = await instance.zap_bondage.get_bound_dots( + subscriber, oracle, provider["endpoint"]) + + if bounded < 999: + await instance.bond( + oracle, + provider["endpoint"], + 999, + **tx_meta) + + query = await instance.query_data( + oracle, "query", provider["endpoint"], + params, + **tx_meta) + + assert query + + receipt = w3.eth.getTransactionReceipt(query) + query_id = instance.zap_dispatch.contract.events.Incoming( + ).processReceipt( + receipt)[0]["args"]["id"] + + @mark.anyio + async def test_cancel_query(self, instance, w3, subscriber): + + def side_effect(q_id, From, gas_price, gas, cb): + return\ + instance.zap_dispatch.contract.functions.cancelQuery( + int(q_id)).transact( + {"from": subscriber, "gas": const.DEFAULT_GAS, + "gasPrice": w3.eth.gas_price}) + + instance.zap_dispatch.cancel_query = AsyncMock() + instance.zap_dispatch.cancel_query.side_effect =\ + side_effect + + await instance.cancel_query(query_id, **tx_meta) + + @mark.anyio + async def test_delegate_bond(self, instance, w3, + provider, oracle, subscriber): + + def side_effect(sub, ep, dots, prov, From, gas_price, gas, cb=None): + return\ + instance.zap_bondage.contract.functions.delegateBond( + sub, prov, Web3.toBytes(text=ep), dots).transact( + {"from": subscriber, "gas": const.DEFAULT_GAS, + "gasPrice": w3.eth.gas_price}) + + instance.zap_bondage.delegate_bond = AsyncMock() + instance.zap_bondage.delegate_bond.side_effect =\ + side_effect + + del_bounded = await instance.delegate_bond( + oracle, instance.zap_bondage.contract.address, + provider["endpoint"], 1, **tx_meta) + + instance.zap_bondage.functions.unbond + + assert del_bounded + assert isinstance(del_bounded, int) + assert del_bounded > 0 + + tx_meta2 = tx_meta + tx_meta2["From"] = instance.zap_bondage.contract.address + instance.unbond( + oracle, provider["endpoint"], 1, **tx_meta2) + + @mark.anyio + async def test_get_num_escrow(self, instance, provider, oracle): + + def side_effect(provider, subscriber, endpoint): + return\ + instance.zap_bondage.contract.functions.getNumEscrow( + provider, subscriber, Web3.toBytes(text=endpoint)).call() + + instance.zap_bondage.get_num_escrow = AsyncMock() + instance.zap_bondage.get_num_escrow.side_effect =\ + side_effect + + escrowed = await instance.get_num_escrow( + oracle, provider["endpoint"]) + + assert isinstance(escrowed, int)