diff --git a/README.md b/README.md index 8fd69ba..3610293 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,12 @@ A python version of [ain-js](https://www.npmjs.com/package/@ainblockchain/ain-js). + ## Installation ``` pip install ain-py ``` -## Run all test -``` -tox -``` ## Examples ```python @@ -25,4 +22,28 @@ async def process(): loop = asyncio.get_event_loop() loop.run_until_complete(process()) -``` \ No newline at end of file +``` + + +## Test How-To +1. Clone AIN Blockchain and install +``` +git clone git@github.com:ainblockchain/ain-blockchain.git +yarn install +``` + +2. Start blockchain locally +``` +bash start_local_blockchain.sh +``` + +3. Run tests +``` +tox +``` + + +## License + +This project is licensed under the MIT License + diff --git a/ain/ain.py b/ain/ain.py index 8becdea..80fbaf5 100644 --- a/ain/ain.py +++ b/ain/ain.py @@ -1,10 +1,11 @@ -import asyncio from typing import List, Any, Union from ain.provider import Provider from ain.net import Network from ain.wallet import Wallet from ain.types import AinOptions, TransactionInput, TransactionBody, ValueOnlyTransactionInput from ain.db import Database +from ain.signer import Signer +from ain.signer.default_signer import DefaultSigner from ain.utils import getTimestamp, toChecksumAddress class Ain: @@ -28,6 +29,8 @@ class Ain: """The `Network` instance.""" wallet: Wallet """The `Wallet` instance.""" + signer: Signer + """The `Signer` instance.""" def __init__(self, providerUrl: str, chainId: int = 0, ainOptions: AinOptions = AinOptions()): self.provider = Provider(self, providerUrl) @@ -36,6 +39,7 @@ def __init__(self, providerUrl: str, chainId: int = 0, ainOptions: AinOptions = self.net = Network(self.provider) self.wallet = Wallet(self, self.chainId) self.db = Database(self, self.provider) + self.signer = DefaultSigner(self.wallet, self.provider) def setProvider(self, providerUrl: str, chainId: int = 0): """Sets a new provider @@ -50,6 +54,14 @@ def setProvider(self, providerUrl: str, chainId: int = 0): self.wallet.chainId = chainId self.db = Database(self, self.provider) + def setSigner(self, signer: Signer): + """Sets a new signer + + Args: + signer (Signer): The signer to set. + """ + self.signer = signer + # TODO(kriii): Return objects typing. async def getBlock( @@ -141,9 +153,7 @@ async def getTransaction(self, transactionHash: str) -> Any: Returns: The transaction with the given transaction hash. """ - return await self.provider.send( - "ain_getTransactionByHash", {"hash": transactionHash} - ) + return await self.provider.send("ain_getTransactionByHash", {"hash": transactionHash}) async def getStateUsage(self, appName: str) -> Any: """Gets a state usage with the given app name. @@ -154,9 +164,7 @@ async def getStateUsage(self, appName: str) -> Any: Returns: The state usage with the given app name. """ - return await self.provider.send( - "ain_getStateUsage", {"app_name": appName} - ) + return await self.provider.send("ain_getStateUsage", {"app_name": appName}) async def validateAppName(self, appName: str) -> Any: """Validate a given app name. @@ -167,56 +175,43 @@ async def validateAppName(self, appName: str) -> Any: Returns: The validity of the given app name. """ - return await self.provider.send( - "ain_validateAppName", {"app_name": appName} - ) + return await self.provider.send("ain_validateAppName", {"app_name": appName}) - async def sendTransaction(self, transactionObject: TransactionInput) -> Any: - """Signs and sends the transaction to the network. + async def sendTransaction(self, transactionObject: TransactionInput, isDryrun = False) -> Any: + """Signs and sends a transaction to the network. Args: - transactionObject (TransactionInput): The transaction. + transactionObject (TransactionInput): The transaction input object. + isDryrun (bool): The dryrun option. Returns: - The transaction result. + The return value of the blockchain API. """ - txBody = await self.buildTransactionBody(transactionObject) - signature = self.wallet.signTransaction( - txBody, getattr(transactionObject, "address", None) - ) - return await self.sendSignedTransaction(signature, txBody) + return await self.signer.sendTransaction(transactionObject, isDryrun) - async def sendSignedTransaction(self, signature: str, txBody: TransactionBody) -> Any: + async def sendSignedTransaction(self, signature: str, txBody: TransactionBody, isDryrun = False) -> Any: """Sends a signed transaction to the network. Args: - signature (str): The signature of the transaction. + signature (str): The signature. txBody (TransactionBody): The transaction body. + isDryrun (bool): The dryrun option. Returns: - The transaction result. + The return value of the blockchain API. """ - return await self.provider.send( - "ain_sendSignedTransaction", {"signature": signature, "tx_body": txBody} - ) + return await self.signer.sendSignedTransaction(signature, txBody, isDryrun) async def sendTransactionBatch(self, transactionObjects: List[TransactionInput]) -> List[Any]: - """Sends a signed transactions to the network. + """Signs and sends multiple transactions in a batch to the network. Args: - transactionObjects (List[TransactionInput]): The list of the transactions. + transactionObjects (List[TransactionInput]): The list of the transaction input objects. Returns: - The transaction results. + The return value of the blockchain API. """ - txListCoroutines = [] - for txInput in transactionObjects: - txListCoroutines.append(self.__buildSignedTransaction(txInput)) - - txList = await asyncio.gather(*txListCoroutines) - return await self.provider.send( - "ain_sendSignedTransactionBatch", {"tx_list": txList} - ) + return await self.signer.sendTransactionBatch(transactionObjects) def depositConsensusStake(self, input: ValueOnlyTransactionInput) -> Any: """Sends a transaction that deposits AIN for consensus staking. @@ -251,69 +246,11 @@ async def getConsensusStakeAmount(self, account: str = None) -> Any: The amount of the AIN of that address. """ if account is None: - address = self.wallet.getImpliedAddress() + address = self.signer.getAddress() else: address = toChecksumAddress(account) return await self.db.ref(f"/deposit_accounts/consensus/{address}").getValue() - async def getNonce(self, args: dict) -> Any: - """Gets a current transaction count of account, which is the nonce of the account. - - Args: - args (dict): May contain a string 'address' and a string 'from' values. - The 'address' indicates the address of the account to get the - nonce of, and the 'from' indicates where to get the nonce from. - It could be either the pending transaction pool ("pending") or - the committed blocks ("committed"). The default value is "committed". - - Returns: - The nonce of the account. - """ - params = dict(args) - if "address" in args: - params["address"] = toChecksumAddress(args["address"]) - else: - params["address"] = self.wallet.getImpliedAddress() - - if "from" in args: - if args["from"] != "pending" and args["from"] != "committed": - raise ValueError("'from' should be either 'pending' or 'committed'") - - ret = await self.provider.send("ain_getNonce", params) - return ret - - async def buildTransactionBody( - self, transactionInput: TransactionInput - ) -> TransactionBody: - """Builds a transaction body from the transaction input. - - Args: - transactionInput (TransactionInput): The transaction input. - - Returns: - TransactionBody: The builded transaction body. - """ - address = self.wallet.getImpliedAddress( - getattr(transactionInput, "address", None) - ) - operation = transactionInput.operation - parent_tx_hash = getattr(transactionInput, "parent_tx_hash", None) - nonce = getattr(transactionInput, "nonce", None) - if nonce is None: - nonce = await self.getNonce({"address": address, "from": "pending"}) - timestamp = getattr(transactionInput, "timestamp", getTimestamp()) - gas_price = getattr(transactionInput, "gas_price", 0) - billing = getattr(transactionInput, "billing", None) - - return TransactionBody( - operation=operation, - parent_tx_hash=parent_tx_hash, - nonce=nonce, - timestamp=timestamp, - gas_price=gas_price, - billing=billing, - ) - def __stakeFunction(self, path: str, input: ValueOnlyTransactionInput) -> Any: """A base function for all staking related database changes. It builds a deposit/withdraw transaction and sends the transaction by calling sendTransaction(). @@ -322,20 +259,7 @@ def __stakeFunction(self, path: str, input: ValueOnlyTransactionInput) -> Any: raise ValueError("a value should be specified.") if type(input.value) is not int: raise ValueError("value has to be a int.") - input.address = self.wallet.getImpliedAddress(getattr(input, "address", None)) + input.address = self.signer.getAddress(getattr(input, "address", None)) ref = self.db.ref(f'{path}/{input.address}').push() input.ref = "value" return ref.setValue(input) - - async def __buildSignedTransaction(self, transactionObject: TransactionInput) -> dict: - """Returns a builded transaction with the signature""" - txBody = await self.buildTransactionBody(transactionObject) - if not hasattr(transactionObject, "nonce"): - # Batch transactions' nonces should be specified. - # If they're not, they default to un-nonced (nonce = -1). - txBody.nonce = -1 - - signature = self.wallet.signTransaction( - txBody, getattr(transactionObject, "address", None) - ) - return {"signature": signature, "tx_body": txBody} diff --git a/ain/db/ref.py b/ain/db/ref.py index 8926dcc..032304e 100644 --- a/ain/db/ref.py +++ b/ain/db/ref.py @@ -160,12 +160,13 @@ async def get(self, gets: List[GetOperation]) -> Any: req = {"type": "GET", "op_list": extendedGets} return await self._ain.provider.send("ain_get", req) - async def deleteValue(self, transactionInput: ValueOnlyTransactionInput = None) -> Any: + async def deleteValue(self, transactionInput: ValueOnlyTransactionInput = None, isDryrun = False) -> Any: """Deletes the value. Args: transactionInput (ValueOnlyTransactionInput): The transaction input object. Any value given will be overwritten with null. + isDryrun (bool): Dryrun option. Returns: The result of the transaction. @@ -184,14 +185,16 @@ async def deleteValue(self, transactionInput: ValueOnlyTransactionInput = None) TransactionInput( operation=operation, parent_tx_hash=getattr(txInput, "parent_tx_hash", None), - ) + ), + isDryrun ) - async def setFunction(self, transactionInput: ValueOnlyTransactionInput) -> Any: + async def setFunction(self, transactionInput: ValueOnlyTransactionInput, isDryrun = False) -> Any: """Sets the function config. Args: transactionInput (ValueOnlyTransactionInput): The transaction input object. + isDryrun (bool): Dryrun option. Returns: The result of the transaction. @@ -203,14 +206,16 @@ async def setFunction(self, transactionInput: ValueOnlyTransactionInput) -> Any: ref, "SET_FUNCTION", self._isGlobal - ) + ), + isDryrun ) - async def setOwner(self, transactionInput: ValueOnlyTransactionInput) -> Any: + async def setOwner(self, transactionInput: ValueOnlyTransactionInput, isDryrun = False) -> Any: """Sets the owner rule. Args: transactionInput (ValueOnlyTransactionInput): The transaction input object. + isDryrun (bool): Dryrun option. Returns: The result of the transaction. @@ -222,14 +227,16 @@ async def setOwner(self, transactionInput: ValueOnlyTransactionInput) -> Any: ref, "SET_OWNER", self._isGlobal - ) + ), + isDryrun ) - async def setRule(self, transactionInput: ValueOnlyTransactionInput) -> Any: + async def setRule(self, transactionInput: ValueOnlyTransactionInput, isDryrun = False) -> Any: """Sets the write rule. Args: transactionInput (ValueOnlyTransactionInput): The transaction input object. + isDryrun (bool): Dryrun option. Returns: The result of the transaction. @@ -241,14 +248,16 @@ async def setRule(self, transactionInput: ValueOnlyTransactionInput) -> Any: ref, "SET_RULE", self._isGlobal - ) + ), + isDryrun ) - async def setValue(self, transactionInput: ValueOnlyTransactionInput) -> Any: + async def setValue(self, transactionInput: ValueOnlyTransactionInput, isDryrun = False) -> Any: """Sets the value. Args: transactionInput (ValueOnlyTransactionInput): The transaction input object. + isDryrun (bool): Dryrun option. Returns: The result of the transaction. @@ -260,14 +269,16 @@ async def setValue(self, transactionInput: ValueOnlyTransactionInput) -> Any: ref, "SET_VALUE", self._isGlobal - ) + ), + isDryrun ) - async def incrementValue(self, transactionInput: ValueOnlyTransactionInput) -> Any: + async def incrementValue(self, transactionInput: ValueOnlyTransactionInput, isDryrun = False) -> Any: """Increments the value. Args: transactionInput (ValueOnlyTransactionInput): The transaction input object. + isDryrun (bool): Dryrun option. Returns: The result of the transaction. @@ -279,14 +290,16 @@ async def incrementValue(self, transactionInput: ValueOnlyTransactionInput) -> A ref, "INC_VALUE", self._isGlobal - ) + ), + isDryrun ) - async def decrementValue(self, transactionInput: ValueOnlyTransactionInput) -> Any: + async def decrementValue(self, transactionInput: ValueOnlyTransactionInput, isDryrun = False) -> Any: """Decrements the value. Args: transactionInput (ValueOnlyTransactionInput): The transaction input object. + isDryrun (bool): Dryrun option. Returns: The result of the transaction. @@ -298,14 +311,16 @@ async def decrementValue(self, transactionInput: ValueOnlyTransactionInput) -> A ref, "DEC_VALUE", self._isGlobal - ) + ), + isDryrun ) - async def set(self, transactionInput: SetMultiTransactionInput) -> Any: + async def set(self, transactionInput: SetMultiTransactionInput, isDryrun = False) -> Any: """Processes the multiple set operations. Args: transactionInput (SetMultiTransactionInput): The transaction input object. + isDryrun (bool): Dryrun option. Returns: The result of the transaction. @@ -314,7 +329,8 @@ async def set(self, transactionInput: SetMultiTransactionInput) -> Any: Reference.extendSetMultiTransactionInput( transactionInput, self._path, - ) + ), + isDryrun ) async def evalRule(self, params: EvalRuleInput) -> Any: @@ -328,7 +344,7 @@ async def evalRule(self, params: EvalRuleInput) -> Any: `True`, if the params satisfy the write rule, `False,` if not. """ - address = self._ain.wallet.getImpliedAddress(getattr(params, "address", None)) + address = self._ain.signer.getAddress(getattr(params, "address", None)) req = { "address": address, "ref": Reference.extendPath(self._path, getattr(params, "ref", None)), @@ -347,7 +363,7 @@ async def evalOwner(self, params: EvalOwnerInput) -> Any: Returns: The owner evaluation result. """ - address = self._ain.wallet.getImpliedAddress(getattr(params, "address", None)) + address = self._ain.signer.getAddress(getattr(params, "address", None)) req = { "address": address, "ref": Reference.extendPath(self._path, getattr(params, "ref", None)), diff --git a/ain/signer/__init__.py b/ain/signer/__init__.py new file mode 100644 index 0000000..2c5f009 --- /dev/null +++ b/ain/signer/__init__.py @@ -0,0 +1,73 @@ +from abc import * +from typing import List, Any +from ain.types import TransactionInput, TransactionBody + +class Signer(metaclass = ABCMeta): + """An abstract class for signing messages and transactions. + """ + + @abstractmethod + def getAddress(self, address: str = None) -> str: + """Gets an account's checksum address. + If the address is not given, the default account of the signer is used. + + Args: + address (str, Optional): The address of the account. + + Returns: + str: The checksum address. + """ + pass + + @abstractmethod + async def signMessage(self, message: Any, address: str = None) -> str: + """Signs a message using an account. + If an address is not given, the default account of the signer is used. + + Args: + message (Any): The message to sign. + address (str, Optional): The address of the account. + + Returns: + str: The signature. + """ + pass + + @abstractmethod + async def sendTransaction(self, transactionObject: TransactionInput, isDryrun = False) -> Any: + """Signs and sends a transaction to the network. + + Args: + transactionObject (TransactionInput): The transaction input object. + isDryrun (bool): The dryrun option. + + Returns: + The return value of the blockchain API. + """ + pass + + @abstractmethod + async def sendTransactionBatch(self, transactionObjects: List[TransactionInput]) -> List[Any]: + """Signs and sends multiple transactions in a batch to the network. + + Args: + transactionObjects (List[TransactionInput]): The list of the transaction input objects. + + Returns: + The return value of the blockchain API. + """ + pass + + @abstractmethod + async def sendSignedTransaction(self, signature: str, txBody: TransactionBody, isDryrun = False) -> Any: + """Sends a signed transaction to the network. + + Args: + signature (str): The signature. + txBody (TransactionBody): The transaction body. + isDryrun (bool): The dryrun option. + + Returns: + The return value of the blockchain API. + """ + pass diff --git a/ain/signer/default_signer.py b/ain/signer/default_signer.py new file mode 100644 index 0000000..1515363 --- /dev/null +++ b/ain/signer/default_signer.py @@ -0,0 +1,170 @@ +from abc import * +import asyncio +from typing import List, Any +from ain.wallet import Wallet +from ain.provider import Provider +from ain.signer import Signer +from ain.types import TransactionInput, TransactionBody +from ain.utils import getTimestamp, toChecksumAddress + +class DefaultSigner(Signer): + """The default concrete class of Signer abstract class implemented using Wallet class. + When Ain class is initialized, DefaultSigner is set as its signer. + + Args: + wallet (Wallet): The wallet to initialize with. + provider (Provider): The network provider to initialize with. + """ + + wallet: Wallet + """The `Wallet` instance.""" + provider: Provider + """The `Provider` instance.""" + + def __init__(self, wallet: Wallet, provider: Provider): + self.wallet = wallet + self.provider = provider + + def getAddress(self, address: str = None) -> str: + """Gets an account's checksum address. + If the address is not given, the default account of the signer is used. + + Args: + address (str, Optional): The address of the account. + + Returns: + str: The checksum address. + """ + return self.wallet.getImpliedAddress(address) + + async def signMessage(self, message: Any, address: str = None) -> str: + """Signs a message using an account. + If an address is not given, the default account of the signer is used. + + Args: + message (Any): The message to sign. + address (str, Optional): The address of the account. + + Returns: + str: The signature. + """ + return self.wallet.sign(message, address) + + async def sendTransaction(self, transactionObject: TransactionInput, isDryrun = False) -> Any: + """Signs and sends a transaction to the network. + + Args: + transactionObject (TransactionInput): The transaction input object. + isDryrun (bool): The dryrun option. + + Returns: + The return value of the blockchain API. + """ + txBody = await self.buildTransactionBody(transactionObject) + signature = self.wallet.signTransaction(txBody, getattr(transactionObject, "address", None)) + return await self.sendSignedTransaction(signature, txBody, isDryrun) + + async def sendTransactionBatch(self, transactionObjects: List[TransactionInput]) -> List[Any]: + """Signs and sends multiple transactions in a batch to the network. + + Args: + transactionObjects (List[TransactionInput]): The list of the transaction input objects. + + Returns: + The return value of the blockchain API. + """ + txListCoroutines = [] + for txInput in transactionObjects: + txListCoroutines.append(self.buildTransactionBodyAndSignature(txInput)) + + txList = await asyncio.gather(*txListCoroutines) + return await self.provider.send("ain_sendSignedTransactionBatch", {"tx_list": txList}) + + async def sendSignedTransaction(self, signature: str, txBody: TransactionBody, isDryrun = False) -> Any: + """Sends a signed transaction to the network. + + Args: + signature (str): The signature. + txBody (TransactionBody): The transaction body. + isDryrun (bool): The dryrun option. + + Returns: + The return value of the blockchain API. + """ + method = "ain_sendSignedTransactionDryrun" if isDryrun == True else "ain_sendSignedTransaction" + return await self.provider.send(method, {"signature": signature, "tx_body": txBody}) + + async def buildTransactionBody(self, transactionInput: TransactionInput) -> TransactionBody: + """Builds a transaction body object from a transaction input object. + + Args: + transactionInput (TransactionInput): The transaction input object. + + Returns: + The TransactionBody object. + """ + address = self.getAddress( + getattr(transactionInput, "address", None) + ) + operation = transactionInput.operation + parent_tx_hash = getattr(transactionInput, "parent_tx_hash", None) + nonce = getattr(transactionInput, "nonce", None) + if nonce is None: + nonce = await self.getNonce({"address": address, "from": "pending"}) + timestamp = getattr(transactionInput, "timestamp", getTimestamp()) + gas_price = getattr(transactionInput, "gas_price", 0) + billing = getattr(transactionInput, "billing", None) + + return TransactionBody( + operation=operation, + parent_tx_hash=parent_tx_hash, + nonce=nonce, + timestamp=timestamp, + gas_price=gas_price, + billing=billing, + ) + + async def buildTransactionBodyAndSignature(self, transactionObject: TransactionInput) -> dict: + """Builds a transaction body object and a signature from a transaction input object. + + Args: + transactionInput (TransactionInput): The transaction input object. + + Returns: + The TransactionBody object and the signature. + """ + txBody = await self.buildTransactionBody(transactionObject) + if not hasattr(transactionObject, "nonce"): + # Batch transactions' nonces should be specified. + # If they're not, they default to un-nonced (nonce = -1). + txBody.nonce = -1 + + signature = self.wallet.signTransaction(txBody, getattr(transactionObject, "address", None)) + return {"signature": signature, "tx_body": txBody} + + async def getNonce(self, args: dict) -> Any: + """Fetches an account's nonce value, which is the current transaction count of the account. + + Args: + args (dict): The ferch options. + It may contain a string 'address' value and a string 'from' value. + The 'address' is the address of the account to get the nonce of, + and the 'from' is the source of the data. + It could be either the pending transaction pool ("pending") or + the committed blocks ("committed"). The default value is "committed". + + Returns: + The nonce value. + """ + params = dict(args) + if "address" in args: + params["address"] = toChecksumAddress(args["address"]) + else: + params["address"] = self.getAddress() + + if "from" in args: + if args["from"] != "pending" and args["from"] != "committed": + raise ValueError("'from' should be either 'pending' or 'committed'") + + return await self.provider.send("ain_getNonce", params) + \ No newline at end of file diff --git a/ain/utils/__init__.py b/ain/utils/__init__.py index 11b6287..526a008 100644 --- a/ain/utils/__init__.py +++ b/ain/utils/__init__.py @@ -1,5 +1,6 @@ import re -import json +import math +import simplejson import time from typing import Any, Union from secrets import token_bytes @@ -45,7 +46,8 @@ def encodeVarInt(number: int) -> bytes: SIGNED_MESSAGE_PREFIX = "AINetwork Signed Message:\n" SIGNED_MESSAGE_PREFIX_BYTES = bytes(SIGNED_MESSAGE_PREFIX, "utf-8") SIGNED_MESSAGE_PREFIX_LENGTH = encodeVarInt(len(SIGNED_MESSAGE_PREFIX)) -AIN_HD_DERIVATION_PATH = "m/44'/412'/0'/0/" +# TODO(platfowner): Migrate to Ethereum HD derivation path 'm/44'/60'/0'/0/'. +AIN_HD_DERIVATION_PATH = "m/44'/412'/0'/0/" # The hardware wallet derivation path of AIN def getTimestamp() -> int: """Gets the current unix timestamp. @@ -211,6 +213,70 @@ def keccak(input: Any, bits: int = 256) -> bytes: k.update(inputBytes) return k.digest() +def countDecimals(value: float) -> int: + """Counts the given number's decimals. + + Args: + value(float): The number. + + Returns: + int: The decimal count. + """ + valueString = str(value) + if math.floor(value) == value : + return 0 + p = re.compile(r'^-{0,1}(\d+\.{0,1}\d*)e-(\d+)$') + matches = p.findall(valueString) + if len(matches) > 0 and len(matches[0]) == 2: + return int(matches[0][1]) + countDecimals(float(matches[0][0])) + else : + parts = valueString.split('.') + if len(parts) >= 2 : + return len(parts[1]) + return 0 + +def replaceFloat(matchObj): + """Generates a replacement string for the given Match object. + + Args: + matchObj(Any): The Match object. + + Returns: + str: The replacement string. + """ + value = float(matchObj.group(0)) + decimals = countDecimals(value) + return (f'%.{decimals}f') % value + +# NOTE(platfowner): This resolves float decimal issues (see https://github.com/ainblockchain/ain-py/issues/31). +def toJsLikeFloats(serialized: str) -> str: + """Reformats float numbers to JavaScript-like formats. + + Args: + serialized(str): The string to be reformatted. + + Returns: + str: The reformatted string. + """ + return re.sub(r'(\d+\.{0,1}\d*e-0[56]{1,1})', replaceFloat, serialized) + +def toJsonString(obj: Any) -> str: + """Serializes the given object to a JSON string. + + Args: + obj (Any): The given object to serialize. + + Returns: + str: The result of the serialization. + """ + serialized = simplejson.dumps( + obj.__dict__, + default=lambda o: o.__dict__, + separators=(",", ":"), + sort_keys=True + ) + return toJsLikeFloats(serialized) + def hashTransaction(transaction: Union[TransactionBody, str]) -> bytes: """Creates the Keccak-256 hash of the transaction body. @@ -222,12 +288,7 @@ def hashTransaction(transaction: Union[TransactionBody, str]) -> bytes: bytes: The Keccak hash of the transaction. """ if type(transaction) is TransactionBody: - transaction = json.dumps( - transaction.__dict__, - default=lambda o: o.__dict__, - separators=(",", ":"), - sort_keys=True - ) + transaction = toJsonString(transaction) return keccak(keccak(transaction)) def hashMessage(message: Any) -> bytes: diff --git a/ain/wallet.py b/ain/wallet.py index 9e7ec65..8ad42bb 100644 --- a/ain/wallet.py +++ b/ain/wallet.py @@ -1,3 +1,5 @@ +import re +import math from typing import TYPE_CHECKING, Any, List, Optional, Union from ain.account import Account, Accounts from ain.types import ( @@ -9,6 +11,7 @@ privateToAddress, toBytes, toChecksumAddress, + countDecimals, bytesToHex, pubToAddress, ecSignMessage, @@ -21,6 +24,8 @@ if TYPE_CHECKING: from ain.ain import Ain +MAX_TRANSFERABLE_DECIMALS = 6; # The maximum decimals of transferable values + class Wallet: """Class for the AIN Blockchain wallet.""" @@ -224,24 +229,30 @@ async def getBalance(self, address: str = None) -> int: addr = toChecksumAddress(address) return await self.ain.db.ref(f"/accounts/{addr}/balance").getValue() - async def transfer(self, toAddress: str, value: int, fromAddress: str = None, nonce: int = None, gas_price: int = None): + async def transfer(self, toAddress: str, value: float, fromAddress: str = None, nonce: int = None, gas_price: int = None, isDryrun = False): """Sends a transfer transaction to the network. Args: toAddress (str): The AIN blockchain address that wants to transfer AIN to. - value (int): The amount of the transferring AIN. + value (float): The amount of the transferring AIN. fromAddress (str, Optional): The AIN blockchain address that wants to transfer AIN from. Defaults to `None`, transfer from the default account of the current wallet. nonce (int, Optional): The nonce of the transfer transaction. Defaults to `None`. gas_price (int, Optional): The gas price of the transfer transaction. Defaults to `None`. + isDryrun (bool): Dryrun option. Returns: The transaction result. """ fromAddr = self.getImpliedAddress(fromAddress) toAddr = toChecksumAddress(toAddress) + if not value > 0 : + raise ValueError('Non-positive transfer value.') + decimalCount = countDecimals(value) + if decimalCount > MAX_TRANSFERABLE_DECIMALS : + raise ValueError(f'Transfer value of more than {MAX_TRANSFERABLE_DECIMALS} decimals.') transferRef = self.ain.db.ref(f"/transfer/{fromAddr}/{toAddr}").push() return await transferRef.setValue( ValueOnlyTransactionInput( @@ -250,7 +261,8 @@ async def transfer(self, toAddress: str, value: int, fromAddress: str = None, no value=value, nonce=nonce, gas_price=gas_price, - ) + ), + isDryrun ) def sign(self, data: str, address: str = None) -> str: diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..8f2b9e3 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,3 @@ +[mypy] +no_implicit_optional = False + diff --git a/requirements.txt b/requirements.txt index ec2ffcf..8d425a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ aiohttp[speedups]==3.8.5 jsonrpcclient==4.0.3 mnemonic==0.20 bip32==3.4 +simplejson==3.19.2 snapshottest sphinx-rtd-theme readthedocs-sphinx-ext diff --git a/setup.py b/setup.py index da95595..cf7457e 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ], description="AI Network Client Library for Python3", install_requires=requirements, @@ -43,6 +44,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/ainblockchain/ain-py", - version="1.0.3", + version="1.1.0", zip_safe=False, ) diff --git a/tests/test_ain.py b/tests/test_ain.py index 1b10999..bf9db9f 100644 --- a/tests/test_ain.py +++ b/tests/test_ain.py @@ -4,6 +4,7 @@ from unittest import TestCase from snapshottest import TestCase as SnapshotTestCase from ain.ain import Ain +from ain.ain import Wallet from ain.provider import JSON_RPC_ENDPOINT from ain.utils import * from ain.utils.v3keystore import * @@ -149,6 +150,15 @@ async def testGetBalance(self): ain.wallet.addAndSetDefaultAccount(accountSk) self.assertGreaterEqual(await ain.wallet.getBalance(), 0) + @asyncTest + async def testTransferIsDryrunTrue(self): + ain = Ain(testNode) + ain.wallet.addAndSetDefaultAccount(accountSk) + balanceBefore = await ain.wallet.getBalance() + await ain.wallet.transfer(transferAddress, 100, nonce=-1, isDryrun=True) # isDryrun = True + balanceAfter = await ain.wallet.getBalance() + self.assertEqual(balanceBefore, balanceAfter) # NOT changed! + @asyncTest async def testTransfer(self): ain = Ain(testNode) @@ -158,6 +168,54 @@ async def testTransfer(self): balanceAfter = await ain.wallet.getBalance() self.assertEqual(balanceBefore - 100, balanceAfter) + @asyncTest + async def testTransferWithAZeroValue(self): + ain = Ain(testNode) + ain.wallet.addAndSetDefaultAccount(accountSk) + balanceBefore = await ain.wallet.getBalance() + try: + await ain.wallet.transfer(transferAddress, 0, nonce=-1) # zero value + self.fail('should not happen') + except ValueError as e: + self.assertEqual(str(e), 'Non-positive transfer value.') + balanceAfter = await ain.wallet.getBalance() + self.assertEqual(balanceBefore, balanceAfter) # NOT changed! + + @asyncTest + async def testTransferWithANegativeValue(self): + ain = Ain(testNode) + ain.wallet.addAndSetDefaultAccount(accountSk) + balanceBefore = await ain.wallet.getBalance() + try: + await ain.wallet.transfer(transferAddress, -0.1, nonce=-1) # negative value + self.fail('should not happen') + except ValueError as e: + self.assertEqual(str(e), 'Non-positive transfer value.') + balanceAfter = await ain.wallet.getBalance() + self.assertEqual(balanceBefore, balanceAfter) # NOT changed! + + @asyncTest + async def testTransferWithAValueOfUpTo6Decimals(self): + ain = Ain(testNode) + ain.wallet.addAndSetDefaultAccount(accountSk) + balanceBefore = await ain.wallet.getBalance() + await ain.wallet.transfer(transferAddress, 0.000001, nonce=-1) # of 6 decimals + balanceAfter = await ain.wallet.getBalance() + self.assertEqual(balanceBefore - 0.000001, balanceAfter) + + @asyncTest + async def testTransferWithAValueOfMoreThan6Decimals(self): + ain = Ain(testNode) + ain.wallet.addAndSetDefaultAccount(accountSk) + balanceBefore = await ain.wallet.getBalance() + try: + await ain.wallet.transfer(transferAddress, 0.0000001, nonce=-1) # of 7 decimals + self.fail('should not happen') + except ValueError as e: + self.assertEqual(str(e), 'Transfer value of more than 6 decimals.') + balanceAfter = await ain.wallet.getBalance() + self.assertEqual(balanceBefore, balanceAfter) # NOT changed! + def testChainId(self): ain = Ain(testNode, 0) ain.wallet.addAndSetDefaultAccount(accountSk) @@ -263,6 +321,32 @@ async def test00ValidateAppNameFalse(self): raised = True self.assertTrue(raised) + @asyncTest + async def test00SendTransactionIsDryrunTrue(self): + op = SetOperation( + type="SET_OWNER", + ref=f"/apps/test{PY_VERSION}", + value={ + ".owner": { + "owners": { + "*": { + "write_owner": True, + "write_rule": True, + "write_function": True, + "branch_owner": True + } + } + } + } + ) + res = await self.ain.sendTransaction(TransactionInput(operation=op), True) # isDryrun = True + self.assertEqual(res["result"]["code"], 0) + targetTxHash = res["tx_hash"] + self.assertTrue(TX_PATTERN.fullmatch(targetTxHash) is not None) + + tx = await self.ain.getTransaction(targetTxHash) + self.assertIsNone(tx) # should be None + @asyncTest async def test00SendTransaction(self): op = SetOperation( @@ -289,6 +373,40 @@ async def test00SendTransaction(self): tx = await self.ain.getTransaction(targetTxHash) self.assertDictEqual(tx["transaction"]["tx_body"]["operation"], op.__dict__) + @asyncTest + async def test00SendSignedTransactionIsDryrunTrue(self): + tx = TransactionBody( + nonce=-1, + gas_price=500, + timestamp=getTimestamp(), + operation=SetOperation( + type="SET_OWNER", + ref=f"/apps/{APP_NAME}{PY_VERSION}", + value={ + ".owner": { + "owners": { + "*": { + "write_owner": True, + "write_rule": True, + "write_function": True, + "branch_owner": True + } + } + } + } + ) + ) + + sig = self.ain.wallet.signTransaction(tx) + res = await self.ain.sendSignedTransaction(sig, tx, True) # isDryrun = True + targetTxHash = res["tx_hash"] + self.assertFalse("code" in res) + self.assertTrue(TX_PATTERN.fullmatch(targetTxHash) is not None) + self.assertEqual(res["result"]["code"], 0) + + tx = await self.ain.getTransaction(targetTxHash) + self.assertIsNone(tx) # should be None + @asyncTest async def test00SendSignedTransaction(self): tx = TransactionBody( @@ -488,6 +606,44 @@ def testRef(self): self.assertEqual(self.ain.db.ref().path, "/") self.assertEqual(self.ain.db.ref(self.allowedPath).path, "/" + self.allowedPath) + @asyncTest + async def test00SetOwnerIsDryrunTrue(self): + res = await self.ain.db.ref(self.allowedPath).setOwner(ValueOnlyTransactionInput( + value={ + ".owner": { + "owners": { + "*": { + "write_owner": True, + "write_rule": True, + "write_function": True, + "branch_owner": True + } + } + } + } + ), + True) + self.assertEqual(res["result"]["code"], 0) + self.assertEqual(res["result"]["is_dryrun"], True) + + fail = await self.ain.db.ref("/consensus").setOwner(ValueOnlyTransactionInput( + value={ + ".owner": { + "owners": { + "*": { + "write_owner": True, + "write_rule": True, + "write_function": True, + "branch_owner": True + } + } + } + } + ), + True) + self.assertEqual(fail["result"]["code"], 12501) + self.assertEqual(fail["result"]["is_dryrun"], True) + @asyncTest async def test00SetOwner(self): res = await self.ain.db.ref(self.allowedPath).setOwner(ValueOnlyTransactionInput( @@ -522,6 +678,24 @@ async def test00SetOwner(self): )) self.assertEqual(fail["result"]["code"], 12501) + @asyncTest + async def test00SetRuleIsDryrunTrue(self): + res = await self.ain.db.ref(self.allowedPath).setRule(ValueOnlyTransactionInput( + value={ ".rule": { "write": "true" } } + ), + True) + self.assertEqual(res["result"]["code"], 0) + self.assertEqual(res["result"]["is_dryrun"], True) + + @asyncTest + async def test00SetRuleIsDryrunTrue(self): + res = await self.ain.db.ref(self.allowedPath).setRule(ValueOnlyTransactionInput( + value={ ".rule": { "write": "true" } } + ), + True) + self.assertEqual(res["result"]["code"], 0) + self.assertEqual(res["result"]["is_dryrun"], True) + @asyncTest async def test00SetRule(self): res = await self.ain.db.ref(self.allowedPath).setRule(ValueOnlyTransactionInput( @@ -529,6 +703,15 @@ async def test00SetRule(self): )) self.assertEqual(res["result"]["code"], 0) + @asyncTest + async def test00SetValueIsDryrunTrue(self): + res = await self.ain.db.ref(f"{self.allowedPath}/username").setValue(ValueOnlyTransactionInput( + value="test_user" + ), + True) + self.assertEqual(res["result"]["code"], 0) + self.assertEqual(res["result"]["is_dryrun"], True) + @asyncTest async def test00SetValue(self): res = await self.ain.db.ref(f"{self.allowedPath}/username").setValue(ValueOnlyTransactionInput( @@ -536,6 +719,23 @@ async def test00SetValue(self): )) self.assertEqual(res["result"]["code"], 0) + @asyncTest + async def test00SetFunctionIsDryrunTrue(self): + res = await self.ain.db.ref(self.allowedPath).setFunction(ValueOnlyTransactionInput( + value={ + ".function": { + "0xFUNCTION_HASH": { + "function_url": "https://events.ainetwork.ai/trigger", + "function_id": "0xFUNCTION_HASH", + "function_type": "REST" + } + } + } + ), + True) + self.assertEqual(res["result"]["code"], 0) + self.assertEqual(res["result"]["is_dryrun"], True) + @asyncTest async def test00SetFunction(self): res = await self.ain.db.ref(self.allowedPath).setFunction(ValueOnlyTransactionInput( @@ -551,6 +751,37 @@ async def test00SetFunction(self): )) self.assertEqual(res["result"]["code"], 0) + @asyncTest + async def test00SetIsDryrunTrue(self): + res = await self.ain.db.ref(self.allowedPath).set(SetMultiTransactionInput( + op_list=[ + SetOperation( + type="SET_RULE", + ref="can/write/", + value={ ".rule": { "write": "true" } } + ), + SetOperation( + type="SET_RULE", + ref="cannot/write", + value={ ".rule": { "write": "false" } } + ), + SetOperation( + type="INC_VALUE", + ref="can/write/", + value=5 + ), + SetOperation( + type="DEC_VALUE", + ref="can/write", + value=10 + ) + ], + nonce=-1 + ), + True) + self.assertEqual(len(res["result"]["result_list"].keys()), 4) + self.assertEqual(res["result"]["is_dryrun"], True) + @asyncTest async def test00Set(self): res = await self.ain.db.ref(self.allowedPath).set(SetMultiTransactionInput( @@ -633,6 +864,12 @@ async def test01GetWithOptions(self): getWithVersion = await self.ain.db.ref().getValue(self.allowedPath, GetOptions(include_version=True)) self.assertTrue("#version" in getWithVersion) + @asyncTest + async def test02DeleteValueIsDryrunTrue(self): + res = await self.ain.db.ref(f"{self.allowedPath}/can/write").deleteValue(isDryrun=True) + self.assertEqual(res["result"]["code"], 0) + self.assertEqual(res["result"]["is_dryrun"], True) + @asyncTest async def test02DeleteValue(self): res = await self.ain.db.ref(f"{self.allowedPath}/can/write").deleteValue() diff --git a/tests/test_ain_utils.py b/tests/test_ain_utils.py index 09b43e3..49a9607 100644 --- a/tests/test_ain_utils.py +++ b/tests/test_ain_utils.py @@ -1,6 +1,7 @@ from unittest import TestCase from ain.utils import * from ain.utils.v3keystore import * +from ain.types import SetOperation, TransactionBody from .data import ( address, pk, @@ -36,6 +37,248 @@ def testKeccakWithoutHexprefix(self): hash = keccak(msg) self.assertEqual(hash.hex(), r) +class TestCountDecimals(TestCase): + def testCountDecimals(self): + self.assertEqual(countDecimals(0), 0) # '0' + self.assertEqual(countDecimals(1), 0) # '1' + self.assertEqual(countDecimals(10), 0) # '10' + self.assertEqual(countDecimals(100), 0) # '100' + self.assertEqual(countDecimals(1000), 0) # '1000' + self.assertEqual(countDecimals(10000), 0) # '10000' + self.assertEqual(countDecimals(100000), 0) # '100000' + self.assertEqual(countDecimals(1000000), 0) # '1000000' + self.assertEqual(countDecimals(10000000), 0) # '10000000' + self.assertEqual(countDecimals(100000000), 0) # '100000000' + self.assertEqual(countDecimals(1000000000), 0) # '1000000000' + self.assertEqual(countDecimals(1234567890), 0) # '1234567890' + self.assertEqual(countDecimals(-1), 0) # '-1' + self.assertEqual(countDecimals(-1000000000), 0) # '-1000000000' + self.assertEqual(countDecimals(11), 0) # '11' + self.assertEqual(countDecimals(101), 0) # '101' + self.assertEqual(countDecimals(1001), 0) # '1001' + self.assertEqual(countDecimals(10001), 0) # '10001' + self.assertEqual(countDecimals(100001), 0) # '100001' + self.assertEqual(countDecimals(1000001), 0) # '1000001' + self.assertEqual(countDecimals(10000001), 0) # '10000001' + self.assertEqual(countDecimals(100000001), 0) # '100000001' + self.assertEqual(countDecimals(1000000001), 0) # '1000000001' + self.assertEqual(countDecimals(-11), 0) # '-11' + self.assertEqual(countDecimals(-1000000001), 0) # '-1000000001' + self.assertEqual(countDecimals(0.1), 1) # '0.1' + self.assertEqual(countDecimals(0.01), 2) # '0.01' + self.assertEqual(countDecimals(0.001), 3) # '0.001' + self.assertEqual(countDecimals(0.0001), 4) # '0.0001' + self.assertEqual(countDecimals(0.00001), 5) # '1e-05' + self.assertEqual(countDecimals(0.000001), 6) # '1e-06' + self.assertEqual(countDecimals(0.0000001), 7) # '1e-07' + self.assertEqual(countDecimals(0.00000001), 8) # '1e-08' + self.assertEqual(countDecimals(0.000000001), 9) # '1e-09' + self.assertEqual(countDecimals(0.0000000001), 10) # '1e-10' + self.assertEqual(countDecimals(-0.1), 1) # '-0.1' + self.assertEqual(countDecimals(-0.0000000001), 10) # '-1e-10' + self.assertEqual(countDecimals(1.2), 1) # '1.2' + self.assertEqual(countDecimals(0.12), 2) # '0.12' + self.assertEqual(countDecimals(0.012), 3) # '0.012' + self.assertEqual(countDecimals(0.0012), 4) # '0.0012' + self.assertEqual(countDecimals(0.00012), 5) # '0.00012' + self.assertEqual(countDecimals(0.000012), 6) # '1.2e-05' + self.assertEqual(countDecimals(0.0000012), 7) # '1.2e-06' + self.assertEqual(countDecimals(0.00000012), 8) # '1.2e-07' + self.assertEqual(countDecimals(0.000000012), 9) # '1.2e-08' + self.assertEqual(countDecimals(0.0000000012), 10) # '1.2e-09' + self.assertEqual(countDecimals(0.00000000012), 11) # '1.2e-10' + self.assertEqual(countDecimals(-1.2), 1) # '-1.2' + self.assertEqual(countDecimals(-0.00000000012), 11) # '-1.2e-10' + self.assertEqual(countDecimals(1.03), 2) # '1.03' + self.assertEqual(countDecimals(1.003), 3) # '1.003' + self.assertEqual(countDecimals(1.0003), 4) # '1.0003' + self.assertEqual(countDecimals(1.00003), 5) # '1.00003' + self.assertEqual(countDecimals(1.000003), 6) # '1.000003' + self.assertEqual(countDecimals(1.0000003), 7) # '1.0000003' + self.assertEqual(countDecimals(1.00000003), 8) # '1.00000003' + self.assertEqual(countDecimals(1.000000003), 9) # '1.000000003' + self.assertEqual(countDecimals(1.0000000003), 10) # '1.0000000003' + self.assertEqual(countDecimals(-1.03), 2) # '-1.03' + self.assertEqual(countDecimals(-1.0000000003), 10) # '-1.0000000003' + +class TestToJsLikeFloats(TestCase): + def testToJsLikeFloats(self): + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":0.0001},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":0.0001},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":1e-05},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":0.00001},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":1e-06},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":0.000001},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":1e-07},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":1e-07},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":0.00012},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":0.00012},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":1.2e-05},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":0.000012},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":1.2e-06},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":0.0000012},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":1.2e-07},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":1.2e-07},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-0.0001},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-0.0001},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-1e-05},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-0.00001},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-1e-06},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-0.000001},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-1e-07},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-1e-07},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-0.00012},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-0.00012},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-1.2e-05},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-0.000012},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-1.2e-06},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-0.0000012},"timestamp":123}') + self.assertEqual( + toJsLikeFloats('{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-1.2e-07},"timestamp":123}'), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-1.2e-07},"timestamp":123}') + +class TestToJsonString(TestCase): + def testToJsonString(self): + txBody = TransactionBody( + operation=SetOperation( + ref="/afan", + value=100, + type="SET_VALUE", + ), + nonce=10, + timestamp=123, + ) + self.assertEqual( + toJsonString(txBody), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":100},"timestamp":123}') + + def testToJsonStringWithFloatValueWithoutExponent(self): + txBody = TransactionBody( + operation=SetOperation( + ref="/afan", + value=1.000003, # '1.000003' + type="SET_VALUE", + ), + nonce=10, + timestamp=123, + ) + self.assertEqual( + toJsonString(txBody), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":1.000003},"timestamp":123}') + + def testToJsonStringWithFloatValueWithExponent4(self): + txBody = TransactionBody( + operation=SetOperation( + ref="/afan", + value=0.00012, # '0.00012' + type="SET_VALUE", + ), + nonce=10, + timestamp=123, + ) + self.assertEqual( + toJsonString(txBody), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":0.00012},"timestamp":123}') # 0.00012 + + def testToJsonStringWithFloatValueWithExponent5(self): + txBody = TransactionBody( + operation=SetOperation( + ref="/afan", + value=0.000012, # '1.2e-05' + type="SET_VALUE", + ), + nonce=10, + timestamp=123, + ) + self.assertEqual( + toJsonString(txBody), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":0.000012},"timestamp":123}') # 0.000012 + + def testToJsonStringWithFloatValueWithExponent6(self): + txBody = TransactionBody( + operation=SetOperation( + ref="/afan", + value=0.0000012, # '1.2e-06' + type="SET_VALUE", + ), + nonce=10, + timestamp=123, + ) + self.assertEqual( + toJsonString(txBody), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":0.0000012},"timestamp":123}') # 0.0000012 + + def testToJsonStringWithFloatValueWithExponent7(self): + txBody = TransactionBody( + operation=SetOperation( + ref="/afan", + value=0.00000012, # '1.2e-07' + type="SET_VALUE", + ), + nonce=10, + timestamp=123, + ) + self.assertEqual( + toJsonString(txBody), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":1.2e-07},"timestamp":123}') # 1.2e-07 + + def testToJsonStringWithNegativeFloatValueWithExponent5(self): + txBody = TransactionBody( + operation=SetOperation( + ref="/afan", + value=-0.000012, # '-1.2e-05' + type="SET_VALUE", + ), + nonce=10, + timestamp=123, + ) + self.assertEqual( + toJsonString(txBody), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-0.000012},"timestamp":123}') # -0.000012 + + def testToJsonStringWithNegativeFloatValueWithExponent6(self): + txBody = TransactionBody( + operation=SetOperation( + ref="/afan", + value=-0.0000012, # '-1.2e-06' + type="SET_VALUE", + ), + nonce=10, + timestamp=123, + ) + self.assertEqual( + toJsonString(txBody), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-0.0000012},"timestamp":123}') # -0.0000012 + + def testToJsonStringWithNegativeFloatValueWithExponent7(self): + txBody = TransactionBody( + operation=SetOperation( + ref="/afan", + value=-0.00000012, # '-1.2e-07' + type="SET_VALUE", + ), + nonce=10, + timestamp=123, + ) + self.assertEqual( + toJsonString(txBody), + '{"nonce":10,"operation":{"ref":"/afan","type":"SET_VALUE","value":-1.2e-07},"timestamp":123}') # -1.2e-07 + class TestByteToHex(TestCase): def testByteToHex(self): byt = bytes.fromhex("5b9ac8") diff --git a/tox.ini b/tox.ini index 1bfd30c..0ba1a46 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310 +envlist = py37,py38,py39,py310,py311 skipsdist = True [testenv]