From 0b6a569557e8122d8f54adcc592804a339154651 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sat, 19 Feb 2022 23:51:44 -0500 Subject: [PATCH 01/36] incorporate split change logic --- pycardano/txbuilder.py | 111 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 9 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 83d94515..9ffa08e8 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -9,13 +9,15 @@ RandomImproveMultiAsset, UTxOSelector, ) -from pycardano.exception import InvalidTransactionException, UTxOSelectionException -from pycardano.hash import VerificationKeyHash +from pycardano.exception import InvalidTransactionException, UTxOSelectionException, InsufficientUTxOBalanceException +from pycardano.hash import VerificationKeyHash, ScriptHash from pycardano.key import VerificationKey from pycardano.logging import logger from pycardano.metadata import AuxiliaryData from pycardano.nativescript import NativeScript, ScriptAll, ScriptAny, ScriptPubkey from pycardano.transaction import ( + AssetName, + Asset, MultiAsset, Transaction, TransactionBody, @@ -23,7 +25,7 @@ UTxO, Value, ) -from pycardano.utils import fee, max_tx_fee +from pycardano.utils import fee, max_tx_fee, min_lovelace from pycardano.witness import TransactionWitnessSet, VerificationKeyWitness __all__ = ["TransactionBuilder"] @@ -153,19 +155,33 @@ def _calc_change(self, fees, inputs, outputs, address) -> List[TransactionOutput provided.multi_asset += self.mint change = provided - requested + if change.coin < 0: + # We assign max fee for now to ensure enough balance regardless of splits condition + # We can implement a more precise fee logic and requirements later + raise InsufficientUTxOBalanceException("Not enough ADA to cover fees") + change_output_arr = [] - # Remove any asset that has 0 quantity + # If there are multi asset in the change if change.multi_asset: + # Remove any asset that has 0 quantity change.multi_asset = change.multi_asset.filter(lambda p, n, v: v > 0) - # If we end up with no multi asset, simply use coin value as change + # Split assets if size exceeds limits + multi_asset_arr = self._pack_tokens_for_change(address, change, self.context.protocol_param.max_val_size) + for multi_asset in multi_asset_arr: + change_value = Value(0, multi_asset) + required_min_lovelace = min_lovelace(change_value, self.context) + change_value.coin = required_min_lovelace + change_output = TransactionOutput(address, change_value) + change -= change_value + change_output_arr.append(change_output) + + # when there is only ADA left, simply use remaining coin value as change if not change.multi_asset: change = change.coin + change_output_arr.append(TransactionOutput(address, change)) - # TODO: Split change if the bundle size exceeds the max utxo size. - # Currently, there is only one change (UTxO) being returned. This is a native solution, it will fail - # when there are too many native tokens attached to the change. - return [TransactionOutput(address, change)] + return change_output_arr def _add_change_and_fee( self, change_address: Optional[Address] @@ -190,6 +206,83 @@ def _add_change_and_fee( return self + def _adding_asset_make_output_overflow(self, output: TransactionOutput, current_assets: Asset, policy_id: ScriptHash, add_asset_name: AssetName, add_asset_val: int, max_val_size: int) -> bool: + """ Check if adding the asset will make output exceed maximum size limit + + Args: + output (TransactionOutput): current output + current_assets (Asset): current Assets to be included in output + policy_id (ScriptHash): policy id containing the MultiAsset + asset_to_add (Asset): Asset to add to current MultiAsset to check size limit + + """ + attempt_assets = current_assets.copy() + attempt_assets += Asset({add_asset_name: add_asset_val}) + attempt_multi_asset = MultiAsset({policy_id, attempt_assets}) + + new_amount = Value(0, attempt_multi_asset) + current_amount = output.amount.copy() + attempt_amount = new_amount + current_amount + + # Calculate minimum ada requirements for more precise value size + required_lovelace = min_lovelace(attempt_amount, self.context) + attempt_amount.coin = required_lovelace + + return len(attempt_amount.to_cbor("bytes")) > max_val_size + + def _pack_tokens_for_change(self, change_address: Optional[Address], change_estimator: Value, max_val_size: int) -> List[MultiAsset]: + multi_asset_arr = [] + base_coin = Value(coin=change_estimator.coin) + output = TransactionOutput(change_address, base_coin) + + # iteratively add tokens to output + for (policy_id, assets) in change_estimator.multi_asset.items(): + temp_multi_asset = MultiAsset() + temp_value = Value(coin=0) + temp_assets = Asset() + old_amount = output.amount.copy() + for asset_name, asset_value in assets.items(): + if self._adding_asset_make_output_overflow(output, temp_assets, policy_id, asset_name, asset_value, max_val_size): + # Insert current assets as one group + temp_multi_asset += MultiAsset({policy_id: temp_multi_asset}) + temp_value.multi_asset = temp_multi_asset + output.amount += temp_value + multi_asset_arr.append(output.amount.multi_asset) + + # Create a new output + base_coin = Value(coin=0) + output = TransactionOutput(change_address, base_coin) + + # Continue building output from where we stopped + old_amount = output.amount.copy() + temp_multi_asset = MultiAsset() + temp_value = Value() + temp_assets = Asset() + + temp_assets += Asset({asset_name: asset_value}) + + # Assess assets in buffer + temp_multi_asset += MultiAsset({policy_id: temp_assets}) + temp_value.multi_asset = temp_multi_asset + output.amount += temp_value + + # Calculate min lovelace required for more precise size + updated_amount = output.amount.copy() + required_lovelace = min_lovelace(updated_amount, self.context) + updated_amount.coin = required_lovelace + + # TODO: discuss this portion + if len(updated_amount.to_cbor("bytes")) > max_val_size: + output.amount = old_amount + break + + multi_asset_arr.append(output.amount.multi_asset) + + return multi_asset_arr + + def _fees_for_output(self): + return + def _input_vkey_hashes(self) -> Set[VerificationKeyHash]: results = set() for i in self.inputs: From 182f780c8bf1929c2aeac3a8a9bcea5ffbc75d9f Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sun, 20 Feb 2022 17:10:22 -0500 Subject: [PATCH 02/36] incorporate split change logic --- pycardano/txbuilder.py | 50 +++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 9ffa08e8..c5846447 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import List, Optional, Set, Union +from copy import deepcopy from pycardano.address import Address from pycardano.backend.base import ChainContext @@ -161,6 +162,11 @@ def _calc_change(self, fees, inputs, outputs, address) -> List[TransactionOutput raise InsufficientUTxOBalanceException("Not enough ADA to cover fees") change_output_arr = [] + # when there is only ADA left, simply use remaining coin value as change + if not change.multi_asset: + change = change.coin + change_output_arr.append(TransactionOutput(address, change)) + # If there are multi asset in the change if change.multi_asset: # Remove any asset that has 0 quantity @@ -168,20 +174,35 @@ def _calc_change(self, fees, inputs, outputs, address) -> List[TransactionOutput # Split assets if size exceeds limits multi_asset_arr = self._pack_tokens_for_change(address, change, self.context.protocol_param.max_val_size) - for multi_asset in multi_asset_arr: + + # Include minimum lovelace into each token output except for the last one + for multi_asset in multi_asset_arr[:-1]: change_value = Value(0, multi_asset) - required_min_lovelace = min_lovelace(change_value, self.context) - change_value.coin = required_min_lovelace - change_output = TransactionOutput(address, change_value) + change_value.coin = min_lovelace(change_value, self.context) + change_output_arr.append(TransactionOutput(address, change_value)) change -= change_value - change_output_arr.append(change_output) + change.multi_asset = change.multi_asset.filter(lambda p, n, v: v > 0) - # when there is only ADA left, simply use remaining coin value as change - if not change.multi_asset: - change = change.coin - change_output_arr.append(TransactionOutput(address, change)) + # Combine remainder of provided ADA with last MultiAsset for output + # There may be rare cases where adding ADA causes size exceeds limit + # We will revisit if it becomes an issue + last_multi_asset = multi_asset_arr[-1] + change_value = Value(change.coin, last_multi_asset) + change_output_arr.append(TransactionOutput(address, change_value)) return change_output_arr + # Remove any asset that has 0 quantity + # if change.multi_asset: + # change.multi_asset = change.multi_asset.filter(lambda p, n, v: v > 0) + # + # # If we end up with no multi asset, simply use coin value as change + # if not change.multi_asset: + # change = change.coin + # + # # TODO: Split change if the bundle size exceeds the max utxo size. + # # Currently, there is only one change (UTxO) being returned. This is a native solution, it will fail + # # when there are too many native tokens attached to the change. + # return [TransactionOutput(address, change)] def _add_change_and_fee( self, change_address: Optional[Address] @@ -216,12 +237,12 @@ def _adding_asset_make_output_overflow(self, output: TransactionOutput, current_ asset_to_add (Asset): Asset to add to current MultiAsset to check size limit """ - attempt_assets = current_assets.copy() + attempt_assets = deepcopy(current_assets) attempt_assets += Asset({add_asset_name: add_asset_val}) - attempt_multi_asset = MultiAsset({policy_id, attempt_assets}) + attempt_multi_asset = MultiAsset({policy_id: attempt_assets}) new_amount = Value(0, attempt_multi_asset) - current_amount = output.amount.copy() + current_amount = deepcopy(output.amount) attempt_amount = new_amount + current_amount # Calculate minimum ada requirements for more precise value size @@ -240,7 +261,7 @@ def _pack_tokens_for_change(self, change_address: Optional[Address], change_esti temp_multi_asset = MultiAsset() temp_value = Value(coin=0) temp_assets = Asset() - old_amount = output.amount.copy() + old_amount = deepcopy(output.amount) for asset_name, asset_value in assets.items(): if self._adding_asset_make_output_overflow(output, temp_assets, policy_id, asset_name, asset_value, max_val_size): # Insert current assets as one group @@ -267,11 +288,10 @@ def _pack_tokens_for_change(self, change_address: Optional[Address], change_esti output.amount += temp_value # Calculate min lovelace required for more precise size - updated_amount = output.amount.copy() + updated_amount = deepcopy(output.amount) required_lovelace = min_lovelace(updated_amount, self.context) updated_amount.coin = required_lovelace - # TODO: discuss this portion if len(updated_amount.to_cbor("bytes")) > max_val_size: output.amount = old_amount break From 7fdb3344a57da07ee24a62d36fc67bc073ffb542 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sun, 20 Feb 2022 17:12:36 -0500 Subject: [PATCH 03/36] incorporate split change logic --- pycardano/txbuilder.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index c5846447..7f1ae517 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -181,6 +181,7 @@ def _calc_change(self, fees, inputs, outputs, address) -> List[TransactionOutput change_value.coin = min_lovelace(change_value, self.context) change_output_arr.append(TransactionOutput(address, change_value)) change -= change_value + # Remove assets with 0 quantity change.multi_asset = change.multi_asset.filter(lambda p, n, v: v > 0) # Combine remainder of provided ADA with last MultiAsset for output @@ -191,18 +192,6 @@ def _calc_change(self, fees, inputs, outputs, address) -> List[TransactionOutput change_output_arr.append(TransactionOutput(address, change_value)) return change_output_arr - # Remove any asset that has 0 quantity - # if change.multi_asset: - # change.multi_asset = change.multi_asset.filter(lambda p, n, v: v > 0) - # - # # If we end up with no multi asset, simply use coin value as change - # if not change.multi_asset: - # change = change.coin - # - # # TODO: Split change if the bundle size exceeds the max utxo size. - # # Currently, there is only one change (UTxO) being returned. This is a native solution, it will fail - # # when there are too many native tokens attached to the change. - # return [TransactionOutput(address, change)] def _add_change_and_fee( self, change_address: Optional[Address] From 3c8ced2eaa6011287577e6725875b72bd5d78638 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sun, 20 Feb 2022 17:36:29 -0500 Subject: [PATCH 04/36] minor modification to change combine logic --- pycardano/txbuilder.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 7f1ae517..26f80035 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -176,25 +176,24 @@ def _calc_change(self, fees, inputs, outputs, address) -> List[TransactionOutput multi_asset_arr = self._pack_tokens_for_change(address, change, self.context.protocol_param.max_val_size) # Include minimum lovelace into each token output except for the last one - for multi_asset in multi_asset_arr[:-1]: - change_value = Value(0, multi_asset) - change_value.coin = min_lovelace(change_value, self.context) + for i, multi_asset in enumerate(multi_asset_arr): + # Combine remainder of provided ADA with last MultiAsset for output + # There may be rare cases where adding ADA causes size exceeds limit + # We will revisit if it becomes an issue + if i == len(multi_asset_arr) - 1: + change_value = Value(change.coin, multi_asset) + else: + change_value = Value(0, multi_asset) + change_value.coin = min_lovelace(change_value, self.context) change_output_arr.append(TransactionOutput(address, change_value)) change -= change_value # Remove assets with 0 quantity change.multi_asset = change.multi_asset.filter(lambda p, n, v: v > 0) - # Combine remainder of provided ADA with last MultiAsset for output - # There may be rare cases where adding ADA causes size exceeds limit - # We will revisit if it becomes an issue - last_multi_asset = multi_asset_arr[-1] - change_value = Value(change.coin, last_multi_asset) - change_output_arr.append(TransactionOutput(address, change_value)) - return change_output_arr def _add_change_and_fee( - self, change_address: Optional[Address] + self, change_address: Optional[Address] ) -> TransactionBuilder: original_outputs = self.outputs[:] if change_address: @@ -216,7 +215,9 @@ def _add_change_and_fee( return self - def _adding_asset_make_output_overflow(self, output: TransactionOutput, current_assets: Asset, policy_id: ScriptHash, add_asset_name: AssetName, add_asset_val: int, max_val_size: int) -> bool: + def _adding_asset_make_output_overflow(self, output: TransactionOutput, current_assets: Asset, + policy_id: ScriptHash, add_asset_name: AssetName, add_asset_val: int, + max_val_size: int) -> bool: """ Check if adding the asset will make output exceed maximum size limit Args: @@ -240,7 +241,8 @@ def _adding_asset_make_output_overflow(self, output: TransactionOutput, current_ return len(attempt_amount.to_cbor("bytes")) > max_val_size - def _pack_tokens_for_change(self, change_address: Optional[Address], change_estimator: Value, max_val_size: int) -> List[MultiAsset]: + def _pack_tokens_for_change(self, change_address: Optional[Address], change_estimator: Value, max_val_size: int) -> \ + List[MultiAsset]: multi_asset_arr = [] base_coin = Value(coin=change_estimator.coin) output = TransactionOutput(change_address, base_coin) @@ -252,7 +254,8 @@ def _pack_tokens_for_change(self, change_address: Optional[Address], change_esti temp_assets = Asset() old_amount = deepcopy(output.amount) for asset_name, asset_value in assets.items(): - if self._adding_asset_make_output_overflow(output, temp_assets, policy_id, asset_name, asset_value, max_val_size): + if self._adding_asset_make_output_overflow(output, temp_assets, policy_id, asset_name, asset_value, + max_val_size): # Insert current assets as one group temp_multi_asset += MultiAsset({policy_id: temp_multi_asset}) temp_value.multi_asset = temp_multi_asset From c2772e80f2af28521d22ccf78603ef8b771de524 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sun, 20 Feb 2022 17:40:13 -0500 Subject: [PATCH 05/36] minor modification to change combine logic --- pycardano/txbuilder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 26f80035..6787a31a 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -164,8 +164,8 @@ def _calc_change(self, fees, inputs, outputs, address) -> List[TransactionOutput # when there is only ADA left, simply use remaining coin value as change if not change.multi_asset: - change = change.coin - change_output_arr.append(TransactionOutput(address, change)) + lovelace_change = change.coin + change_output_arr.append(TransactionOutput(address, lovelace_change)) # If there are multi asset in the change if change.multi_asset: From d303a02ff23db8bff2c9964880932db68f8e4480 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sun, 20 Feb 2022 17:44:54 -0500 Subject: [PATCH 06/36] minor modification to change combine logic --- pycardano/txbuilder.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 6787a31a..935cfe02 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -31,7 +31,6 @@ __all__ = ["TransactionBuilder"] - FAKE_VKEY = VerificationKey.from_primitive( bytes.fromhex( "58205e750db9facf42b15594790e3ac882e" "d5254eb214a744353a2e24e4e65b8ceb4" @@ -54,7 +53,7 @@ class TransactionBuilder: """ def __init__( - self, context: ChainContext, utxo_selectors: Optional[List[UTxOSelector]] = None + self, context: ChainContext, utxo_selectors: Optional[List[UTxOSelector]] = None ): self.context = context self._inputs = [] @@ -242,7 +241,7 @@ def _adding_asset_make_output_overflow(self, output: TransactionOutput, current_ return len(attempt_amount.to_cbor("bytes")) > max_val_size def _pack_tokens_for_change(self, change_address: Optional[Address], change_estimator: Value, max_val_size: int) -> \ - List[MultiAsset]: + List[MultiAsset]: multi_asset_arr = [] base_coin = Value(coin=change_estimator.coin) output = TransactionOutput(change_address, base_coin) @@ -387,7 +386,7 @@ def build(self, change_address: Optional[Address] = None) -> TransactionBody: selected_amount.coin, selected_amount.multi_asset.filter( lambda p, n, v: p in requested_amount.multi_asset - and n in requested_amount.multi_asset[p] + and n in requested_amount.multi_asset[p] ), ) From e8f37fa9f36cc90339b82e431c8cbab0a24a3238 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Mon, 7 Mar 2022 15:29:46 -0500 Subject: [PATCH 07/36] integrate diff changes --- pycardano/serialization.py | 7 +++++ pycardano/txbuilder.py | 62 ++++++++++++++++++++++++++------------ 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/pycardano/serialization.py b/pycardano/serialization.py index 69008e4f..2142ab74 100644 --- a/pycardano/serialization.py +++ b/pycardano/serialization.py @@ -4,6 +4,7 @@ import re from collections import OrderedDict, defaultdict +from copy import deepcopy from dataclasses import Field, dataclass, fields from datetime import datetime from decimal import Decimal @@ -628,6 +629,12 @@ def __delitem__(self, key): def __repr__(self): return self.data.__repr__() + def __copy__(self): + return self.__class__(self) + + def __deepcopy__(self, memodict={}): + return self.__class__(deepcopy(self.data)) + def to_shallow_primitive(self) -> dict: # Sort keys in a map according to https://datatracker.ietf.org/doc/html/rfc7049#section-3.9 def _get_sortable_val(key): diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 716319a2..2511db05 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -1,7 +1,7 @@ from __future__ import annotations -from typing import List, Optional, Set, Union from copy import deepcopy +from typing import List, Optional, Set, Union from pycardano.address import Address from pycardano.backend.base import ChainContext @@ -10,15 +10,19 @@ RandomImproveMultiAsset, UTxOSelector, ) -from pycardano.exception import InvalidTransactionException, UTxOSelectionException, InsufficientUTxOBalanceException -from pycardano.hash import VerificationKeyHash, ScriptHash +from pycardano.exception import ( + InsufficientUTxOBalanceException, + InvalidTransactionException, + UTxOSelectionException, +) +from pycardano.hash import ScriptHash, VerificationKeyHash from pycardano.key import VerificationKey from pycardano.logging import logger from pycardano.metadata import AuxiliaryData from pycardano.nativescript import NativeScript, ScriptAll, ScriptAny, ScriptPubkey from pycardano.transaction import ( - AssetName, Asset, + AssetName, MultiAsset, Transaction, TransactionBody, @@ -53,7 +57,7 @@ class TransactionBuilder: """ def __init__( - self, context: ChainContext, utxo_selectors: Optional[List[UTxOSelector]] = None + self, context: ChainContext, utxo_selectors: Optional[List[UTxOSelector]] = None ): self.context = context self._inputs = [] @@ -168,6 +172,11 @@ def _calc_change(self, fees, inputs, outputs, address) -> List[TransactionOutput # We assign max fee for now to ensure enough balance regardless of splits condition # We can implement a more precise fee logic and requirements later raise InsufficientUTxOBalanceException("Not enough ADA to cover fees") + + # Remove any asset that has 0 quantity + if change.multi_asset: + change.multi_asset = change.multi_asset.filter(lambda p, n, v: v > 0) + change_output_arr = [] # when there is only ADA left, simply use remaining coin value as change @@ -177,11 +186,10 @@ def _calc_change(self, fees, inputs, outputs, address) -> List[TransactionOutput # If there are multi asset in the change if change.multi_asset: - # Remove any asset that has 0 quantity - change.multi_asset = change.multi_asset.filter(lambda p, n, v: v > 0) - # Split assets if size exceeds limits - multi_asset_arr = self._pack_tokens_for_change(address, change, self.context.protocol_param.max_val_size) + multi_asset_arr = self._pack_tokens_for_change( + address, change, self.context.protocol_param.max_val_size + ) # Include minimum lovelace into each token output except for the last one for i, multi_asset in enumerate(multi_asset_arr): @@ -201,7 +209,7 @@ def _calc_change(self, fees, inputs, outputs, address) -> List[TransactionOutput return change_output_arr def _add_change_and_fee( - self, change_address: Optional[Address] + self, change_address: Optional[Address] ) -> TransactionBuilder: original_outputs = self.outputs[:] if change_address: @@ -223,10 +231,16 @@ def _add_change_and_fee( return self - def _adding_asset_make_output_overflow(self, output: TransactionOutput, current_assets: Asset, - policy_id: ScriptHash, add_asset_name: AssetName, add_asset_val: int, - max_val_size: int) -> bool: - """ Check if adding the asset will make output exceed maximum size limit + def _adding_asset_make_output_overflow( + self, + output: TransactionOutput, + current_assets: Asset, + policy_id: ScriptHash, + add_asset_name: AssetName, + add_asset_val: int, + max_val_size: int, + ) -> bool: + """Check if adding the asset will make output exceed maximum size limit Args: output (TransactionOutput): current output @@ -249,8 +263,12 @@ def _adding_asset_make_output_overflow(self, output: TransactionOutput, current_ return len(attempt_amount.to_cbor("bytes")) > max_val_size - def _pack_tokens_for_change(self, change_address: Optional[Address], change_estimator: Value, max_val_size: int) -> \ - List[MultiAsset]: + def _pack_tokens_for_change( + self, + change_address: Optional[Address], + change_estimator: Value, + max_val_size: int, + ) -> List[MultiAsset]: multi_asset_arr = [] base_coin = Value(coin=change_estimator.coin) output = TransactionOutput(change_address, base_coin) @@ -262,8 +280,14 @@ def _pack_tokens_for_change(self, change_address: Optional[Address], change_esti temp_assets = Asset() old_amount = deepcopy(output.amount) for asset_name, asset_value in assets.items(): - if self._adding_asset_make_output_overflow(output, temp_assets, policy_id, asset_name, asset_value, - max_val_size): + if self._adding_asset_make_output_overflow( + output, + temp_assets, + policy_id, + asset_name, + asset_value, + max_val_size, + ): # Insert current assets as one group temp_multi_asset += MultiAsset({policy_id: temp_multi_asset}) temp_value.multi_asset = temp_multi_asset @@ -396,7 +420,7 @@ def build(self, change_address: Optional[Address] = None) -> TransactionBody: selected_amount.coin, selected_amount.multi_asset.filter( lambda p, n, v: p in requested_amount.multi_asset - and n in requested_amount.multi_asset[p] + and n in requested_amount.multi_asset[p] ), ) From 4c7ba766d0e187abc59d119f2aaad5343b7dd492 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Mon, 7 Mar 2022 15:49:58 -0500 Subject: [PATCH 08/36] integrate diff changes and remove unused functions --- pycardano/txbuilder.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 2511db05..a2e760e1 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -324,9 +324,6 @@ def _pack_tokens_for_change( return multi_asset_arr - def _fees_for_output(self): - return - def _input_vkey_hashes(self) -> Set[VerificationKeyHash]: results = set() for i in self.inputs: From d80063cd246b0dd8eb2c9bcdd6df9b1525759c94 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sun, 13 Mar 2022 17:39:32 -0400 Subject: [PATCH 09/36] add change split test case. Modify txbuilder logic in handeling splits. --- pycardano/txbuilder.py | 39 +++++++++----- test/pycardano/test_txbuilder.py | 82 ++++++++++++++++++++++++++++- test/pycardano/util.py | 90 ++++++++++++++++++-------------- 3 files changed, 159 insertions(+), 52 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index a2e760e1..0ddff918 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -156,7 +156,9 @@ def required_signers(self) -> List[VerificationKeyHash]: def required_signers(self, signers: List[VerificationKeyHash]): self._required_signers = signers - def _calc_change(self, fees, inputs, outputs, address) -> List[TransactionOutput]: + def _calc_change( + self, fees, inputs, outputs, address, precise_fee=False + ) -> List[TransactionOutput]: requested = Value(fees) for o in outputs: requested += o.amount @@ -196,14 +198,24 @@ def _calc_change(self, fees, inputs, outputs, address) -> List[TransactionOutput # Combine remainder of provided ADA with last MultiAsset for output # There may be rare cases where adding ADA causes size exceeds limit # We will revisit if it becomes an issue + if ( + precise_fee + and change.coin - min_lovelace(Value(0, multi_asset), self.context) + < 0 + ): + raise InsufficientUTxOBalanceException( + "Not enough ADA left to cover non-ADA assets in a change address" + ) + if i == len(multi_asset_arr) - 1: + # Include all ada in last output change_value = Value(change.coin, multi_asset) else: change_value = Value(0, multi_asset) change_value.coin = min_lovelace(change_value, self.context) + change_output_arr.append(TransactionOutput(address, change_value)) change -= change_value - # Remove assets with 0 quantity change.multi_asset = change.multi_asset.filter(lambda p, n, v: v > 0) return change_output_arr @@ -216,7 +228,7 @@ def _add_change_and_fee( # Set fee to max self.fee = max_tx_fee(self.context) changes = self._calc_change( - self.fee, self.inputs, self.outputs, change_address + self.fee, self.inputs, self.outputs, change_address, precise_fee=False ) self._outputs += changes @@ -225,7 +237,7 @@ def _add_change_and_fee( if change_address: self._outputs = original_outputs changes = self._calc_change( - self.fee, self.inputs, self.outputs, change_address + self.fee, self.inputs, self.outputs, change_address, precise_fee=True ) self._outputs += changes @@ -268,7 +280,7 @@ def _pack_tokens_for_change( change_address: Optional[Address], change_estimator: Value, max_val_size: int, - ) -> List[MultiAsset]: + ) -> List[MultiAsset]: multi_asset_arr = [] base_coin = Value(coin=change_estimator.coin) output = TransactionOutput(change_address, base_coin) @@ -288,10 +300,12 @@ def _pack_tokens_for_change( asset_value, max_val_size, ): - # Insert current assets as one group - temp_multi_asset += MultiAsset({policy_id: temp_multi_asset}) - temp_value.multi_asset = temp_multi_asset - output.amount += temp_value + # Insert current assets as one group if current assets isn't null + # This handles edge case when first Asset from next policy will cause overflow + if temp_assets: + temp_multi_asset += MultiAsset({policy_id: temp_assets}) + temp_value.multi_asset = temp_multi_asset + output.amount += temp_value multi_asset_arr.append(output.amount.multi_asset) # Create a new output @@ -299,7 +313,7 @@ def _pack_tokens_for_change( output = TransactionOutput(change_address, base_coin) # Continue building output from where we stopped - old_amount = output.amount.copy() + old_amount = deepcopy(output.amount) temp_multi_asset = MultiAsset() temp_value = Value() temp_assets = Asset() @@ -320,8 +334,9 @@ def _pack_tokens_for_change( output.amount = old_amount break - multi_asset_arr.append(output.amount.multi_asset) - + multi_asset_arr.append(output.amount.multi_asset) + # Remove records where MultiAsset is null due to overflow of adding + # items at the beginning of next policy to previous policy MultiAssets return multi_asset_arr def _input_vkey_hashes(self) -> Set[VerificationKeyHash]: diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index e61c889a..756cdf78 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -1,10 +1,14 @@ +from dataclasses import replace from test.pycardano.util import chain_context import pytest from pycardano.address import Address from pycardano.coinselection import RandomImproveMultiAsset -from pycardano.exception import InvalidTransactionException +from pycardano.exception import ( + InsufficientUTxOBalanceException, + InvalidTransactionException, +) from pycardano.key import VerificationKey from pycardano.nativescript import ( InvalidBefore, @@ -198,3 +202,79 @@ def test_tx_builder_mint_multi_asset(chain_context): } assert expected == tx_body.to_primitive() + + +def test_tx_add_change_split_nfts(chain_context): + # Set the max value size to be very small for testing purpose + param = {"max_val_size": 50} + temp_protocol_param = replace(chain_context.protocol_param, **param) + chain_context.protocol_param = temp_protocol_param + tx_builder = TransactionBuilder(chain_context, [RandomImproveMultiAsset([0, 0])]) + sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" + sender_address = Address.from_primitive(sender) + + tx_builder.add_input_address(sender).add_output( + TransactionOutput.from_primitive([sender, 7000000]) + ) + + tx_body = tx_builder.build(change_address=sender_address) + + expected = { + 0: [ + [b"11111111111111111111111111111111", 0], + [b"22222222222222222222222222222222", 1], + ], + 1: [ + # First output + [sender_address.to_primitive(), 7000000], + # Change output + [ + sender_address.to_primitive(), + [1344798, {b"1111111111111111111111111111": {b"Token1": 1}}], + ], + # Second change output from split due to change size limit exceed + # Fourth output as change + [ + sender_address.to_primitive(), + [2482881, {b"1111111111111111111111111111": {b"Token2": 2}}], + ], + ], + 2: 172321, + } + + assert expected == tx_body.to_primitive() + + +def test_tx_add_change_split_nfts_not_enough_add(chain_context): + vk1 = VerificationKey.from_cbor( + "58206443a101bdb948366fc87369336224595d36d8b0eee5602cba8b81a024e58473" + ) + vk2 = VerificationKey.from_cbor( + "58206443a101bdb948366fc87369336224595d36d8b0eee5602cba8b81a024e58475" + ) + spk1 = ScriptPubkey(key_hash=vk1.hash()) + spk2 = ScriptPubkey(key_hash=vk2.hash()) + before = InvalidHereAfter(123456789) + after = InvalidBefore(123456780) + script = ScriptAll([before, after, spk1, ScriptAll([spk1, spk2])]) + policy_id = script.hash() + + # Set the max value size to be very small for testing purpose + param = {"max_val_size": 50} + temp_protocol_param = replace(chain_context.protocol_param, **param) + chain_context.protocol_param = temp_protocol_param + tx_builder = TransactionBuilder(chain_context) + sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" + sender_address = Address.from_primitive(sender) + + # Add sender address as input + mint = {policy_id.payload: {b"Token3": 1}} + tx_builder.add_input_address(sender).add_output( + TransactionOutput.from_primitive([sender, 7000000]) + ) + tx_builder.mint = MultiAsset.from_primitive(mint) + tx_builder.native_scripts = [script] + tx_builder.ttl = 123456789 + + with pytest.raises(InsufficientUTxOBalanceException): + tx_body = tx_builder.build(change_address=sender_address) diff --git a/test/pycardano/util.py b/test/pycardano/util.py index aaf2a389..25e50d6f 100644 --- a/test/pycardano/util.py +++ b/test/pycardano/util.py @@ -16,52 +16,64 @@ def check_two_way_cbor(serializable: CBORSerializable): class FixedChainContext(ChainContext): + + _protocol_param = ProtocolParameters( + min_fee_constant=155381, + min_fee_coefficient=44, + max_block_size=73728, + max_tx_size=16384, + max_block_header_size=1100, + key_deposit=2000000, + pool_deposit=500000000, + pool_influence=0.3, + treasury_expansion=0.2, + monetary_expansion=0.003, + decentralization_param=0, + protocol_major_version=6, + protocol_minor_version=0, + min_utxo=1000000, + min_pool_cost=340000000, + price_mem=0.0577, + price_step=0.0000721, + max_tx_ex_mem=10000000, + max_tx_ex_steps=10000000000, + max_block_ex_mem=50000000, + max_block_ex_steps=40000000000, + max_val_size=5000, + collateral_percent=150, + max_collateral_inputs=3, + coins_per_utxo_word=34482, + ) + + _genesis_param = GenesisParameters( + active_slots_coefficient=0.05, + update_quorum=5, + max_lovelace_supply=45000000000000000, + network_magic=764824073, + epoch_length=432000, + system_start=1506203091, + slots_per_kes_period=129600, + slot_length=1, + max_kes_evolutions=62, + security_param=2160, + ) + @property def protocol_param(self) -> ProtocolParameters: """Get current protocol parameters""" - return ProtocolParameters( - min_fee_constant=155381, - min_fee_coefficient=44, - max_block_size=73728, - max_tx_size=16384, - max_block_header_size=1100, - key_deposit=2000000, - pool_deposit=500000000, - pool_influence=0.3, - treasury_expansion=0.2, - monetary_expansion=0.003, - decentralization_param=0, - protocol_major_version=6, - protocol_minor_version=0, - min_utxo=1000000, - min_pool_cost=340000000, - price_mem=0.0577, - price_step=0.0000721, - max_tx_ex_mem=10000000, - max_tx_ex_steps=10000000000, - max_block_ex_mem=50000000, - max_block_ex_steps=40000000000, - max_val_size=5000, - collateral_percent=150, - max_collateral_inputs=3, - coins_per_utxo_word=34482, - ) + return self._protocol_param + + # Create setter function to allow parameter modifications + # for testing purposes + @protocol_param.setter + def protocol_param(self, protocol_param: ProtocolParameters): + # if type(protocol_param) is ProtocolParameters: + self._protocol_param = protocol_param @property def genesis_param(self) -> GenesisParameters: """Get chain genesis parameters""" - return GenesisParameters( - active_slots_coefficient=0.05, - update_quorum=5, - max_lovelace_supply=45000000000000000, - network_magic=764824073, - epoch_length=432000, - system_start=1506203091, - slots_per_kes_period=129600, - slot_length=1, - max_kes_evolutions=62, - security_param=2160, - ) + return self._genesis_param @property def network(self) -> Network: From ca3b3f2178f855a780c3508164961fbe6a8d945b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Mar 2022 02:35:21 +0000 Subject: [PATCH 10/36] Bump pytest from 7.0.1 to 7.1.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.1 to 7.1.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.0.1...7.1.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 45fa57ab..25f57ae1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -411,11 +411,11 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.0.1" +version = "7.1.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} @@ -712,7 +712,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "1816337d2a726bd3529e45ce46f4f0df17c8e91be7cdd3d1611759f61626b60d" +content-hash = "4692a71eefaffc93ec2535830725482d451fbf687f6d86c98c4657622f631c3e" [metadata.files] alabaster = [ @@ -1041,8 +1041,8 @@ pyparsing = [ {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ - {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, - {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, + {file = "pytest-7.1.1-py3-none-any.whl", hash = "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea"}, + {file = "pytest-7.1.1.tar.gz", hash = "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63"}, ] pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, diff --git a/pyproject.toml b/pyproject.toml index e6e01158..a2d33167 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ websocket-client = "^1.2.3" [tool.poetry.dev-dependencies] Sphinx = "^4.3.2" sphinx-rtd-theme = "^1.0.0" -pytest = "^7.0.1" +pytest = "^7.1.1" pytest-cov = "^3.0.0" flake8 = "^4.0.1" isort = "^5.10.1" From 68cc134f40e88af2d9635514bfa0abfecadf99e2 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Mon, 18 Apr 2022 23:11:02 -0400 Subject: [PATCH 11/36] replace max fee with more accurately estimated fees --- pycardano/txbuilder.py | 42 +++++++++++++++++++++++--------- test/pycardano/test_txbuilder.py | 2 +- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index aeb62966..f3f11ae4 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -301,11 +301,7 @@ def _calc_change( # Combine remainder of provided ADA with last MultiAsset for output # There may be rare cases where adding ADA causes size exceeds limit # We will revisit if it becomes an issue - if ( - precise_fee - and change.coin - min_lovelace(Value(0, multi_asset), self.context) - < 0 - ): + if change.coin - min_lovelace(Value(0, multi_asset), self.context) < 0: raise InsufficientUTxOBalanceException( "Not enough ADA left to cover non-ADA assets in a change address" ) @@ -327,18 +323,25 @@ def _add_change_and_fee( self, change_address: Optional[Address] ) -> TransactionBuilder: original_outputs = self.outputs[:] + + plutus_execution_units = ExecutionUnits(0, 0) + for redeemer in self.redeemers: + plutus_execution_units += redeemer.ex_units + if change_address: # Set fee to max - self.fee = max_tx_fee(self.context) + self.fee = fee( + self.context, + len(self._build_full_fake_tx().to_cbor("bytes")), + plutus_execution_units.steps, + plutus_execution_units.mem, + ) changes = self._calc_change( - self.fee, self.inputs, self.outputs, change_address, precise_fee=False + self.fee, self.inputs, self.outputs, change_address, precise_fee=True ) self._outputs += changes - plutus_execution_units = ExecutionUnits(0, 0) - for redeemer in self.redeemers: - plutus_execution_units += redeemer.ex_units - + # With changes included, we can estimate the fee more precisely self.fee = fee( self.context, len(self._build_full_fake_tx().to_cbor("bytes")), @@ -588,6 +591,22 @@ def build(self, change_address: Optional[Address] = None) -> TransactionBody: for o in self.outputs: requested_amount += o.amount + # Include min fees associated as part of requested amount. + # The fees will be slightly smaller because changes aren't included yet. + # The edge case will be covered in _calc_change function if requested is + # greater than provided + plutus_execution_units = ExecutionUnits(0, 0) + for redeemer in self.redeemers: + plutus_execution_units += redeemer.ex_units + + min_fee = fee( + self.context, + len(self._build_full_fake_tx().to_cbor("bytes")), + plutus_execution_units.steps, + plutus_execution_units.mem, + ) + requested_amount += min_fee + # Trim off assets that are not requested because they will be returned as changes eventually. trimmed_selected_amount = Value( selected_amount.coin, @@ -624,6 +643,7 @@ def build(self, change_address: Optional[Address] = None) -> TransactionBody: additional_utxo_pool, [TransactionOutput(None, unfulfilled_amount)], self.context, + include_max_fee=False ) for s in selected: selected_amount += s.output.amount diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index a949a159..63d26917 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -112,7 +112,7 @@ def test_tx_builder_with_certain_input(chain_context): assert expected == tx_body.to_primitive() - +# TODO: verify the Random UTXO selector so only sufficient utxos need to be selected to reduce fee def test_tx_builder_multi_asset(chain_context): tx_builder = TransactionBuilder(chain_context) sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" From e4eefd306fdb1deaca54cec15036ceb0e9408381 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Tue, 19 Apr 2022 21:06:09 -0400 Subject: [PATCH 12/36] replace max fee with more precise fee estimation --- pycardano/txbuilder.py | 22 ++++--- test/pycardano/test_txbuilder.py | 107 ++++++++++++++++++++++++++++--- 2 files changed, 111 insertions(+), 18 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index f3f11ae4..e9ba3803 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -286,6 +286,8 @@ def _calc_change( # when there is only ADA left, simply use remaining coin value as change if not change.multi_asset: + if change.coin < min_lovelace(change, self.context): + raise InsufficientUTxOBalanceException("Not enough ADA left for change") lovelace_change = change.coin change_output_arr.append(TransactionOutput(address, lovelace_change)) @@ -301,7 +303,7 @@ def _calc_change( # Combine remainder of provided ADA with last MultiAsset for output # There may be rare cases where adding ADA causes size exceeds limit # We will revisit if it becomes an issue - if change.coin - min_lovelace(Value(0, multi_asset), self.context) < 0: + if change.coin < min_lovelace(Value(0, multi_asset), self.context): raise InsufficientUTxOBalanceException( "Not enough ADA left to cover non-ADA assets in a change address" ) @@ -591,10 +593,8 @@ def build(self, change_address: Optional[Address] = None) -> TransactionBody: for o in self.outputs: requested_amount += o.amount - # Include min fees associated as part of requested amount. - # The fees will be slightly smaller because changes aren't included yet. - # The edge case will be covered in _calc_change function if requested is - # greater than provided + # Include min fees associated as part of requested amount + # The fees will be slightly smaller because changes aren't included yet plutus_execution_units = ExecutionUnits(0, 0) for redeemer in self.redeemers: plutus_execution_units += redeemer.ex_units @@ -617,7 +617,13 @@ def build(self, change_address: Optional[Address] = None) -> TransactionBody: ) unfulfilled_amount = requested_amount - trimmed_selected_amount - unfulfilled_amount.coin = max(0, unfulfilled_amount.coin) + # if remainder is smaller than minimum ADA required in change, + # we need to select additional UTxOs available from the address + unfulfilled_amount.coin = max( + 0, + unfulfilled_amount.coin + + min_lovelace(selected_amount - trimmed_selected_amount, self.context), + ) # Clean up all non-positive assets unfulfilled_amount.multi_asset = unfulfilled_amount.multi_asset.filter( lambda p, n, v: v > 0 @@ -643,7 +649,7 @@ def build(self, change_address: Optional[Address] = None) -> TransactionBody: additional_utxo_pool, [TransactionOutput(None, unfulfilled_amount)], self.context, - include_max_fee=False + include_max_fee=False, ) for s in selected: selected_amount += s.output.amount @@ -756,4 +762,4 @@ def build_and_sign( VerificationKeyWitness(signing_key.to_verification_key(), signature) ) - return Transaction(tx_body, witness_set, auxiliary_data=self.auxiliary_data) \ No newline at end of file + return Transaction(tx_body, witness_set, auxiliary_data=self.auxiliary_data) diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 63d26917..058ecf6b 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -112,7 +112,7 @@ def test_tx_builder_with_certain_input(chain_context): assert expected == tx_body.to_primitive() -# TODO: verify the Random UTXO selector so only sufficient utxos need to be selected to reduce fee + def test_tx_builder_multi_asset(chain_context): tx_builder = TransactionBuilder(chain_context) sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" @@ -120,7 +120,7 @@ def test_tx_builder_multi_asset(chain_context): # Add sender address as input tx_builder.add_input_address(sender).add_output( - TransactionOutput.from_primitive([sender, 1000000]) + TransactionOutput.from_primitive([sender, 3000000]) ).add_output( TransactionOutput.from_primitive( [sender, [2000000, {b"1" * 28: {b"Token1": 1}}]] @@ -136,7 +136,7 @@ def test_tx_builder_multi_asset(chain_context): ], 1: [ # First output - [sender_address.to_primitive(), 1000000], + [sender_address.to_primitive(), 3000000], # Second output [ sender_address.to_primitive(), @@ -145,7 +145,7 @@ def test_tx_builder_multi_asset(chain_context): # Third output as change [ sender_address.to_primitive(), - [7827767, {b"1111111111111111111111111111": {b"Token2": 2}}], + [5827767, {b"1111111111111111111111111111": {b"Token2": 2}}], ], ], 2: 172233, @@ -171,7 +171,8 @@ def test_tx_builder_raises_utxo_selection(chain_context): with pytest.raises(UTxOSelectionException) as e: tx_body = tx_builder.build(change_address=sender_address) assert "Unfulfilled amount:" in e.value.args[0] - assert "'coin': 991000000" in e.value.args[0] + # The unfulfilled amount includes requested (991000000) and estimated fees (161101) + assert "'coin': 991161101" in e.value.args[0] assert "{AssetName(b'NewToken'): 1}" in e.value.args[0] @@ -189,6 +190,92 @@ def test_tx_too_big_exception(chain_context): tx_body = tx_builder.build(change_address=sender_address) +def test_tx_small_utxo_precise_fee(chain_context): + tx_builder = TransactionBuilder(chain_context) + sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" + sender_address = Address.from_primitive(sender) + + tx_in1 = TransactionInput.from_primitive([b"1" * 32, 3]) + tx_out1 = TransactionOutput.from_primitive([sender, 4000000]) + utxo1 = UTxO(tx_in1, tx_out1) + + tx_builder.add_input(utxo1).add_output( + TransactionOutput.from_primitive([sender, 2500000]) + ) + + # This will not fail as we replace max fee constraint with more precise fee calculation + # And remainder is greater than minimum ada required for change + tx_body = tx_builder.build(change_address=sender_address) + + expect = { + 0: [ + [b"11111111111111111111111111111111", 3], + ], + 1: [ + # First output + [sender_address.to_primitive(), 2500000], + # Second output as change + [sender_address.to_primitive(), 1334587], + ], + 2: 165413, + } + + expect == tx_body.to_primitive() + + +def test_tx_small_utxo_balance_fail(chain_context): + tx_builder = TransactionBuilder(chain_context) + sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" + sender_address = Address.from_primitive(sender) + + tx_in1 = TransactionInput.from_primitive([b"1" * 32, 3]) + tx_out1 = TransactionOutput.from_primitive([sender, 4000000]) + utxo1 = UTxO(tx_in1, tx_out1) + + tx_builder.add_input(utxo1).add_output( + TransactionOutput.from_primitive([sender, 3000000]) + ) + + # Balance is smaller than minimum ada required in change + # No more UTxO is available, throwing UTxO selection exception + with pytest.raises(UTxOSelectionException): + tx_body = tx_builder.build(change_address=sender_address) + + +def test_tx_small_utxo_balance_pass(chain_context): + tx_builder = TransactionBuilder(chain_context) + sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" + sender_address = Address.from_primitive(sender) + + tx_in1 = TransactionInput.from_primitive([b"1" * 32, 3]) + tx_out1 = TransactionOutput.from_primitive([sender, 4000000]) + utxo1 = UTxO(tx_in1, tx_out1) + + tx_builder.add_input(utxo1).add_input_address(sender_address).add_output( + TransactionOutput.from_primitive([sender, 3000000]) + ) + + # Balance is smaller than minimum ada required in change + # No more UTxO is available, throwing UTxO selection exception + tx_body = tx_builder.build(change_address=sender_address) + + expected = { + 0: [ + [b"11111111111111111111111111111111", 3], + [b"11111111111111111111111111111111", 1], + ], + 1: [ + # First output + [sender_address.to_primitive(), 3000000], + # Second output as change + [sender_address.to_primitive(), 5833003], + ], + 2: 166997, + } + + expected == tx_body.to_primitive() + + def test_tx_builder_mint_multi_asset(chain_context): vk1 = VerificationKey.from_cbor( "58206443a101bdb948366fc87369336224595d36d8b0eee5602cba8b81a024e58473" @@ -210,7 +297,7 @@ def test_tx_builder_mint_multi_asset(chain_context): # Add sender address as input mint = {policy_id.payload: {b"Token1": 1}} tx_builder.add_input_address(sender).add_output( - TransactionOutput.from_primitive([sender, 1000000]) + TransactionOutput.from_primitive([sender, 3000000]) ).add_output(TransactionOutput.from_primitive([sender, [2000000, mint]])) tx_builder.mint = MultiAsset.from_primitive(mint) tx_builder.native_scripts = [script] @@ -225,14 +312,14 @@ def test_tx_builder_mint_multi_asset(chain_context): ], 1: [ # First output - [sender_address.to_primitive(), 1000000], + [sender_address.to_primitive(), 3000000], # Second output [sender_address.to_primitive(), [2000000, mint]], # Third output as change [ sender_address.to_primitive(), [ - 7811267, + 5811267, {b"1111111111111111111111111111": {b"Token1": 1, b"Token2": 2}}, ], ], @@ -332,7 +419,7 @@ def test_not_enough_input_amount(chain_context): TransactionOutput.from_primitive([sender, input_utxo.output.amount]) ) - with pytest.raises(InvalidTransactionException): + with pytest.raises(UTxOSelectionException): # Tx builder must fail here because there is not enough amount of input ADA to pay tx fee tx_body = tx_builder.build(change_address=sender_address) @@ -490,4 +577,4 @@ def test_estimate_execution_unit(chain_context): "657374546f6b656e01021a0003391a09a1581c876f19078b059c928258d848c8cd871864d28" "1eb6776ed7f80b68536a14954657374546f6b656e010b58206b5664c6f79646f2a4c17bdc1e" "cb6f6bf540db5c82dfa0a9d806c435398756fa" == tx_body.to_cbor() - ) \ No newline at end of file + ) From 0e6758bd8ec549979e4fd4e860c68437c227dcd2 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Tue, 19 Apr 2022 21:11:07 -0400 Subject: [PATCH 13/36] remove max fee import --- pycardano/txbuilder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index e9ba3803..45943557 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -41,7 +41,7 @@ UTxO, Value, ) -from pycardano.utils import fee, max_tx_fee, min_lovelace, script_data_hash +from pycardano.utils import fee, min_lovelace, script_data_hash from pycardano.witness import TransactionWitnessSet, VerificationKeyWitness __all__ = ["TransactionBuilder"] From c713360554737bf5f6f3b90a46b0222ee627b64a Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Wed, 20 Apr 2022 22:49:36 -0400 Subject: [PATCH 14/36] modify format to be consistent with code base --- pycardano/txbuilder.py | 72 ++++++++++++++++++-------------- test/pycardano/test_txbuilder.py | 2 +- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 45943557..1c5fc946 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -111,8 +111,10 @@ class TransactionBuilder: def add_input(self, utxo: UTxO) -> TransactionBuilder: """Add a specific UTxO to transaction's inputs. + Args: utxo (UTxO): UTxO to be added. + Returns: TransactionBuilder: Current transaction builder. """ @@ -149,11 +151,13 @@ def add_script_input( self, utxo: UTxO, script: bytes, datum: Datum, redeemer: Redeemer ) -> TransactionBuilder: """Add a script UTxO to transaction's inputs. + Args: utxo (UTxO): Script UTxO to be added. script (Optional[bytes]): A plutus script. datum (Optional[Datum]): A plutus datum to unlock the UTxO. redeemer (Optional[Redeemer]): A plutus redeemer to unlock the UTxO. + Returns: TransactionBuilder: Current transaction builder. """ @@ -179,8 +183,10 @@ def add_input_address(self, address: Union[Address, str]) -> TransactionBuilder: Unlike :meth:`add_input`, which deterministically adds a UTxO to the transaction's inputs, `add_input_address` will not immediately select any UTxO when called. Instead, it will delegate UTxO selection to :class:`UTxOSelector`s of the builder when :meth:`build` is called. + Args: address (Union[Address, str]): Address to be added. + Returns: TransactionBuilder: The current transaction builder. """ @@ -194,10 +200,12 @@ def add_output( add_datum_to_witness: bool = False, ) -> TransactionBuilder: """Add a transaction output. + Args: tx_out (TransactionOutput): The transaction output to be added. datum (Datum): Attach a datum hash to this transaction output. add_datum_to_witness (bool): Optionally add the actual datum to transaction witness set. Defaults to False. + Returns: TransactionBuilder: Current transaction builder. """ @@ -326,30 +334,16 @@ def _add_change_and_fee( ) -> TransactionBuilder: original_outputs = self.outputs[:] - plutus_execution_units = ExecutionUnits(0, 0) - for redeemer in self.redeemers: - plutus_execution_units += redeemer.ex_units - if change_address: # Set fee to max - self.fee = fee( - self.context, - len(self._build_full_fake_tx().to_cbor("bytes")), - plutus_execution_units.steps, - plutus_execution_units.mem, - ) + self.fee = self._estimate_fee() changes = self._calc_change( self.fee, self.inputs, self.outputs, change_address, precise_fee=True ) self._outputs += changes # With changes included, we can estimate the fee more precisely - self.fee = fee( - self.context, - len(self._build_full_fake_tx().to_cbor("bytes")), - plutus_execution_units.steps, - plutus_execution_units.mem, - ) + self.fee = self._estimate_fee() if change_address: self._outputs = original_outputs @@ -370,11 +364,17 @@ def _adding_asset_make_output_overflow( max_val_size: int, ) -> bool: """Check if adding the asset will make output exceed maximum size limit + Args: - output (TransactionOutput): current output - current_assets (Asset): current Assets to be included in output - policy_id (ScriptHash): policy id containing the MultiAsset - asset_to_add (Asset): Asset to add to current MultiAsset to check size limit + output (TransactionOutput): Current output + current_assets (Asset): Current Assets to be included in output + policy_id (ScriptHash): Policy id containing the MultiAsset + add_asset_name (AssetName): Name of asset to add to current MultiAsset + add_asset_val (int): Value of asset to add to current MultiAsset + max_val_size (int): maximum size limit for output + + Returns: + bool: whether adding asset will make output greater than maximum size limit """ attempt_assets = deepcopy(current_assets) attempt_assets += Asset({add_asset_name: add_asset_val}) @@ -553,6 +553,7 @@ def _build_full_fake_tx(self) -> Transaction: def build_witness_set(self) -> TransactionWitnessSet: """Build a transaction witness set, excluding verification key witnesses. This function is especially useful when the transaction involves Plutus scripts. + Returns: TransactionWitnessSet: A transaction witness set without verification key witnesses. """ @@ -571,11 +572,27 @@ def _ensure_no_input_exclusion_conflict(self): f"{intersection}." ) + def _estimate_fee(self): + plutus_execution_units = ExecutionUnits(0, 0) + for redeemer in self.redeemers: + plutus_execution_units += redeemer.ex_units + + estimated_fee = fee( + self.context, + len(self._build_full_fake_tx().to_cbor("bytes")), + plutus_execution_units.steps, + plutus_execution_units.mem, + ) + + return estimated_fee + def build(self, change_address: Optional[Address] = None) -> TransactionBody: """Build a transaction body from all constraints set through the builder. + Args: change_address (Optional[Address]): Address to which changes will be returned. If not provided, the transaction body will likely be unbalanced (sum of inputs is greater than the sum of outputs). + Returns: TransactionBody: A transaction body. """ @@ -594,18 +611,7 @@ def build(self, change_address: Optional[Address] = None) -> TransactionBody: requested_amount += o.amount # Include min fees associated as part of requested amount - # The fees will be slightly smaller because changes aren't included yet - plutus_execution_units = ExecutionUnits(0, 0) - for redeemer in self.redeemers: - plutus_execution_units += redeemer.ex_units - - min_fee = fee( - self.context, - len(self._build_full_fake_tx().to_cbor("bytes")), - plutus_execution_units.steps, - plutus_execution_units.mem, - ) - requested_amount += min_fee + requested_amount += self._estimate_fee() # Trim off assets that are not requested because they will be returned as changes eventually. trimmed_selected_amount = Value( @@ -743,11 +749,13 @@ def build_and_sign( ) -> Transaction: """Build a transaction body from all constraints set through the builder and sign the transaction with provided signing keys. + Args: signing_keys (List[Union[SigningKey, ExtendedSigningKey]]): A list of signing keys that will be used to sign the transaction. change_address (Optional[Address]): Address to which changes will be returned. If not provided, the transaction body will likely be unbalanced (sum of inputs is greater than the sum of outputs). + Returns: Transaction: A signed transaction. """ diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 058ecf6b..d08ce4b0 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -256,7 +256,7 @@ def test_tx_small_utxo_balance_pass(chain_context): ) # Balance is smaller than minimum ada required in change - # No more UTxO is available, throwing UTxO selection exception + # Additional UTxOs are selected from the input address tx_body = tx_builder.build(change_address=sender_address) expected = { From 71d93e3e92f2c751b6d7bf7347e5efb69b1e9287 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Mon, 16 May 2022 23:15:59 -0400 Subject: [PATCH 15/36] supports utxo queries using kupo --- integration-test/docker-compose.yml | 18 ++++++ integration-test/test/test_all.py | 16 +++-- pycardano/backend/ogmios.py | 92 ++++++++++++++++++++++++++--- 3 files changed, 113 insertions(+), 13 deletions(-) diff --git a/integration-test/docker-compose.yml b/integration-test/docker-compose.yml index c8f0ec94..c81a28f2 100644 --- a/integration-test/docker-compose.yml +++ b/integration-test/docker-compose.yml @@ -40,6 +40,24 @@ services: max-size: "200k" max-file: "10" + kupo: + image: cardanosolutions/kupo + environment: + NETWORK: "${NETWORK:-local}" + + command: [ + "--node-socket", "/ipc/node.socket", + "--node-config", "/code/configs/${NETWORK:-local}/config.json", + "--host", "0.0.0.0", + "--since", "origin", + "--match", "*", + "--workdir", "/db" + ] + volumes: + - .:/code + - .:/db + - node-ipc:/ipc + volumes: node-db: node-ipc: diff --git a/integration-test/test/test_all.py b/integration-test/test/test_all.py index d45cc1fa..1b5fb466 100644 --- a/integration-test/test/test_all.py +++ b/integration-test/test/test_all.py @@ -22,7 +22,9 @@ class TestAll: OGMIOS_WS = "ws://localhost:1337" - chain_context = OgmiosChainContext(OGMIOS_WS, Network.TESTNET) + KUPO_HTTP = "http://localhost:1442/v1/matches" + + chain_context = OgmiosChainContext(OGMIOS_WS, Network.TESTNET, support_kupo=True, http_url=KUPO_HTTP) check_chain_context(chain_context) @@ -167,7 +169,7 @@ def load_or_create_key_pair(base_dir, base_name): time.sleep(3) - utxos = self.chain_context.utxos(str(address)) + utxos = self.chain_context.utxos(str(address), use_kupo=True) found_nft = False for utxo in utxos: @@ -177,8 +179,12 @@ def load_or_create_key_pair(base_dir, base_name): assert found_nft, f"Cannot find target NFT in address: {address}" + # Generate another set of keys and address to send NFT to + skey2, vkey2 = load_or_create_key_pair(key_dir, "keys2") + address2 = Address(vkey2.hash(), network=self.NETWORK) + nft_to_send = TransactionOutput( - address, + address2, Value( 20000000, MultiAsset.from_primitive({policy_id.payload: {b"MY_NFT_1": 1}}), @@ -203,7 +209,7 @@ def load_or_create_key_pair(base_dir, base_name): time.sleep(3) - utxos = self.chain_context.utxos(str(address)) + utxos = self.chain_context.utxos(str(address2), use_kupo=True) found_nft = False for utxo in utxos: @@ -211,7 +217,7 @@ def load_or_create_key_pair(base_dir, base_name): if output == nft_to_send: found_nft = True - assert found_nft, f"Cannot find target NFT in address: {address}" + assert found_nft, f"Cannot find target NFT in address: {address2}" @retry(tries=2, delay=6) def test_plutus(self): diff --git a/pycardano/backend/ogmios.py b/pycardano/backend/ogmios.py index 2ec2acad..d22d30eb 100644 --- a/pycardano/backend/ogmios.py +++ b/pycardano/backend/ogmios.py @@ -4,6 +4,7 @@ from typing import Dict, List, Union import websocket +import requests from pycardano.address import Address from pycardano.backend.base import ChainContext, GenesisParameters, ProtocolParameters @@ -25,13 +26,19 @@ class OgmiosChainContext(ChainContext): - def __init__(self, ws_url: str, network: Network, compact_result=True): + def __init__(self, ws_url: str, network: Network, compact_result=True, support_kupo=False, http_url=None): self._ws_url = ws_url self._network = network self._service_name = "ogmios.v1:compact" if compact_result else "ogmios" + self._support_kupo = support_kupo + self._http_url = http_url if support_kupo else None self._last_known_block_slot = 0 self._genesis_param = None self._protocol_param = None + if self._support_kupo and not self._http_url: + raise Exception( + "Cannot find http url to request from Kupo." + ) def _request(self, method: str, args: dict) -> Union[dict, int]: ws = websocket.WebSocket() @@ -152,8 +159,16 @@ def last_block_slot(self) -> int: args = {"query": "chainTip"} return self._request(method, args)["slot"] - def utxos(self, address: str) -> List[UTxO]: - """Get all UTxOs associated with an address. + def _extract_asset_info(self, asset_hash: str): + policy_hex, asset_name_hex = asset_hash.split(".") + policy = ScriptHash.from_primitive(policy_hex) + asset_name_hex = AssetName.from_primitive(asset_name_hex) + + return policy_hex, policy, asset_name_hex + + def _utxos_kupo(self, address: str) -> List[UTxO]: + """Get all UTxOs associated with an address with Kupo. + Since UTxO querying will be deprecated from Ogmios in next major release: https://ogmios.dev/mini-protocols/local-state-query/. Args: address (str): An address encoded with bech32. @@ -161,6 +176,52 @@ def utxos(self, address: str) -> List[UTxO]: Returns: List[UTxO]: A list of UTxOs. """ + address_url = self._http_url + "/" + address + results = requests.get(address_url).json() + + utxos = [] + + for result in results: + tx_id = result['transaction_id'] + index = result['output_index'] + tx_in = TransactionInput.from_primitive([tx_id, index]) + + lovelace_amount = result['value']['coins'] + + datum_hash = result['datum_hash'] + + if not result['value']['assets']: + tx_out = TransactionOutput( + Address.from_primitive(address), + amount=lovelace_amount, + datum_hash=datum_hash + ) + else: + multi_assets = MultiAsset() + + for asset, quantity in result['value']['assets'].items(): + policy_hex, policy, asset_name_hex = self._extract_asset_info(asset) + multi_assets.setdefault(policy, Asset())[asset_name_hex] = quantity + + tx_out = TransactionOutput( + Address.from_primitive(address), + amount=Value(lovelace_amount, multi_assets), + datum_hash=datum_hash, + ) + utxos.append(UTxO(tx_in, tx_out)) + + return utxos + + def _utxos_ogmios(self, address: str) -> List[UTxO]: + """Get all UTxOs associated with an address with Ogmios. + + Args: + address (str): An address encoded with bech32. + + Returns: + List[UTxO]: A list of UTxOs. + """ + method = "Query" args = {"query": {"utxo": [address]}} results = self._request(method, args) @@ -187,11 +248,8 @@ def utxos(self, address: str) -> List[UTxO]: else: multi_assets = MultiAsset() - for asset in output["value"]["assets"]: - quantity = output["value"]["assets"][asset] - policy_hex, asset_name_hex = asset.split(".") - policy = ScriptHash.from_primitive(policy_hex) - asset_name_hex = AssetName.from_primitive(asset_name_hex) + for asset, quantity in output["value"]["assets"].items(): + policy_hex, policy, asset_name_hex = self._extract_asset_info(asset) multi_assets.setdefault(policy, Asset())[asset_name_hex] = quantity tx_out = TransactionOutput( @@ -203,6 +261,24 @@ def utxos(self, address: str) -> List[UTxO]: return utxos + + def utxos(self, address: str, use_kupo=False) -> List[UTxO]: + """Get all UTxOs associated with an address. + + Args: + address (str): An address encoded with bech32. + + Returns: + List[UTxO]: A list of UTxOs. + """ + if not use_kupo: + utxos = self._utxos_ogmios(address) + else: + utxos = self._utxos_kupo(address) + + return utxos + + def submit_tx(self, cbor: Union[bytes, str]): """Submit a transaction to the blockchain. From dffeff4c949813e3e227f56f09b47cd4ee853597 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Tue, 17 May 2022 22:25:35 -0400 Subject: [PATCH 16/36] support utxo query with Kupo --- integration-test/docker-compose.yml | 5 +++-- integration-test/run_tests.sh | 4 ++-- integration-test/test/test_all.py | 15 ++++++++++++- pycardano/backend/ogmios.py | 34 ++++++++++++++++------------- pycardano/txbuilder.py | 2 +- test/pycardano/test_txbuilder.py | 8 ------- 6 files changed, 39 insertions(+), 29 deletions(-) diff --git a/integration-test/docker-compose.yml b/integration-test/docker-compose.yml index c81a28f2..bd04a646 100644 --- a/integration-test/docker-compose.yml +++ b/integration-test/docker-compose.yml @@ -51,12 +51,13 @@ services: "--host", "0.0.0.0", "--since", "origin", "--match", "*", - "--workdir", "/db" + "--in-memory" ] volumes: - .:/code - - .:/db - node-ipc:/ipc + ports: + - ${KUPO_PORT:-1442}:1442 volumes: node-db: diff --git a/integration-test/run_tests.sh b/integration-test/run_tests.sh index bfef60eb..3b10ca92 100755 --- a/integration-test/run_tests.sh +++ b/integration-test/run_tests.sh @@ -11,7 +11,7 @@ poetry install ./bootstrap.sh local-alonzo # Cleanup containers and volumes in case there is any running -docker-compose down --volume +docker-compose down --volumes # Launch containers docker-compose up -d @@ -21,4 +21,4 @@ export EXTENDED_PAYMENT_KEY="$ROOT"/keys/extended.skey poetry run pytest -s "$ROOT"/test # Cleanup -docker-compose down --volume +docker-compose down --volumes diff --git a/integration-test/test/test_all.py b/integration-test/test/test_all.py index 1b5fb466..806f02fa 100644 --- a/integration-test/test/test_all.py +++ b/integration-test/test/test_all.py @@ -24,7 +24,9 @@ class TestAll: KUPO_HTTP = "http://localhost:1442/v1/matches" - chain_context = OgmiosChainContext(OGMIOS_WS, Network.TESTNET, support_kupo=True, http_url=KUPO_HTTP) + chain_context = OgmiosChainContext( + OGMIOS_WS, Network.TESTNET, support_kupo=True, http_url=KUPO_HTTP + ) check_chain_context(chain_context) @@ -219,6 +221,17 @@ def load_or_create_key_pair(base_dir, base_name): assert found_nft, f"Cannot find target NFT in address: {address2}" + # Compare utxo query using Ogmios and Kupo + # Right now, all UTxOs of the address will be returned, which requires Ogmios to validate + # if the UTxOs are spent. This feature is being considered to be added to Kupo to avoid + # extra API calls. See discussion here: https://github.com/CardanoSolutions/kupo/discussions/19. + + utxos2_kupo = self.chain_context.utxos(str(address2), use_kupo=True) + utxos2_ogmios = self.chain_context.utxos(str(address2), use_kupo=False) + + assert utxos2_kupo == utxos2_ogmios + + @retry(tries=2, delay=6) def test_plutus(self): diff --git a/pycardano/backend/ogmios.py b/pycardano/backend/ogmios.py index d22d30eb..c53ff863 100644 --- a/pycardano/backend/ogmios.py +++ b/pycardano/backend/ogmios.py @@ -3,8 +3,8 @@ import time from typing import Dict, List, Union -import websocket import requests +import websocket from pycardano.address import Address from pycardano.backend.base import ChainContext, GenesisParameters, ProtocolParameters @@ -26,7 +26,14 @@ class OgmiosChainContext(ChainContext): - def __init__(self, ws_url: str, network: Network, compact_result=True, support_kupo=False, http_url=None): + def __init__( + self, + ws_url: str, + network: Network, + compact_result=True, + support_kupo=False, + http_url=None, + ): self._ws_url = ws_url self._network = network self._service_name = "ogmios.v1:compact" if compact_result else "ogmios" @@ -36,9 +43,7 @@ def __init__(self, ws_url: str, network: Network, compact_result=True, support_k self._genesis_param = None self._protocol_param = None if self._support_kupo and not self._http_url: - raise Exception( - "Cannot find http url to request from Kupo." - ) + raise Exception("Cannot find http url to request from Kupo.") def _request(self, method: str, args: dict) -> Union[dict, int]: ws = websocket.WebSocket() @@ -168,7 +173,8 @@ def _extract_asset_info(self, asset_hash: str): def _utxos_kupo(self, address: str) -> List[UTxO]: """Get all UTxOs associated with an address with Kupo. - Since UTxO querying will be deprecated from Ogmios in next major release: https://ogmios.dev/mini-protocols/local-state-query/. + Since UTxO querying will be deprecated from Ogmios in next + major release: https://ogmios.dev/mini-protocols/local-state-query/. Args: address (str): An address encoded with bech32. @@ -182,24 +188,24 @@ def _utxos_kupo(self, address: str) -> List[UTxO]: utxos = [] for result in results: - tx_id = result['transaction_id'] - index = result['output_index'] + tx_id = result["transaction_id"] + index = result["output_index"] tx_in = TransactionInput.from_primitive([tx_id, index]) - lovelace_amount = result['value']['coins'] + lovelace_amount = result["value"]["coins"] - datum_hash = result['datum_hash'] + datum_hash = result["datum_hash"] - if not result['value']['assets']: + if not result["value"]["assets"]: tx_out = TransactionOutput( Address.from_primitive(address), amount=lovelace_amount, - datum_hash=datum_hash + datum_hash=datum_hash, ) else: multi_assets = MultiAsset() - for asset, quantity in result['value']['assets'].items(): + for asset, quantity in result["value"]["assets"].items(): policy_hex, policy, asset_name_hex = self._extract_asset_info(asset) multi_assets.setdefault(policy, Asset())[asset_name_hex] = quantity @@ -261,7 +267,6 @@ def _utxos_ogmios(self, address: str) -> List[UTxO]: return utxos - def utxos(self, address: str, use_kupo=False) -> List[UTxO]: """Get all UTxOs associated with an address. @@ -278,7 +283,6 @@ def utxos(self, address: str, use_kupo=False) -> List[UTxO]: return utxos - def submit_tx(self, cbor: Union[bytes, str]): """Submit a transaction to the blockchain. diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 04a98462..830f3bba 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -41,7 +41,7 @@ UTxO, Value, ) -from pycardano.utils import fee, min_lovelace, script_data_hash +from pycardano.utils import fee, max_tx_fee, min_lovelace, script_data_hash from pycardano.witness import TransactionWitnessSet, VerificationKeyWitness __all__ = ["TransactionBuilder"] diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index c43be201..7ca57a55 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -188,11 +188,7 @@ def test_tx_too_big_exception(chain_context): def test_tx_small_utxo_precise_fee(chain_context): -<<<<<<< HEAD - tx_builder = TransactionBuilder(chain_context) -======= tx_builder = TransactionBuilder(chain_context, [RandomImproveMultiAsset([0, 0])]) ->>>>>>> 7ac73501ff3093b1b467b51c685ded3336cdc526 sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" sender_address = Address.from_primitive(sender) @@ -221,11 +217,7 @@ def test_tx_small_utxo_precise_fee(chain_context): 2: 165413, } -<<<<<<< HEAD - expect == tx_body.to_primitive() -======= assert expect == tx_body.to_primitive() ->>>>>>> 7ac73501ff3093b1b467b51c685ded3336cdc526 def test_tx_small_utxo_balance_fail(chain_context): From d514d3daf34544c59451a367db03ac31fe04f956 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Tue, 17 May 2022 22:26:17 -0400 Subject: [PATCH 17/36] reformat --- integration-test/test/test_all.py | 1 - 1 file changed, 1 deletion(-) diff --git a/integration-test/test/test_all.py b/integration-test/test/test_all.py index 806f02fa..66c0b822 100644 --- a/integration-test/test/test_all.py +++ b/integration-test/test/test_all.py @@ -231,7 +231,6 @@ def load_or_create_key_pair(base_dir, base_name): assert utxos2_kupo == utxos2_ogmios - @retry(tries=2, delay=6) def test_plutus(self): From 7122dcbfa7dffd9881bc67b037bc1f4f679e3bfb Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Wed, 25 May 2022 22:25:56 -0400 Subject: [PATCH 18/36] query utxo with Kupo when kupo url is provided --- integration-test/test/test_all.py | 31 +++++------ pycardano/backend/ogmios.py | 91 ++++++++++++++++++++----------- 2 files changed, 74 insertions(+), 48 deletions(-) diff --git a/integration-test/test/test_all.py b/integration-test/test/test_all.py index 66c0b822..af288e0d 100644 --- a/integration-test/test/test_all.py +++ b/integration-test/test/test_all.py @@ -22,11 +22,9 @@ class TestAll: OGMIOS_WS = "ws://localhost:1337" - KUPO_HTTP = "http://localhost:1442/v1/matches" + KUPO_URL = "http://localhost:1442/v1/matches" - chain_context = OgmiosChainContext( - OGMIOS_WS, Network.TESTNET, support_kupo=True, http_url=KUPO_HTTP - ) + chain_context = OgmiosChainContext(OGMIOS_WS, Network.TESTNET, kupo_url=KUPO_URL) check_chain_context(chain_context) @@ -156,6 +154,15 @@ def load_or_create_key_pair(base_dir, base_name): nft_output = TransactionOutput(address, Value(min_val, my_nft)) builder.add_output(nft_output) + # Pass in genesis utxo for Kupo to start syncing transactions + utxo_init = UTxO( + TransactionInput.from_primitive( + ["732bfd67e66be8e8288349fcaaa2294973ef6271cc189a239bb431275401b8e5", 0] + ), + TransactionOutput(address, 900000000000, None), + ) + builder.add_input(utxo_init) + # Build and sign transaction signed_tx = builder.build_and_sign( [self.payment_skey, self.extended_payment_skey, policy_skey], address @@ -171,7 +178,7 @@ def load_or_create_key_pair(base_dir, base_name): time.sleep(3) - utxos = self.chain_context.utxos(str(address), use_kupo=True) + utxos = self.chain_context.utxos(str(address)) found_nft = False for utxo in utxos: @@ -211,7 +218,7 @@ def load_or_create_key_pair(base_dir, base_name): time.sleep(3) - utxos = self.chain_context.utxos(str(address2), use_kupo=True) + utxos = self.chain_context.utxos(str(address2)) found_nft = False for utxo in utxos: @@ -221,22 +228,12 @@ def load_or_create_key_pair(base_dir, base_name): assert found_nft, f"Cannot find target NFT in address: {address2}" - # Compare utxo query using Ogmios and Kupo - # Right now, all UTxOs of the address will be returned, which requires Ogmios to validate - # if the UTxOs are spent. This feature is being considered to be added to Kupo to avoid - # extra API calls. See discussion here: https://github.com/CardanoSolutions/kupo/discussions/19. - - utxos2_kupo = self.chain_context.utxos(str(address2), use_kupo=True) - utxos2_ogmios = self.chain_context.utxos(str(address2), use_kupo=False) - - assert utxos2_kupo == utxos2_ogmios - @retry(tries=2, delay=6) def test_plutus(self): # ----------- Giver give --------------- - with open("plutus_scripts/fortytwo.plutus", "r") as f: + with open("./plutus_scripts/fortytwo.plutus", "r") as f: script_hex = f.read() forty_two_script = cbor2.loads(bytes.fromhex(script_hex)) diff --git a/pycardano/backend/ogmios.py b/pycardano/backend/ogmios.py index c53ff863..4c72e979 100644 --- a/pycardano/backend/ogmios.py +++ b/pycardano/backend/ogmios.py @@ -31,19 +31,15 @@ def __init__( ws_url: str, network: Network, compact_result=True, - support_kupo=False, - http_url=None, + kupo_url=None, ): self._ws_url = ws_url self._network = network self._service_name = "ogmios.v1:compact" if compact_result else "ogmios" - self._support_kupo = support_kupo - self._http_url = http_url if support_kupo else None + self._kupo_url = kupo_url self._last_known_block_slot = 0 self._genesis_param = None self._protocol_param = None - if self._support_kupo and not self._http_url: - raise Exception("Cannot find http url to request from Kupo.") def _request(self, method: str, args: dict) -> Union[dict, int]: ws = websocket.WebSocket() @@ -171,6 +167,23 @@ def _extract_asset_info(self, asset_hash: str): return policy_hex, policy, asset_name_hex + def _check_utxo_unspent(self, tx_id: str, index: int) -> bool: + """Check whether an UTxO is unspent with Ogmios. + + Args: + tx_id (str): transaction id. + index (int): transaction index. + """ + + method = "Query" + args = {"query": {"utxo": [{"txId": tx_id, "index": index}]}} + results = self._request(method, args) + + if results: + return True + else: + return False + def _utxos_kupo(self, address: str) -> List[UTxO]: """Get all UTxOs associated with an address with Kupo. Since UTxO querying will be deprecated from Ogmios in next @@ -182,7 +195,7 @@ def _utxos_kupo(self, address: str) -> List[UTxO]: Returns: List[UTxO]: A list of UTxOs. """ - address_url = self._http_url + "/" + address + address_url = self._kupo_url + "/" + address results = requests.get(address_url).json() utxos = [] @@ -190,31 +203,47 @@ def _utxos_kupo(self, address: str) -> List[UTxO]: for result in results: tx_id = result["transaction_id"] index = result["output_index"] - tx_in = TransactionInput.from_primitive([tx_id, index]) - lovelace_amount = result["value"]["coins"] + # Right now, all UTxOs of the address will be returned with Kupo, which requires Ogmios to + # validate if the UTxOs are spent with output reference. This feature is being considered to + # be added to Kupo to avoid extra API calls. + # See discussion here: https://github.com/CardanoSolutions/kupo/discussions/19. + if self._check_utxo_unspent(tx_id, index): + tx_in = TransactionInput.from_primitive([tx_id, index]) - datum_hash = result["datum_hash"] + lovelace_amount = result["value"]["coins"] - if not result["value"]["assets"]: - tx_out = TransactionOutput( - Address.from_primitive(address), - amount=lovelace_amount, - datum_hash=datum_hash, + datum_hash = ( + DatumHash.from_primitive(result["datum_hash"]) + if result["datum_hash"] + else None ) - else: - multi_assets = MultiAsset() - - for asset, quantity in result["value"]["assets"].items(): - policy_hex, policy, asset_name_hex = self._extract_asset_info(asset) - multi_assets.setdefault(policy, Asset())[asset_name_hex] = quantity - tx_out = TransactionOutput( - Address.from_primitive(address), - amount=Value(lovelace_amount, multi_assets), - datum_hash=datum_hash, - ) - utxos.append(UTxO(tx_in, tx_out)) + if not result["value"]["assets"]: + tx_out = TransactionOutput( + Address.from_primitive(address), + amount=lovelace_amount, + datum_hash=datum_hash, + ) + else: + multi_assets = MultiAsset() + + for asset, quantity in result["value"]["assets"].items(): + policy_hex, policy, asset_name_hex = self._extract_asset_info( + asset + ) + multi_assets.setdefault(policy, Asset())[ + asset_name_hex + ] = quantity + + tx_out = TransactionOutput( + Address.from_primitive(address), + amount=Value(lovelace_amount, multi_assets), + datum_hash=datum_hash, + ) + utxos.append(UTxO(tx_in, tx_out)) + else: + continue return utxos @@ -267,7 +296,7 @@ def _utxos_ogmios(self, address: str) -> List[UTxO]: return utxos - def utxos(self, address: str, use_kupo=False) -> List[UTxO]: + def utxos(self, address: str) -> List[UTxO]: """Get all UTxOs associated with an address. Args: @@ -276,10 +305,10 @@ def utxos(self, address: str, use_kupo=False) -> List[UTxO]: Returns: List[UTxO]: A list of UTxOs. """ - if not use_kupo: - utxos = self._utxos_ogmios(address) - else: + if self._kupo_url: utxos = self._utxos_kupo(address) + else: + utxos = self._utxos_ogmios(address) return utxos From 902d41006a9810e2e2f0d0f995c4e1d6ffcccaf0 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Thu, 26 May 2022 21:30:13 -0400 Subject: [PATCH 19/36] remove the hardcoded genesis utxo in test_all file --- integration-test/test/test_all.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/integration-test/test/test_all.py b/integration-test/test/test_all.py index f6da0caa..eb2f449c 100644 --- a/integration-test/test/test_all.py +++ b/integration-test/test/test_all.py @@ -170,15 +170,6 @@ def load_or_create_key_pair(base_dir, base_name): nft_output = TransactionOutput(address, Value(min_val, my_nft)) builder.add_output(nft_output) - # Pass in genesis utxo for Kupo to start syncing transactions - utxo_init = UTxO( - TransactionInput.from_primitive( - ["732bfd67e66be8e8288349fcaaa2294973ef6271cc189a239bb431275401b8e5", 0] - ), - TransactionOutput(address, 900000000000, None), - ) - builder.add_input(utxo_init) - # Build and sign transaction signed_tx = builder.build_and_sign( [self.payment_skey, self.extended_payment_skey, policy_skey], address @@ -194,12 +185,8 @@ def load_or_create_key_pair(base_dir, base_name): self.assert_output(address, nft_output) - # Generate another set of keys and address to send NFT to - skey2, vkey2 = load_or_create_key_pair(key_dir, "keys2") - address2 = Address(vkey2.hash(), network=self.NETWORK) - nft_to_send = TransactionOutput( - address2, + address, Value( 20000000, MultiAsset.from_primitive({policy_id.payload: {b"MY_NFT_1": 1}}), @@ -222,7 +209,7 @@ def load_or_create_key_pair(base_dir, base_name): print("############### Submitting transaction ###############") self.chain_context.submit_tx(signed_tx.to_cbor()) - self.assert_output(address2, nft_to_send) + self.assert_output(address, nft_to_send) @retry(tries=4, delay=6, backoff=2, jitter=(1, 3)) def test_plutus(self): From d5b857a0f95a10bf4dd6f30873c6d79fed4c28a7 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sun, 5 Jun 2022 23:58:20 -0400 Subject: [PATCH 20/36] Create usage guides for Plutus and fix hyperlinks --- Makefile | 1 - docs/source/guides/plutus.rst | 124 +++++++++++++++++++++++++++++ docs/source/guides/transaction.rst | 2 +- docs/source/index.rst | 1 + 4 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 docs/source/guides/plutus.rst diff --git a/Makefile b/Makefile index ba5fee89..f8f1a424 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,6 @@ format: ## runs code style and formatter docs: ## build the documentation poetry export --dev --without-hashes > docs/requirements.txt - rm -r docs/build poetry run sphinx-build docs/source docs/build/html $(BROWSER) docs/build/html/index.html diff --git a/docs/source/guides/plutus.rst b/docs/source/guides/plutus.rst new file mode 100644 index 00000000..5a898460 --- /dev/null +++ b/docs/source/guides/plutus.rst @@ -0,0 +1,124 @@ +====== +Plutus +====== + +Plutus is the native language to write smart contract on Cardano's extended UTxO model (EUTxO). It allows us to incorporate expressive logics to determine when a particular UTxO can be spent. +To learn more about EUTxO and its advantages, you can refer to the `Cardano docs `_ or the `class notes `_ from Plutus pioneer program (PPP). +To learn how Plutus enables logic creation, we need to understand a couple key concepts: + +* **Plutus script**: the smart contract that acts as the validator of the transaction. By evaluating the inputs from someone who wants to spend the UTxO, they either approve or deny it (by returning either True or False). The script is compiled into Plutus Core binary and sits on-chain. +* **Script address**: the hash of the Plutus script binary. They hold UTxOs like typical public key address, but every time a transaction tries to consume the UTxOs on this address, the Plutus script generated this address will be executed by evaluating the input of the transaction, namely datum, redeemer and script context. The transaction is only valid if the script returns True. +* **Datum**: the datum is a piece of information associated with a UTxO. When someone sends fund to script address, he or she attaches the hash of the datum to "lock" the fund. When someone tries to consume the UTxO, he or she needs to provide datum whose hash matches the attached datum hash and redeemer that meets the conditions specified by the Plutus script to "unlock" the fund. +* **Redeemer**: the redeemer shares the same data format as datum, but is a separate input. Redeemer value is attached to the input transaction to unlock funds from a script and is used by the script to validate the transaction. +* **Script context**: The script context provides information about the pending transaction, along with which input triggered the validation. + +------------------ +Example - FortyTwo +------------------ + +We demonstrate how these concepts come into play using a simple example from PPP - FortyTwo. The original script in haskell can be found here `here `_. Using PyCardano, we will show one can send and lock funds at a script address, and how someone else with the correct redeemer value can unlock and receive the funds. + +Step 1 + +Similar to `Transaction guide <../guides/transaction.html>`_, we build a chain context using `BlockFrostChainContext <../api/pycardano.backend.base.html#pycardano.backend.blockfrost.BlockFrostChainContext>`_:: + + >>> from pycardano import BlockFrostChainContext, Network + >>> network = Network.TESTNET + >>> context = BlockFrostChainContext("your_blockfrost_project_id", network) + +Step 2 + +Create script address:: + + >>> import cbor2 + >>> from pycardano import ( + ... Address, + ... PaymentVerificationKey, + ... PaymentSigningKey, + ... plutus_script_hash, + ... Transaction, + ... TransactionBuilder, + ... PlutusData, + ... Redeemer, + ... ) + + >>> # Assuming the hexadecimal file of the script exists at your local path + >>> with open("path/to/fortytwo.plutus", "r") as f: + >>> script_hex = f.read() + >>> forty_two_script = cbor2.loads(bytes.fromhex(script_hex)) + + >>> script_hash = plutus_script_hash(forty_two_script) + >>> script_address = Address(script_hash, network=network) + +Step 3 + +Giver/Locker sends funds to script address:: + + >>> payment_vkey = PaymentVerificationKey.load("path/to/payment.vkey") + >>> payment_skey = PaymentSigningKey.load("path/to/payment.skey") + >>> giver_address = Address(payment_vkey.hash(), network=network) + + >>> builder = TransactionBuilder(context) + >>> builder.add_input_address(giver_address) + + >>> datum = PlutusData() # A Unit type "()" in Haskell + >>> builder.add_output( + >>> TransactionOutput(script_address, 50000000, datum_hash=datum_hash(datum)) + >>> ) + +Build, sign and submit the transaction: + + >>> signed_tx = builder.build_and_sign([payment_skey], giver_address) + >>> context.submit_tx(signed_tx.to_cbor()) + +`PlutusData` helper class that can serialize itself into a CBOR format, which could be intepreted as a data structure in Plutus scripts. Wrapping datum in PlutusData class will reduce the complexity of serialization and deserialization tremendously. It supports data type of int, bytes, List and hashmap. For our example, we leave it empty. But to construct any arbitrary datum, we can do the following:: + + >>> # Create sample datum + >>> @dataclass + ... class Test(PlutusData): + ... CONSTR_ID = 1 + ... a: int + ... b: bytes + + >>> test = Test(123, b"321") + >>> test.to_cbor() + 'd87a9f187b43333231ff' + >>> assert test == Test.from_cbor("d87a9f187b43333231ff") + True + +Step 4 + +Taker/Unlocker sends transaction to consume funds. Here we specify the redeemer tag as spend and pass in the redeemer value of 42. If the redeemer value is anything else, the validator will fail and funds won't be retrieved:: + + >>> redeemer = Redeemer(RedeemerTag.SPEND, 42) + + >>> utxo_to_spend = context.utxos(str(script_address))[0] + >>> extended_payment_vkey = PaymentVerificationKey.load("path/to/extended_payment.vkey") + >>> extended_payment_skey = PaymentSigningKey.load("path/to/extended_payment.skey") + >>> taker_address = Address(extended_payment_vkey.hash(), network=network) + + >>> builder = TransactionBuilder(context) + +Add info on the UTxO to spend, Plutus script, actual datum and the redeemer. Specify funds amount to take:: + + >>> builder.add_script_input(utxo_to_spend, forty_two_script, datum, redeemer) + >>> take_output = TransactionOutput(taker_address, 25123456) + >>> builder.add_output(take_output) + +Taker/Unlocker provides collateral. Collateral has been introduced in Alonzo transactions to cover the cost of the validating node executing a failing script. In this scenario, the provided UTXO is consumed instead of the fees. A UTXO provided for collateral must only have ada, no other native assets:: + + >>> non_nft_utxo = None + >>> for utxo in context.utxos(str(taker_address)): + >>> # multi_asset should be empty for collateral utxo + >>> if not utxo.output.amount.multi_asset: + >>> non_nft_utxo = utxo + >>> break + + >>> builder.collaterals.append(non_nft_utxo) + + >>> signed_tx = builder.build_and_sign([self.extended_payment_skey], taker_address) + + >>> chain_context.submit_tx(signed_tx.to_cbor()) + +The funds locked in script address is successfully retrieved to the taker address. + diff --git a/docs/source/guides/transaction.rst b/docs/source/guides/transaction.rst index 057ff124..193141a6 100644 --- a/docs/source/guides/transaction.rst +++ b/docs/source/guides/transaction.rst @@ -103,7 +103,7 @@ Step 1 To use a transaction builder, we first need to create a chain context, so the builder can read protocol parameters and search proper transaction inputs to use. Currently, the available chain context is -`BlockFrostChainContext `_ :: +`BlockFrostChainContext <../api/pycardano.backend.base.html#pycardano.backend.blockfrost.BlockFrostChainContext>`_ :: >>> from pycardano import BlockFrostChainContext, Network >>> network = Network.TESTNET diff --git a/docs/source/index.rst b/docs/source/index.rst index 810f735b..9b4d3005 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -24,6 +24,7 @@ making it a light-weight library that is easy and fast to set up in all kinds of guides/serialization guides/instance_creation guides/transaction + guides/plutus .. toctree:: From 8af6486864575ee1912de8589380fec76a2ce835 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Mon, 13 Jun 2022 21:32:43 -0400 Subject: [PATCH 21/36] create separate section for datum/redeemer serialiation --- docs/source/guides/plutus.rst | 71 +++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/docs/source/guides/plutus.rst b/docs/source/guides/plutus.rst index 5a898460..e8a55025 100644 --- a/docs/source/guides/plutus.rst +++ b/docs/source/guides/plutus.rst @@ -9,9 +9,63 @@ To learn how Plutus enables logic creation, we need to understand a couple key c * **Plutus script**: the smart contract that acts as the validator of the transaction. By evaluating the inputs from someone who wants to spend the UTxO, they either approve or deny it (by returning either True or False). The script is compiled into Plutus Core binary and sits on-chain. * **Script address**: the hash of the Plutus script binary. They hold UTxOs like typical public key address, but every time a transaction tries to consume the UTxOs on this address, the Plutus script generated this address will be executed by evaluating the input of the transaction, namely datum, redeemer and script context. The transaction is only valid if the script returns True. * **Datum**: the datum is a piece of information associated with a UTxO. When someone sends fund to script address, he or she attaches the hash of the datum to "lock" the fund. When someone tries to consume the UTxO, he or she needs to provide datum whose hash matches the attached datum hash and redeemer that meets the conditions specified by the Plutus script to "unlock" the fund. -* **Redeemer**: the redeemer shares the same data format as datum, but is a separate input. Redeemer value is attached to the input transaction to unlock funds from a script and is used by the script to validate the transaction. +* **Redeemer**: the redeemer shares the same data format as datum, but is a separate input. It includes datum, along with other information such as types of actions to take with the target UTxO and computational resources to reserve. Redeemer value is attached to the input transaction to unlock funds from a script and is used by the script to validate the transaction. * **Script context**: The script context provides information about the pending transaction, along with which input triggered the validation. +-------------------------------- +Datum and Redeemer Serialization +-------------------------------- +To calculate the hash of a datum, we can leverage the helper class `PlutusData`. `PlutusData` can serialize itself into a CBOR format, which can be interpreted as a data structure in Plutus scripts. Wrapping datum in PlutusData class will reduce the complexity of serialization and deserialization tremendously. It supports data type of int, bytes, List and hashmap. Below are some examples on how to construct some arbitrary datums. + +Empty datum:: + + >>> empty_datum = PlutusData() + >>> empty_datum.to_cbor() + 'd87980' + +Sample datum with int, bytes, List and hashmap inputs:: + + >>> # Create sample datum + >>> @dataclass + ... class MyDatum(PlutusData): + ... CONSTR_ID = 1 + ... a: int + ... b: bytes + ... c: IndefiniteList + ... d: dict + + >>> datum = MyDatum(123, b"1234", IndefiniteList([4, 5, 6]), {1: b"1", 2: b"2"}) + >>> datum.to_cbor() + 'd87a9f187b43333231ff' + +You can also wrap `PlutusData` within `PlutusData`:: + + >>> @dataclass + ... class InclusionDatum(PlutusData): + ... CONSTR_ID = 1 + ... beneficiary: bytes + ... deadline: int + ... other_data: MyDatum + + >>> key_hash = bytes.fromhex("c2ff616e11299d9094ce0a7eb5b7284b705147a822f4ffbd471f971a") + >>> deadline = 1643235300000 + >>> other_datum = MyDatum(123, b"1234", IndefiniteList([4, 5, 6]), {1: b"1", 2: b"2"}) + >>> include_datum = InclusionDatum(key_hash, deadline, other_datum) + >>> include_datum.to_cbor() + 'd87a9f581cc2ff616e11299d9094ce0a7eb5b7284b705147a822f4ffbd471f971a1b0000017e9874d2a0d8668218829f187b44313233349f040506ffa2014131024132ffff' + +`PlutusData` supports conversion from/to JSON format, which +is easier to read and write. The above could be convered to JSON like this:: + + >>> encoded_json = include_datum.to_json(separators=(",", ":") + +Similarly, redeemer can be serialized like following:: + + >>> data = MyDatum(123, b"234", IndefiniteList([]), {1: b"1", 2: b"2"}) + >>> redeemer = MyRedeemer(RedeemerTag.SPEND, data, ExecutionUnits(1000000, 1000000)) + >>> redeemer.to_cbor() + '840000d8668218829f187b433233349fffa2014131024132ff821a000f42401a000f4240' + ------------------ Example - FortyTwo ------------------ @@ -71,21 +125,6 @@ Build, sign and submit the transaction: >>> signed_tx = builder.build_and_sign([payment_skey], giver_address) >>> context.submit_tx(signed_tx.to_cbor()) -`PlutusData` helper class that can serialize itself into a CBOR format, which could be intepreted as a data structure in Plutus scripts. Wrapping datum in PlutusData class will reduce the complexity of serialization and deserialization tremendously. It supports data type of int, bytes, List and hashmap. For our example, we leave it empty. But to construct any arbitrary datum, we can do the following:: - - >>> # Create sample datum - >>> @dataclass - ... class Test(PlutusData): - ... CONSTR_ID = 1 - ... a: int - ... b: bytes - - >>> test = Test(123, b"321") - >>> test.to_cbor() - 'd87a9f187b43333231ff' - >>> assert test == Test.from_cbor("d87a9f187b43333231ff") - True - Step 4 Taker/Unlocker sends transaction to consume funds. Here we specify the redeemer tag as spend and pass in the redeemer value of 42. If the redeemer value is anything else, the validator will fail and funds won't be retrieved:: From 4a12d13c5cec1f555c473e7f5863a175666b49bc Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Mon, 13 Jun 2022 21:41:43 -0400 Subject: [PATCH 22/36] add -f flag to try to clean up the build if it exists --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index f8f1a424..09779caa 100644 --- a/Makefile +++ b/Makefile @@ -66,6 +66,7 @@ format: ## runs code style and formatter docs: ## build the documentation poetry export --dev --without-hashes > docs/requirements.txt + rm -r -f docs/build poetry run sphinx-build docs/source docs/build/html $(BROWSER) docs/build/html/index.html From 5c2c02529564d45b7ccea785e787374e705195ae Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sun, 5 Jun 2022 23:58:20 -0400 Subject: [PATCH 23/36] Create usage guides for Plutus and fix hyperlinks --- Makefile | 1 - docs/source/guides/plutus.rst | 124 +++++++++++++++++++++++++++++ docs/source/guides/transaction.rst | 2 +- docs/source/index.rst | 1 + 4 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 docs/source/guides/plutus.rst diff --git a/Makefile b/Makefile index ba5fee89..f8f1a424 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,6 @@ format: ## runs code style and formatter docs: ## build the documentation poetry export --dev --without-hashes > docs/requirements.txt - rm -r docs/build poetry run sphinx-build docs/source docs/build/html $(BROWSER) docs/build/html/index.html diff --git a/docs/source/guides/plutus.rst b/docs/source/guides/plutus.rst new file mode 100644 index 00000000..5a898460 --- /dev/null +++ b/docs/source/guides/plutus.rst @@ -0,0 +1,124 @@ +====== +Plutus +====== + +Plutus is the native language to write smart contract on Cardano's extended UTxO model (EUTxO). It allows us to incorporate expressive logics to determine when a particular UTxO can be spent. +To learn more about EUTxO and its advantages, you can refer to the `Cardano docs `_ or the `class notes `_ from Plutus pioneer program (PPP). +To learn how Plutus enables logic creation, we need to understand a couple key concepts: + +* **Plutus script**: the smart contract that acts as the validator of the transaction. By evaluating the inputs from someone who wants to spend the UTxO, they either approve or deny it (by returning either True or False). The script is compiled into Plutus Core binary and sits on-chain. +* **Script address**: the hash of the Plutus script binary. They hold UTxOs like typical public key address, but every time a transaction tries to consume the UTxOs on this address, the Plutus script generated this address will be executed by evaluating the input of the transaction, namely datum, redeemer and script context. The transaction is only valid if the script returns True. +* **Datum**: the datum is a piece of information associated with a UTxO. When someone sends fund to script address, he or she attaches the hash of the datum to "lock" the fund. When someone tries to consume the UTxO, he or she needs to provide datum whose hash matches the attached datum hash and redeemer that meets the conditions specified by the Plutus script to "unlock" the fund. +* **Redeemer**: the redeemer shares the same data format as datum, but is a separate input. Redeemer value is attached to the input transaction to unlock funds from a script and is used by the script to validate the transaction. +* **Script context**: The script context provides information about the pending transaction, along with which input triggered the validation. + +------------------ +Example - FortyTwo +------------------ + +We demonstrate how these concepts come into play using a simple example from PPP - FortyTwo. The original script in haskell can be found here `here `_. Using PyCardano, we will show one can send and lock funds at a script address, and how someone else with the correct redeemer value can unlock and receive the funds. + +Step 1 + +Similar to `Transaction guide <../guides/transaction.html>`_, we build a chain context using `BlockFrostChainContext <../api/pycardano.backend.base.html#pycardano.backend.blockfrost.BlockFrostChainContext>`_:: + + >>> from pycardano import BlockFrostChainContext, Network + >>> network = Network.TESTNET + >>> context = BlockFrostChainContext("your_blockfrost_project_id", network) + +Step 2 + +Create script address:: + + >>> import cbor2 + >>> from pycardano import ( + ... Address, + ... PaymentVerificationKey, + ... PaymentSigningKey, + ... plutus_script_hash, + ... Transaction, + ... TransactionBuilder, + ... PlutusData, + ... Redeemer, + ... ) + + >>> # Assuming the hexadecimal file of the script exists at your local path + >>> with open("path/to/fortytwo.plutus", "r") as f: + >>> script_hex = f.read() + >>> forty_two_script = cbor2.loads(bytes.fromhex(script_hex)) + + >>> script_hash = plutus_script_hash(forty_two_script) + >>> script_address = Address(script_hash, network=network) + +Step 3 + +Giver/Locker sends funds to script address:: + + >>> payment_vkey = PaymentVerificationKey.load("path/to/payment.vkey") + >>> payment_skey = PaymentSigningKey.load("path/to/payment.skey") + >>> giver_address = Address(payment_vkey.hash(), network=network) + + >>> builder = TransactionBuilder(context) + >>> builder.add_input_address(giver_address) + + >>> datum = PlutusData() # A Unit type "()" in Haskell + >>> builder.add_output( + >>> TransactionOutput(script_address, 50000000, datum_hash=datum_hash(datum)) + >>> ) + +Build, sign and submit the transaction: + + >>> signed_tx = builder.build_and_sign([payment_skey], giver_address) + >>> context.submit_tx(signed_tx.to_cbor()) + +`PlutusData` helper class that can serialize itself into a CBOR format, which could be intepreted as a data structure in Plutus scripts. Wrapping datum in PlutusData class will reduce the complexity of serialization and deserialization tremendously. It supports data type of int, bytes, List and hashmap. For our example, we leave it empty. But to construct any arbitrary datum, we can do the following:: + + >>> # Create sample datum + >>> @dataclass + ... class Test(PlutusData): + ... CONSTR_ID = 1 + ... a: int + ... b: bytes + + >>> test = Test(123, b"321") + >>> test.to_cbor() + 'd87a9f187b43333231ff' + >>> assert test == Test.from_cbor("d87a9f187b43333231ff") + True + +Step 4 + +Taker/Unlocker sends transaction to consume funds. Here we specify the redeemer tag as spend and pass in the redeemer value of 42. If the redeemer value is anything else, the validator will fail and funds won't be retrieved:: + + >>> redeemer = Redeemer(RedeemerTag.SPEND, 42) + + >>> utxo_to_spend = context.utxos(str(script_address))[0] + >>> extended_payment_vkey = PaymentVerificationKey.load("path/to/extended_payment.vkey") + >>> extended_payment_skey = PaymentSigningKey.load("path/to/extended_payment.skey") + >>> taker_address = Address(extended_payment_vkey.hash(), network=network) + + >>> builder = TransactionBuilder(context) + +Add info on the UTxO to spend, Plutus script, actual datum and the redeemer. Specify funds amount to take:: + + >>> builder.add_script_input(utxo_to_spend, forty_two_script, datum, redeemer) + >>> take_output = TransactionOutput(taker_address, 25123456) + >>> builder.add_output(take_output) + +Taker/Unlocker provides collateral. Collateral has been introduced in Alonzo transactions to cover the cost of the validating node executing a failing script. In this scenario, the provided UTXO is consumed instead of the fees. A UTXO provided for collateral must only have ada, no other native assets:: + + >>> non_nft_utxo = None + >>> for utxo in context.utxos(str(taker_address)): + >>> # multi_asset should be empty for collateral utxo + >>> if not utxo.output.amount.multi_asset: + >>> non_nft_utxo = utxo + >>> break + + >>> builder.collaterals.append(non_nft_utxo) + + >>> signed_tx = builder.build_and_sign([self.extended_payment_skey], taker_address) + + >>> chain_context.submit_tx(signed_tx.to_cbor()) + +The funds locked in script address is successfully retrieved to the taker address. + diff --git a/docs/source/guides/transaction.rst b/docs/source/guides/transaction.rst index 057ff124..193141a6 100644 --- a/docs/source/guides/transaction.rst +++ b/docs/source/guides/transaction.rst @@ -103,7 +103,7 @@ Step 1 To use a transaction builder, we first need to create a chain context, so the builder can read protocol parameters and search proper transaction inputs to use. Currently, the available chain context is -`BlockFrostChainContext `_ :: +`BlockFrostChainContext <../api/pycardano.backend.base.html#pycardano.backend.blockfrost.BlockFrostChainContext>`_ :: >>> from pycardano import BlockFrostChainContext, Network >>> network = Network.TESTNET diff --git a/docs/source/index.rst b/docs/source/index.rst index 810f735b..9b4d3005 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -24,6 +24,7 @@ making it a light-weight library that is easy and fast to set up in all kinds of guides/serialization guides/instance_creation guides/transaction + guides/plutus .. toctree:: From bdf88c07af8502f8e0684b9121d52eaa8b9b023a Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Mon, 13 Jun 2022 21:32:43 -0400 Subject: [PATCH 24/36] create separate section for datum/redeemer serialiation --- docs/source/guides/plutus.rst | 71 +++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/docs/source/guides/plutus.rst b/docs/source/guides/plutus.rst index 5a898460..e8a55025 100644 --- a/docs/source/guides/plutus.rst +++ b/docs/source/guides/plutus.rst @@ -9,9 +9,63 @@ To learn how Plutus enables logic creation, we need to understand a couple key c * **Plutus script**: the smart contract that acts as the validator of the transaction. By evaluating the inputs from someone who wants to spend the UTxO, they either approve or deny it (by returning either True or False). The script is compiled into Plutus Core binary and sits on-chain. * **Script address**: the hash of the Plutus script binary. They hold UTxOs like typical public key address, but every time a transaction tries to consume the UTxOs on this address, the Plutus script generated this address will be executed by evaluating the input of the transaction, namely datum, redeemer and script context. The transaction is only valid if the script returns True. * **Datum**: the datum is a piece of information associated with a UTxO. When someone sends fund to script address, he or she attaches the hash of the datum to "lock" the fund. When someone tries to consume the UTxO, he or she needs to provide datum whose hash matches the attached datum hash and redeemer that meets the conditions specified by the Plutus script to "unlock" the fund. -* **Redeemer**: the redeemer shares the same data format as datum, but is a separate input. Redeemer value is attached to the input transaction to unlock funds from a script and is used by the script to validate the transaction. +* **Redeemer**: the redeemer shares the same data format as datum, but is a separate input. It includes datum, along with other information such as types of actions to take with the target UTxO and computational resources to reserve. Redeemer value is attached to the input transaction to unlock funds from a script and is used by the script to validate the transaction. * **Script context**: The script context provides information about the pending transaction, along with which input triggered the validation. +-------------------------------- +Datum and Redeemer Serialization +-------------------------------- +To calculate the hash of a datum, we can leverage the helper class `PlutusData`. `PlutusData` can serialize itself into a CBOR format, which can be interpreted as a data structure in Plutus scripts. Wrapping datum in PlutusData class will reduce the complexity of serialization and deserialization tremendously. It supports data type of int, bytes, List and hashmap. Below are some examples on how to construct some arbitrary datums. + +Empty datum:: + + >>> empty_datum = PlutusData() + >>> empty_datum.to_cbor() + 'd87980' + +Sample datum with int, bytes, List and hashmap inputs:: + + >>> # Create sample datum + >>> @dataclass + ... class MyDatum(PlutusData): + ... CONSTR_ID = 1 + ... a: int + ... b: bytes + ... c: IndefiniteList + ... d: dict + + >>> datum = MyDatum(123, b"1234", IndefiniteList([4, 5, 6]), {1: b"1", 2: b"2"}) + >>> datum.to_cbor() + 'd87a9f187b43333231ff' + +You can also wrap `PlutusData` within `PlutusData`:: + + >>> @dataclass + ... class InclusionDatum(PlutusData): + ... CONSTR_ID = 1 + ... beneficiary: bytes + ... deadline: int + ... other_data: MyDatum + + >>> key_hash = bytes.fromhex("c2ff616e11299d9094ce0a7eb5b7284b705147a822f4ffbd471f971a") + >>> deadline = 1643235300000 + >>> other_datum = MyDatum(123, b"1234", IndefiniteList([4, 5, 6]), {1: b"1", 2: b"2"}) + >>> include_datum = InclusionDatum(key_hash, deadline, other_datum) + >>> include_datum.to_cbor() + 'd87a9f581cc2ff616e11299d9094ce0a7eb5b7284b705147a822f4ffbd471f971a1b0000017e9874d2a0d8668218829f187b44313233349f040506ffa2014131024132ffff' + +`PlutusData` supports conversion from/to JSON format, which +is easier to read and write. The above could be convered to JSON like this:: + + >>> encoded_json = include_datum.to_json(separators=(",", ":") + +Similarly, redeemer can be serialized like following:: + + >>> data = MyDatum(123, b"234", IndefiniteList([]), {1: b"1", 2: b"2"}) + >>> redeemer = MyRedeemer(RedeemerTag.SPEND, data, ExecutionUnits(1000000, 1000000)) + >>> redeemer.to_cbor() + '840000d8668218829f187b433233349fffa2014131024132ff821a000f42401a000f4240' + ------------------ Example - FortyTwo ------------------ @@ -71,21 +125,6 @@ Build, sign and submit the transaction: >>> signed_tx = builder.build_and_sign([payment_skey], giver_address) >>> context.submit_tx(signed_tx.to_cbor()) -`PlutusData` helper class that can serialize itself into a CBOR format, which could be intepreted as a data structure in Plutus scripts. Wrapping datum in PlutusData class will reduce the complexity of serialization and deserialization tremendously. It supports data type of int, bytes, List and hashmap. For our example, we leave it empty. But to construct any arbitrary datum, we can do the following:: - - >>> # Create sample datum - >>> @dataclass - ... class Test(PlutusData): - ... CONSTR_ID = 1 - ... a: int - ... b: bytes - - >>> test = Test(123, b"321") - >>> test.to_cbor() - 'd87a9f187b43333231ff' - >>> assert test == Test.from_cbor("d87a9f187b43333231ff") - True - Step 4 Taker/Unlocker sends transaction to consume funds. Here we specify the redeemer tag as spend and pass in the redeemer value of 42. If the redeemer value is anything else, the validator will fail and funds won't be retrieved:: From 7840deaf7d2a00a82d747567fe6535b73a1ed6ce Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Mon, 13 Jun 2022 21:41:43 -0400 Subject: [PATCH 25/36] add -f flag to try to clean up the build if it exists --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index f8f1a424..09779caa 100644 --- a/Makefile +++ b/Makefile @@ -66,6 +66,7 @@ format: ## runs code style and formatter docs: ## build the documentation poetry export --dev --without-hashes > docs/requirements.txt + rm -r -f docs/build poetry run sphinx-build docs/source docs/build/html $(BROWSER) docs/build/html/index.html From ee4d3306a5b8797d5c976638390848ac2dddcb91 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sat, 17 Sep 2022 19:05:15 -0400 Subject: [PATCH 26/36] hdwallet implementation and unit test --- poetry.lock | 627 +++++--------------------- pycardano/hdwallet.py | 485 ++++++++++++++++++++ pyproject.toml | 2 + test/pycardano/backend/test_ogmios.py | 2 +- test/pycardano/test_hdwallet.py | 68 +++ test/pycardano/test_nativescript.py | 2 +- test/pycardano/test_util.py | 5 +- 7 files changed, 666 insertions(+), 525 deletions(-) create mode 100644 pycardano/hdwallet.py create mode 100644 test/pycardano/test_hdwallet.py diff --git a/poetry.lock b/poetry.lock index b627765c..32daa3ff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -172,14 +172,14 @@ ecdsa = "*" [[package]] name = "coverage" -version = "6.4" +version = "6.4.4" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -tomli = {version = "*", optional = true, markers = "python_version < \"3.11\" and extra == \"toml\""} +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] @@ -234,6 +234,14 @@ six = ">=1.9.0" gmpy = ["gmpy"] gmpy2 = ["gmpy2"] +[[package]] +name = "ecpy" +version = "1.2.5" +description = "Pure Pyhton Elliptic Curve Library" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "execnet" version = "1.9.0" @@ -370,6 +378,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "mnemonic" +version = "0.20" +description = "Implementation of Bitcoin BIP-0039" +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "mypy-extensions" version = "0.4.3" @@ -432,8 +448,8 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["pytest-benchmark", "pytest"] +dev = ["tox", "pre-commit"] [[package]] name = "py" @@ -536,7 +552,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] [[package]] name = "pytest-forked" @@ -665,8 +681,8 @@ python-versions = ">=3.6" sphinx = ">=1.8" [package.extras] +rtd = ["sphinx-book-theme", "myst-nb", "ipython", "sphinx"] code_style = ["pre-commit (==2.12.1)"] -rtd = ["sphinx", "ipython", "myst-nb", "sphinx-book-theme"] [[package]] name = "sphinx-rtd-theme" @@ -681,7 +697,7 @@ docutils = "<0.18" sphinx = ">=1.6" [package.extras] -dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] +dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] [[package]] name = "sphinxcontrib-applehelp" @@ -692,8 +708,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-devhelp" @@ -704,8 +720,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-htmlhelp" @@ -716,8 +732,8 @@ optional = false python-versions = ">=3.6" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest", "html5lib"] +test = ["html5lib", "pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-jsmath" @@ -728,7 +744,7 @@ optional = false python-versions = ">=3.5" [package.extras] -test = ["pytest", "flake8", "mypy"] +test = ["mypy", "flake8", "pytest"] [[package]] name = "sphinxcontrib-qthelp" @@ -739,8 +755,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-serializinghtml" @@ -812,9 +828,9 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] -optional = ["python-socks", "wsaccel"] test = ["websockets"] +optional = ["wsaccel", "python-socks"] +docs = ["sphinx-rtd-theme (>=0.5)", "Sphinx (>=3.4)"] [[package]] name = "werkzeug" @@ -842,512 +858,79 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "745272095bbbeed88ed661cbb599584a6a4440b4d84d80247aad39f0dd1f5c66" +content-hash = "fc594a913a31e56985043766c99a9f125ef306e0ad06d271805d16c8680550d1" [metadata.files] -alabaster = [ - {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, - {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, -] -asn1crypto = [ - {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, - {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] -babel = [ - {file = "Babel-2.10.1-py3-none-any.whl", hash = "sha256:3f349e85ad3154559ac4930c3918247d319f21910d5ce4b25d439ed8693b98d2"}, - {file = "Babel-2.10.1.tar.gz", hash = "sha256:98aeaca086133efb3e1e2aad0396987490c8425929ddbcfe0550184fdc54cd13"}, -] -black = [ - {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, - {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, - {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, - {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, - {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, - {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, - {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, - {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, - {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, - {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, - {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, - {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, - {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, - {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, - {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, - {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, - {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, - {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, - {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, - {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, - {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, - {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, - {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, -] -blockfrost-python = [ - {file = "blockfrost-python-0.4.4.tar.gz", hash = "sha256:256612146ec8627ff341719da937b90c2288aeb2e51d47fb4c70b35169d981ac"}, - {file = "blockfrost_python-0.4.4-py3-none-any.whl", hash = "sha256:45ee6bb2daff2e589e3038db93462acdd10998063da477401a7002873cd62155"}, -] -cbor2 = [ - {file = "cbor2-5.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8a643b19ace1584043bbf4e2d0b4fae8bebd6b6ffab14ea6478d3ff07f58e854"}, - {file = "cbor2-5.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e10f2f4fcf5ab6a8b24d22f7109f48cad8143f669795899370170d7b36ed309f"}, - {file = "cbor2-5.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de925608dc6d73cd1aab08800bff38f71f90459c15db3a71a67023b0fc697da"}, - {file = "cbor2-5.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62fc15bfe187e4994c457e6055687514c417d6099de62dd33ae766561f05847e"}, - {file = "cbor2-5.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3843a9bb970343e9c896aa71a34fa80983cd0ddec6eacdb2284b5e83f4ee7511"}, - {file = "cbor2-5.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b35c5d4d14fe804f718d5a5968a528970d2a7046aa87045538f189a98e5c7055"}, - {file = "cbor2-5.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:0a3a1b2f6b83ab4ce806df48360cc16d34cd315f17549dbda9fdd371bea04497"}, - {file = "cbor2-5.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b09ff6148a8cd529512479a1d6521fb7687fb03b448973933c3b03711d00bfc"}, - {file = "cbor2-5.4.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d21ccd1ec802e88dba1c373724a09538a0237116ab589c5301ca4c59478f7c10"}, - {file = "cbor2-5.4.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07975f956baddb8dfeca4966f1871fd2482cb36af24c461f763732a44675225"}, - {file = "cbor2-5.4.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9538ab1b4207e76ee02a52362d77e312921ec1dc75b6fb42182887d87d0ca53e"}, - {file = "cbor2-5.4.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cbca58220f52fd50d8985e4079e10c71196d538fb6685f157f608a29253409a4"}, - {file = "cbor2-5.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c617c7f94936d65ed9c8e99c6c03e3dc83313d69c6bfea810014ec658e9b1a9d"}, - {file = "cbor2-5.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70789805b9aebd215626188aa05bb09908ed51e3268d4db5ae6a08276efdbcb1"}, - {file = "cbor2-5.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0e4ae67a697c664b579b87c4ef9d60e26c146b95bff443a9a38abb16f6981ff0"}, - {file = "cbor2-5.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab6c934806759d453a9bb5318f2703c831e736be005ac35d5bd5cf2093ba57b1"}, - {file = "cbor2-5.4.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:981b9ffc4f2947a0f030e71ce5eac31334bc81369dd57c6c1273c94c6cdb0b5a"}, - {file = "cbor2-5.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cbe7cdeed26cd8ec2dcfed2b8876bc137ad8b9e0abb07aa5fb05770148a4b5c7"}, - {file = "cbor2-5.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bc8c5606aa0ae510bdb3c7d987f92df39ef87d09e0f0588a4d1daffd3cb0453"}, - {file = "cbor2-5.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5c50da4702ac5ca3a8e7cb9f34f62b4ea91bc81b76c2fba03888b366da299cd8"}, - {file = "cbor2-5.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37ae0ce5afe864d1a1c5b05becaf8aaca7b7131cb7b0b935d7e79b29fb1cea28"}, - {file = "cbor2-5.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2f30f7ef329ea6ec630ceabe5a539fed407b9c81e27e2322644e3efbbd1b2a76"}, - {file = "cbor2-5.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d549abea7115c8a0d7c61a31a895c031f902a7b4c875f9efd8ce41e466baf83a"}, - {file = "cbor2-5.4.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fab0e00c28305db59f7005150447d08dd13da6a82695a2132c28beba590fd2c"}, - {file = "cbor2-5.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:20291dad09cf9c4e5f434d376dd9d60f5ab5e066b308005f50e7c5e22e504214"}, - {file = "cbor2-5.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5aaf3406c9d661d11f87e792edb9a38561dba1441afba7fb883d6d963e67f32c"}, - {file = "cbor2-5.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:4e8590193fcbbb9477010ca0f094f6540a5e723965c90eea7a37edbe75f0ec4d"}, - {file = "cbor2-5.4.3.tar.gz", hash = "sha256:62b863c5ee6ced4032afe948f3c1484f375550995d3b8498145237fe28e546c2"}, -] -certifi = [ - {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"}, - {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"}, -] -certvalidator = [ - {file = "certvalidator-0.11.1-py2.py3-none-any.whl", hash = "sha256:77520b269f516d4fb0902998d5bd0eb3727fe153b659aa1cb828dcf12ea6b8de"}, - {file = "certvalidator-0.11.1.tar.gz", hash = "sha256:922d141c94393ab285ca34338e18dd4093e3ae330b1f278e96c837cb62cffaad"}, -] -cffi = [ - {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, - {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, - {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, - {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, - {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, - {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, - {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, - {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, - {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, - {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, - {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, - {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, - {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, - {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, - {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, - {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, - {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, - {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, - {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, - {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, - {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, - {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, - {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, - {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, - {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, - {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -cose = [ - {file = "cose-0.9.dev8-py3-none-any.whl", hash = "sha256:f1c3be98e50724e846e3a1d23efe19a150665a4f24917ac8bfbc8e5abb31ccb0"}, - {file = "cose-0.9.dev8.tar.gz", hash = "sha256:c48d1edcf7fbc564f4f4ac9d0daa52378ea9d26216e5c4bf4b324883ae5ef880"}, -] -coverage = [ - {file = "coverage-6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50ed480b798febce113709846b11f5d5ed1e529c88d8ae92f707806c50297abf"}, - {file = "coverage-6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:26f8f92699756cb7af2b30720de0c5bb8d028e923a95b6d0c891088025a1ac8f"}, - {file = "coverage-6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60c2147921da7f4d2d04f570e1838db32b95c5509d248f3fe6417e91437eaf41"}, - {file = "coverage-6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:750e13834b597eeb8ae6e72aa58d1d831b96beec5ad1d04479ae3772373a8088"}, - {file = "coverage-6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af5b9ee0fc146e907aa0f5fb858c3b3da9199d78b7bb2c9973d95550bd40f701"}, - {file = "coverage-6.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a022394996419142b33a0cf7274cb444c01d2bb123727c4bb0b9acabcb515dea"}, - {file = "coverage-6.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5a78cf2c43b13aa6b56003707c5203f28585944c277c1f3f109c7b041b16bd39"}, - {file = "coverage-6.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9229d074e097f21dfe0643d9d0140ee7433814b3f0fc3706b4abffd1e3038632"}, - {file = "coverage-6.4-cp310-cp310-win32.whl", hash = "sha256:fb45fe08e1abc64eb836d187b20a59172053999823f7f6ef4f18a819c44ba16f"}, - {file = "coverage-6.4-cp310-cp310-win_amd64.whl", hash = "sha256:3cfd07c5889ddb96a401449109a8b97a165be9d67077df6802f59708bfb07720"}, - {file = "coverage-6.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:03014a74023abaf5a591eeeaf1ac66a73d54eba178ff4cb1fa0c0a44aae70383"}, - {file = "coverage-6.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c82f2cd69c71698152e943f4a5a6b83a3ab1db73b88f6e769fabc86074c3b08"}, - {file = "coverage-6.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b546cf2b1974ddc2cb222a109b37c6ed1778b9be7e6b0c0bc0cf0438d9e45a6"}, - {file = "coverage-6.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc173f1ce9ffb16b299f51c9ce53f66a62f4d975abe5640e976904066f3c835d"}, - {file = "coverage-6.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c53ad261dfc8695062fc8811ac7c162bd6096a05a19f26097f411bdf5747aee7"}, - {file = "coverage-6.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:eef5292b60b6de753d6e7f2d128d5841c7915fb1e3321c3a1fe6acfe76c38052"}, - {file = "coverage-6.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:543e172ce4c0de533fa892034cce260467b213c0ea8e39da2f65f9a477425211"}, - {file = "coverage-6.4-cp37-cp37m-win32.whl", hash = "sha256:00c8544510f3c98476bbd58201ac2b150ffbcce46a8c3e4fb89ebf01998f806a"}, - {file = "coverage-6.4-cp37-cp37m-win_amd64.whl", hash = "sha256:b84ab65444dcc68d761e95d4d70f3cfd347ceca5a029f2ffec37d4f124f61311"}, - {file = "coverage-6.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d548edacbf16a8276af13063a2b0669d58bbcfca7c55a255f84aac2870786a61"}, - {file = "coverage-6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:033ebec282793bd9eb988d0271c211e58442c31077976c19c442e24d827d356f"}, - {file = "coverage-6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742fb8b43835078dd7496c3c25a1ec8d15351df49fb0037bffb4754291ef30ce"}, - {file = "coverage-6.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55fae115ef9f67934e9f1103c9ba826b4c690e4c5bcf94482b8b2398311bf9c"}, - {file = "coverage-6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd698341626f3c77784858427bad0cdd54a713115b423d22ac83a28303d1d95"}, - {file = "coverage-6.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:62d382f7d77eeeaff14b30516b17bcbe80f645f5cf02bb755baac376591c653c"}, - {file = "coverage-6.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:016d7f5cf1c8c84f533a3c1f8f36126fbe00b2ec0ccca47cc5731c3723d327c6"}, - {file = "coverage-6.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:69432946f154c6add0e9ede03cc43b96e2ef2733110a77444823c053b1ff5166"}, - {file = "coverage-6.4-cp38-cp38-win32.whl", hash = "sha256:83bd142cdec5e4a5c4ca1d4ff6fa807d28460f9db919f9f6a31babaaa8b88426"}, - {file = "coverage-6.4-cp38-cp38-win_amd64.whl", hash = "sha256:4002f9e8c1f286e986fe96ec58742b93484195defc01d5cc7809b8f7acb5ece3"}, - {file = "coverage-6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e4f52c272fdc82e7c65ff3f17a7179bc5f710ebc8ce8a5cadac81215e8326740"}, - {file = "coverage-6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b5578efe4038be02d76c344007b13119b2b20acd009a88dde8adec2de4f630b5"}, - {file = "coverage-6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8099ea680201c2221f8468c372198ceba9338a5fec0e940111962b03b3f716a"}, - {file = "coverage-6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a00441f5ea4504f5abbc047589d09e0dc33eb447dc45a1a527c8b74bfdd32c65"}, - {file = "coverage-6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e76bd16f0e31bc2b07e0fb1379551fcd40daf8cdf7e24f31a29e442878a827c"}, - {file = "coverage-6.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8d2e80dd3438e93b19e1223a9850fa65425e77f2607a364b6fd134fcd52dc9df"}, - {file = "coverage-6.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:341e9c2008c481c5c72d0e0dbf64980a4b2238631a7f9780b0fe2e95755fb018"}, - {file = "coverage-6.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:21e6686a95025927775ac501e74f5940cdf6fe052292f3a3f7349b0abae6d00f"}, - {file = "coverage-6.4-cp39-cp39-win32.whl", hash = "sha256:968ed5407f9460bd5a591cefd1388cc00a8f5099de9e76234655ae48cfdbe2c3"}, - {file = "coverage-6.4-cp39-cp39-win_amd64.whl", hash = "sha256:e35217031e4b534b09f9b9a5841b9344a30a6357627761d4218818b865d45055"}, - {file = "coverage-6.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:e637ae0b7b481905358624ef2e81d7fb0b1af55f5ff99f9ba05442a444b11e45"}, - {file = "coverage-6.4.tar.gz", hash = "sha256:727dafd7f67a6e1cad808dc884bd9c5a2f6ef1f8f6d2f22b37b96cb0080d4f49"}, -] -cryptography = [ - {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181"}, - {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178"}, - {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"}, - {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15"}, - {file = "cryptography-37.0.2-cp36-abi3-win32.whl", hash = "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0"}, - {file = "cryptography-37.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d"}, - {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9"}, - {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1"}, - {file = "cryptography-37.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023"}, - {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06"}, - {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717"}, - {file = "cryptography-37.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f"}, - {file = "cryptography-37.0.2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982"}, - {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4"}, - {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de"}, - {file = "cryptography-37.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452"}, - {file = "cryptography-37.0.2.tar.gz", hash = "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e"}, -] -decorator = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] -docutils = [ - {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, - {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, -] -ecdsa = [ - {file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"}, - {file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"}, -] -execnet = [ - {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, - {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, -] -flake8 = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, -] -flask = [ - {file = "Flask-2.1.2-py3-none-any.whl", hash = "sha256:fad5b446feb0d6db6aec0c3184d16a8c1f6c3e464b511649c8918a9be100b4fe"}, - {file = "Flask-2.1.2.tar.gz", hash = "sha256:315ded2ddf8a6281567edb27393010fe3406188bafbfe65a3339d5787d89e477"}, -] -idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, -] -imagesize = [ - {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, - {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, -] -itsdangerous = [ - {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, - {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, -] -jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] -markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -oscrypto = [ - {file = "oscrypto-1.3.0-py2.py3-none-any.whl", hash = "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085"}, - {file = "oscrypto-1.3.0.tar.gz", hash = "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, -] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, -] -pygments = [ - {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, - {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, -] -pynacl = [ - {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, - {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -pytest = [ - {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, - {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, -] -pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, -] -pytest-forked = [ - {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, - {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"}, -] -pytest-xdist = [ - {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, - {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, -] -pytz = [ - {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, - {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, -] -requests = [ - {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, - {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, -] -retry = [ - {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, - {file = "retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -snowballstemmer = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] -sphinx = [ - {file = "Sphinx-4.3.2-py3-none-any.whl", hash = "sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851"}, - {file = "Sphinx-4.3.2.tar.gz", hash = "sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c"}, -] -sphinx-copybutton = [ - {file = "sphinx-copybutton-0.5.0.tar.gz", hash = "sha256:a0c059daadd03c27ba750da534a92a63e7a36a7736dcf684f26ee346199787f6"}, - {file = "sphinx_copybutton-0.5.0-py3-none-any.whl", hash = "sha256:9684dec7434bd73f0eea58dda93f9bb879d24bff2d8b187b1f2ec08dfe7b5f48"}, -] -sphinx-rtd-theme = [ - {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, - {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, -] -sphinxcontrib-applehelp = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, -] -sphinxcontrib-devhelp = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] -sphinxcontrib-htmlhelp = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, -] -sphinxcontrib-jsmath = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] -sphinxcontrib-qthelp = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] -sphinxcontrib-serializinghtml = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -typed-ast = [ - {file = "typed_ast-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ad3b48cf2b487be140072fb86feff36801487d4abb7382bb1929aaac80638ea"}, - {file = "typed_ast-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:542cd732351ba8235f20faa0fc7398946fe1a57f2cdb289e5497e1e7f48cfedb"}, - {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc2c11ae59003d4a26dda637222d9ae924387f96acae9492df663843aefad55"}, - {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd5df1313915dbd70eaaa88c19030b441742e8b05e6103c631c83b75e0435ccc"}, - {file = "typed_ast-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:e34f9b9e61333ecb0f7d79c21c28aa5cd63bec15cb7e1310d7d3da6ce886bc9b"}, - {file = "typed_ast-1.5.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f818c5b81966d4728fec14caa338e30a70dfc3da577984d38f97816c4b3071ec"}, - {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3042bfc9ca118712c9809201f55355479cfcdc17449f9f8db5e744e9625c6805"}, - {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4fff9fdcce59dc61ec1b317bdb319f8f4e6b69ebbe61193ae0a60c5f9333dc49"}, - {file = "typed_ast-1.5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8e0b8528838ffd426fea8d18bde4c73bcb4167218998cc8b9ee0a0f2bfe678a6"}, - {file = "typed_ast-1.5.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ef1d96ad05a291f5c36895d86d1375c0ee70595b90f6bb5f5fdbee749b146db"}, - {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed44e81517364cb5ba367e4f68fca01fba42a7a4690d40c07886586ac267d9b9"}, - {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f60d9de0d087454c91b3999a296d0c4558c1666771e3460621875021bf899af9"}, - {file = "typed_ast-1.5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9e237e74fd321a55c90eee9bc5d44be976979ad38a29bbd734148295c1ce7617"}, - {file = "typed_ast-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee852185964744987609b40aee1d2eb81502ae63ee8eef614558f96a56c1902d"}, - {file = "typed_ast-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:27e46cdd01d6c3a0dd8f728b6a938a6751f7bd324817501c15fb056307f918c6"}, - {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d64dabc6336ddc10373922a146fa2256043b3b43e61f28961caec2a5207c56d5"}, - {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8cdf91b0c466a6c43f36c1964772918a2c04cfa83df8001ff32a89e357f8eb06"}, - {file = "typed_ast-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:9cc9e1457e1feb06b075c8ef8aeb046a28ec351b1958b42c7c31c989c841403a"}, - {file = "typed_ast-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e20d196815eeffb3d76b75223e8ffed124e65ee62097e4e73afb5fec6b993e7a"}, - {file = "typed_ast-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:37e5349d1d5de2f4763d534ccb26809d1c24b180a477659a12c4bde9dd677d74"}, - {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f1a27592fac87daa4e3f16538713d705599b0a27dfe25518b80b6b017f0a6d"}, - {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8831479695eadc8b5ffed06fdfb3e424adc37962a75925668deeb503f446c0a3"}, - {file = "typed_ast-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:20d5118e494478ef2d3a2702d964dae830aedd7b4d3b626d003eea526be18718"}, - {file = "typed_ast-1.5.3.tar.gz", hash = "sha256:27f25232e2dd0edfe1f019d6bfaaf11e86e657d9bdb7b0956db95f560cceb2b3"}, -] -typeguard = [ - {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, - {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, -] -typing-extensions = [ - {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, - {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, -] -urllib3 = [ - {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, - {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, -] -websocket-client = [ - {file = "websocket-client-1.3.2.tar.gz", hash = "sha256:50b21db0058f7a953d67cc0445be4b948d7fc196ecbeb8083d68d94628e4abf6"}, - {file = "websocket_client-1.3.2-py3-none-any.whl", hash = "sha256:722b171be00f2b90e1d4fb2f2b53146a536ca38db1da8ff49c972a4e1365d0ef"}, -] -werkzeug = [ - {file = "Werkzeug-2.1.2-py3-none-any.whl", hash = "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255"}, - {file = "Werkzeug-2.1.2.tar.gz", hash = "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6"}, -] -zipp = [ - {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, - {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, -] +alabaster = [] +asn1crypto = [] +atomicwrites = [] +attrs = [] +babel = [] +black = [] +blockfrost-python = [] +cbor2 = [] +certifi = [] +certvalidator = [] +cffi = [] +charset-normalizer = [] +click = [] +colorama = [] +cose = [] +coverage = [] +cryptography = [] +decorator = [] +docutils = [] +ecdsa = [] +ecpy = [] +execnet = [] +flake8 = [] +flask = [] +idna = [] +imagesize = [] +importlib-metadata = [] +iniconfig = [] +isort = [] +itsdangerous = [] +jinja2 = [] +markupsafe = [] +mccabe = [] +mnemonic = [] +mypy-extensions = [] +oscrypto = [] +packaging = [] +pathspec = [] +platformdirs = [] +pluggy = [] +py = [] +pycodestyle = [] +pycparser = [] +pyflakes = [] +pygments = [] +pynacl = [] +pyparsing = [] +pytest = [] +pytest-cov = [] +pytest-forked = [] +pytest-xdist = [] +pytz = [] +requests = [] +retry = [] +six = [] +snowballstemmer = [] +sphinx = [] +sphinx-copybutton = [] +sphinx-rtd-theme = [] +sphinxcontrib-applehelp = [] +sphinxcontrib-devhelp = [] +sphinxcontrib-htmlhelp = [] +sphinxcontrib-jsmath = [] +sphinxcontrib-qthelp = [] +sphinxcontrib-serializinghtml = [] +tomli = [] +typed-ast = [] +typeguard = [] +typing-extensions = [] +urllib3 = [] +websocket-client = [] +werkzeug = [] +zipp = [] diff --git a/pycardano/hdwallet.py b/pycardano/hdwallet.py new file mode 100644 index 00000000..5bdbb382 --- /dev/null +++ b/pycardano/hdwallet.py @@ -0,0 +1,485 @@ +import hashlib +import hmac +import unicodedata +from binascii import hexlify, unhexlify +from typing import Optional, Union + +from ecpy.curves import Curve +from mnemonic import Mnemonic + + +def _Fk(message, secret): + return hmac.new(secret, message, hashlib.sha512).digest() + + +class HDWallet: + """ + Hierarchical Deterministic Wallet for Cardano + """ + + def __int__(self): + self._seed: Optional[bytes] = None + self._mnemonic: Optional[str] = None + self._passphrase: Optional[str] = None + self._entropy: Optional[str] = None + + self._root_xprivate_key: Optional[tuple] = None + self._root_public_key: Optional[bytes] = None + self._root_chain_code: Optional[bytes] = None + self._xprivate_key: Optional[tuple] = None + self._public_key: Optional[bytes] = None + self._chain_code: Optional[bytes] = None + self._path: str = "m" + self._depth: int = 0 + self._index: int = 0 + + def from_seed(self, seed: str) -> "HDWallet": + """ + Create an HDWallet instance from master key. + + Args: + seed: Master key of 96 bytes from seed hex string. + + Returns: + HDWallet -- Hierarchical Deterministic Wallet instance. + """ + + seed = bytearray(bytes.fromhex(seed)) + seed_modified = self._tweak_bits(seed) + self._seed = seed_modified + + kL, kR, c = seed_modified[:32], seed_modified[32:64], seed_modified[64:] + + # root public key + cv25519 = Curve.get_curve("Ed25519") + k_scalar = int.from_bytes(bytes(kL), "little") + P = k_scalar * cv25519.generator + A = cv25519.encode_point(P) + + # set root keys and parent keys + self._root_xprivate_key = self._xprivate_key = (kL, kR) + self._root_public_key = self._public_key = A + self._root_chain_code = self._chain_code = c + + return self + + def from_mnemonic( + self, mnemonic: Union[str, list], passphrase: str = "" + ) -> "HDWallet": + """ + Create master key and HDWallet from Mnemonic words. + + Args: + mnemonic: Mnemonic words. + passphrase: Mnemonic passphrase or password, default to ``None``. + + Returns: + HDWallet -- Hierarchical Deterministic Wallet instance. + """ + + if not self.is_mnemonic(mnemonic=mnemonic): + raise ValueError("Invalid mnemonic words.") + + self._mnemonic = unicodedata.normalize("NFKD", mnemonic) + self._passphrase = str(passphrase) if passphrase else "" + + entropy = Mnemonic(language="english").to_entropy(words=mnemonic) + self._entropy = hexlify(entropy).decode() + + seed = bytearray( + hashlib.pbkdf2_hmac( + "sha512", + password=passphrase.encode(), + salt=entropy, + iterations=4096, + dklen=96, + ) + ) + + return self.from_seed(seed=hexlify(seed).decode()) + + def from_entropy(self, entropy: str, passphrase: str = None) -> "HDWallet": + """ + Create master key and HDWallet from Mnemonic words. + + Args: + entropy: Entropy hex string. + passphrase: Mnemonic passphrase or password, default to ``None``. + + Returns: + HDWallet -- Hierarchical Deterministic Wallet instance. + """ + + if not self.is_entropy(entropy): + raise ValueError("Invalid entropy") + + self._entropy = entropy + + seed = bytearray( + hashlib.pbkdf2_hmac( + "sha512", password=passphrase, salt=entropy, iterations=4096, dklen=96 + ) + ) + return self.from_seed(seed=hexlify(seed).decode()) + + def _tweak_bits(self, seed: bytearray) -> bytearray: + """ + Modify seed based on Icarus master node derivation scheme. + + The process follows + `CIP-0003#Wallet Key Generation `_. + + Process: + - clear the lowest 3 bits + - clear the highest 3 bits + - set the highest second bit + + Args: + seed: Seed in bytearray + + Returns: + modified bytearray seed. + """ + seed[0] &= 0b11111000 + seed[31] &= 0b00011111 + seed[31] |= 0b01000000 + + return seed + + def derive_from_path(self, path: str, private: bool = True) -> "HDWallet": + """ + Derive keys from a path following CIP-1852 specifications. + + Args: + path: Derivation path for the key generation. + private: whether to derive private child keys or public child keys. + + Returns: + HDWallet instance with keys derived + + Examples: + >>> mnemonic_words = "test walk nut penalty hip pave soap entry language right filter choice" + >>> hdwallet = HDWallet().from_mnemonic(mnemonic_words) + >>> hdwallet.derive_from_path("m/1852'/1815'/0'/0/0") + >>> hdwallet.public_key + '73fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7d' + """ + + if path[:2] != "m/": + raise ValueError( + 'Bad path, please insert like this type of path "m/0\'/0"! ' + ) + + for index in path.lstrip("m/").split("/"): + if index.endswith("'"): + self.derive_from_index(int(index[:-1]), private=private, hardened=True) + else: + self.derive_from_index(int(index), private=private, hardened=False) + + def derive_from_index( + self, index: int, private: bool = True, hardened: bool = False + ) -> "HDWallet": + """ + Derive keys from index. + + Args: + index: Derivation index. + private: whether to derive private child keys or public child keys. + hardened: whether to derive hardened address. Default to False. + + Returns: + HDWallet instance with keys derived + + Examples: + >>> mnemonic_words = "test walk nut penalty hip pave soap entry language right filter choice" + >>> hdwallet = HDWallet().from_mnemonic(mnemonic_words) + >>> hdwallet.derive_from_index(index=1852, hardened=True) + >>> hdwallet.derive_from_index(index=1815, hardened=True) + >>> hdwallet.derive_from_index(index=0, hardened=True) + >>> hdwallet.derive_from_index(index=0) + >>> hdwallet.derive_from_index(index=0) + >>> hdwallet.public_key + '73fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7d' + """ + + if not isinstance(index, int): + raise ValueError("Bad index, Please import only integer number!") + + if not self._root_xprivate_key and not self._root_public_key: + raise ValueError("Missing root keys. Can't do derivation.") + + if hardened: + index += 2**31 + + # derive private child key + if private: + node = (self._xprivate_key, self._public_key, self._chain_code) + self._derive_private_child_key_by_index(node, index) + # derive public child key + else: + node = (self._public_key, self._chain_code) + self._derive_public_child_key_by_index(node, index) + + def _derive_private_child_key_by_index( + self, private_pnode: ((bytes, bytes), bytes, bytes), index: int + ) -> Optional["HDWallet"]: + """ + Derive private child keys from parent node. + + PROCESS: + 1. encode i 4-bytes little endian, il = encode_U32LE(i) + 2. if i is less than 2^31 + - compute Z = HMAC-SHA512(key=c, Data=0x02 | A | il ) + - compute c_ = HMAC-SHA512(key=c, Data=0x03 | A | il ) + else + - compute Z = HMAC-SHA512(key=c, Data=0x00 | kL | kR | il ) + - compute c_ = HMAC-SHA512(key=c, Data=0x01 | kL | kR | il ) + 3. ci = lowest_32bytes(c_) + 4. set ZL = highest_28bytes(Z) + set ZR = lowest_32bytes(Z) + 5. compute kL_i: + zl_ = LEBytes_to_int(ZL) + kL_ = LEBytes_to_int(kL) + kLi_ = zl_*8 + kL_ + if kLi_ % order == 0: child does not exist + kL_i = int_to_LEBytes(kLi_) + 6. compute kR_i + zr_ = LEBytes_to_int(ZR) + kR_ = LEBytes_to_int(kR) + kRi_ = (zr_ + kRn_) % 2^256 + kR_i = int_to_LEBytes(kRi_) + 7. compute A + A = kLi_.G + 8. return ((kL_i,kR_i), A_i, c) + + Args: + private_pnode: ((kLP,kRP), AP, cP). (kLP,kRP) is 64 bytes parent private eddsa key, + AP is 32 btyes parent public key, cP is 32 btyes parent chain code. + index: child index to compute (hardened if >= 0x80000000) + + Returns: + HDWallet with child node derived. + + """ + + if not private_pnode: + return None + + # unpack argument + ((kLP, kRP), AP, cP) = private_pnode + assert 0 <= index < 2**32 + + i_bytes = index.to_bytes(4, "little") + + # compute Z,c + if index < 2**31: + # regular child + Z = _Fk(b"\x02" + AP + i_bytes, cP) + c = _Fk(b"\x03" + AP + i_bytes, cP)[32:] + else: + # harderned child + Z = _Fk(b"\x00" + (kLP + kRP) + i_bytes, cP) + c = _Fk(b"\x01" + (kLP + kRP) + i_bytes, cP)[32:] + + ZL, ZR = Z[:28], Z[32:] + + # compute KLi + kLn = int.from_bytes(ZL, "little") * 8 + int.from_bytes(kLP, "little") + + # compute KRi + kRn = (int.from_bytes(ZR, "little") + int.from_bytes(kRP, "little")) % 2**256 + + kL = kLn.to_bytes(32, "little") + kR = kRn.to_bytes(32, "little") + + # compue Ai + cv25519 = Curve.get_curve("Ed25519") + k_scalar = int.from_bytes(kL, "little") + P = k_scalar * cv25519.generator + A = cv25519.encode_point(P) + + self._xprivate_key = (kL, kR) + self._public_key = A + self._chain_code = c + + return self + + def _derive_public_child_key_by_index( + self, public_pnode: (bytes, bytes), index: int + ) -> Optional["HDWallet"]: + """ + Derive public child keys from parent node. + + Args: + public_pnode: (AP, cP). AP is 32 btyes parent public key, cP is 32 btyes parent chain code. + index: child index to compute (hardened if >= 0x80000000) + + Returns: + HDWallet with child node derived. + """ + + if not public_pnode: + return None + + # unpack argument + (AP, cP) = public_pnode + assert 0 <= index < 2**32 + + i_bytes = index.to_bytes(4, "little") + + # compute Z,c + if index < 2**31: + # regular child + Z = _Fk(b"\x02" + AP + i_bytes, cP) + c = _Fk(b"\x03" + AP + i_bytes, cP)[32:] + else: + # can't derive hardened child from public keys + return None + + ZL = Z[:28] + + # compute ZLi + ZLint = int.from_bytes(ZL, "little") + ZLint_x_8 = 8 * ZLint + + # compue Ai + cv25519 = Curve.get_curve("Ed25519") + P = ZLint_x_8 * cv25519.generator + Q = cv25519.decode_point(AP) + PQ = P + Q + A = cv25519.encode_point(PQ) + + self._public_key = A + self._chain_code = c + + return self + + @property + def root_xprivate_key(self): + return (self._root_xprivate_key[0].hex(), self._root_xprivate_key[1].hex()) + + @property + def root_public_key(self): + return None if not self._root_public_key else self._root_public_key.hex() + + @property + def root_chain_code(self): + return None if not self._root_chain_code else self._root_chain_code.hex() + + @property + def xprivate_key(self): + return ( + (None, None) + if not self._xprivate_key + else (self._xprivate_key[0].hex(), self._xprivate_key[1].hex()) + ) + + @property + def public_key(self): + return None if not self._public_key else self._public_key.hex() + + @property + def chain_code(self): + return None if not self._chain_code else self._chain_code.hex() + + @staticmethod + def generate_mnemonic(language: str = "english", strength: int = 256) -> str: + """ + Generate mnemonic words. + + Args: + language (str): language for the mnemonic words. + strength (int): length of the mnemoic words. Valid values are 128/160/192/224/256. + + Returns: + mnemonic (str): mnemonic words. + """ + + if language and language not in [ + "english", + "french", + "italian", + "japanese", + "chinese_simplified", + "chinese_traditional", + "korean", + "spanish", + ]: + raise ValueError( + "invalid language, use only this options english, french, " + "italian, spanish, chinese_simplified, chinese_traditional, japanese or korean languages." + ) + if strength not in [128, 160, 192, 224, 256]: + raise ValueError( + "Strength should be one of the following " + "[128, 160, 192, 224, 256], but it is not (%d)." % strength + ) + + return Mnemonic(language=language).generate(strength=strength) + + @staticmethod + def is_mnemonic(mnemonic: str, language: Optional[str] = None) -> bool: + """ + Check if mnemonic words are valid. + + Args: + mnemonic (str): Mnemonic words in string format. + language (Optional[str]): Mnemonic language, default to None. + + Returns: + bool. Whether the input mnemonic words is valid. + """ + + if language and language not in [ + "english", + "french", + "italian", + "japanese", + "chinese_simplified", + "chinese_traditional", + "korean", + "spanish", + ]: + raise ValueError( + "invalid language, use only this options english, french, " + "italian, spanish, chinese_simplified, chinese_traditional, japanese or korean languages." + ) + try: + mnemonic = unicodedata.normalize("NFKD", mnemonic) + if language is None: + for _language in [ + "english", + "french", + "italian", + "chinese_simplified", + "chinese_traditional", + "japanese", + "korean", + "spanish", + ]: + valid = False + if Mnemonic(language=_language).check(mnemonic=mnemonic) is True: + valid = True + break + return valid + else: + return Mnemonic(language=language).check(mnemonic=mnemonic) + except ValueError: + print("The input mnemonic words are not valid. Words should be in string format seperated by space.") + + @staticmethod + def is_entropy(entropy: str) -> bool: + """ + Check entropy hex string. + + Args: + entropy: entropy converted from mnemonic words. + + Returns: + bool. Whether entropy is valid or not. + """ + + try: + return len(unhexlify(entropy)) in [16, 20, 24, 28, 32] + except ValueError: + print("The input entropy is not valid.") diff --git a/pyproject.toml b/pyproject.toml index d81eefe9..fd5d994d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,8 @@ typeguard = "^2.13.3" blockfrost-python = "0.4.4" websocket-client = "^1.2.3" cose = "^0.9.dev8" +mnemonic = "^0.20" +ECPy = "^1.2.5" [tool.poetry.dev-dependencies] Sphinx = "^4.3.2" diff --git a/test/pycardano/backend/test_ogmios.py b/test/pycardano/backend/test_ogmios.py index 7b149481..f960f3a7 100644 --- a/test/pycardano/backend/test_ogmios.py +++ b/test/pycardano/backend/test_ogmios.py @@ -43,7 +43,7 @@ "maxLovelaceSupply": 1000000000000, "protocolParameters": { "minUtxoValue": 1000000, - } + }, } UTXOS = [ diff --git a/test/pycardano/test_hdwallet.py b/test/pycardano/test_hdwallet.py new file mode 100644 index 00000000..6f269e82 --- /dev/null +++ b/test/pycardano/test_hdwallet.py @@ -0,0 +1,68 @@ +from pycardano.address import Address +from pycardano.hdwallet import HDWallet +from pycardano.key import PaymentVerificationKey +from pycardano.network import Network + +MNEMONIC_12 = "test walk nut penalty hip pave soap entry language right filter choice" +MNEMONIC_15 = "art forum devote street sure rather head chuckle guard poverty release quote oak craft enemy" + + +def test_mnemonic(): + wrong_mnemonic = "test walk nut penalty hip pave soap entry language right filter" + hdwallet = HDWallet() + assert not hdwallet.is_mnemonic(wrong_mnemonic) + + +def test_mnemonic_generation(): + hdwallet = HDWallet() + mnemonic_words = hdwallet.generate_mnemonic(strength=128) + assert hdwallet.is_mnemonic(mnemonic_words) + + +def test_payment_address_12_reward(): + hdwallet_stake = HDWallet() + hdwallet_stake.from_mnemonic(MNEMONIC_12) + hdwallet_stake.derive_from_path("m/1852'/1815'/0'/2/0") + stake_public_key = hdwallet_stake.public_key + stake_vk = PaymentVerificationKey.from_primitive(bytes.fromhex(stake_public_key)) + + assert ( + Address( + payment_part=None, staking_part=stake_vk.hash(), network=Network.TESTNET + ).encode() + == "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl" + ) + + +def test_payment_address_12_base(): + hdwallet_spend = HDWallet().from_mnemonic(MNEMONIC_12) + hdwallet_spend.derive_from_path("m/1852'/1815'/0'/0/0") + spend_public_key = hdwallet_spend.public_key + spend_vk = PaymentVerificationKey.from_primitive(bytes.fromhex(spend_public_key)) + + hdwallet_stake = HDWallet().from_mnemonic(MNEMONIC_12) + hdwallet_stake.derive_from_path("m/1852'/1815'/0'/2/0") + stake_public_key = hdwallet_stake.public_key + stake_vk = PaymentVerificationKey.from_primitive(bytes.fromhex(stake_public_key)) + + assert ( + Address(spend_vk.hash(), stake_vk.hash(), network=Network.TESTNET).encode() + == "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp" + ) + + +def test_payment_address_15_base(): + hdwallet_spend = HDWallet().from_mnemonic(MNEMONIC_15) + hdwallet_spend.derive_from_path("m/1852'/1815'/0'/0/0") + spend_public_key = hdwallet_spend.public_key + spend_vk = PaymentVerificationKey.from_primitive(bytes.fromhex(spend_public_key)) + + hdwallet_stake = HDWallet().from_mnemonic(MNEMONIC_15) + hdwallet_stake.derive_from_path("m/1852'/1815'/0'/2/0") + stake_public_key = hdwallet_stake.public_key + stake_vk = PaymentVerificationKey.from_primitive(bytes.fromhex(stake_public_key)) + + assert ( + Address(spend_vk.hash(), stake_vk.hash(), network=Network.MAINNET).encode() + == "addr1q9u5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qld6xc3" + ) diff --git a/test/pycardano/test_nativescript.py b/test/pycardano/test_nativescript.py index a973851c..a9c0ad2f 100644 --- a/test/pycardano/test_nativescript.py +++ b/test/pycardano/test_nativescript.py @@ -193,6 +193,6 @@ def test_from_dict(): } script_from_dict = NativeScript.from_dict(script_all_dict) - + assert script_from_dict == script_all assert script_from_dict.to_dict() == script_all_dict diff --git a/test/pycardano/test_util.py b/test/pycardano/test_util.py index b8e288c5..2c83b3e2 100644 --- a/test/pycardano/test_util.py +++ b/test/pycardano/test_util.py @@ -11,7 +11,10 @@ def test_min_lovelace_ada_only(chain_context): def test_min_lovelace_ada_only_2(chain_context): - assert min_lovelace(Value(2000000), chain_context) == chain_context.protocol_param.min_utxo + assert ( + min_lovelace(Value(2000000), chain_context) + == chain_context.protocol_param.min_utxo + ) class TestMinLoveLaceMultiAsset: From 436a80a69205646d1b0107d871ed54ff5b04a52c Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sat, 17 Sep 2022 19:40:10 -0400 Subject: [PATCH 27/36] modify poetry --- poetry.lock | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 72831012..afdadd13 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1494,5 +1494,4 @@ Werkzeug = [ zipp = [ {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, -] ->>>>>>> e194656c8ea0073fbf3fcdceccb931c302500095 +] \ No newline at end of file From be58baf415d352eb50f7e5f843af8d2f1d499aed Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sat, 17 Sep 2022 21:21:24 -0400 Subject: [PATCH 28/36] modify poetry file --- poetry.lock | 76 ----------------------------------------------------- 1 file changed, 76 deletions(-) diff --git a/poetry.lock b/poetry.lock index afdadd13..77419ffe 100644 --- a/poetry.lock +++ b/poetry.lock @@ -881,82 +881,6 @@ lock-version = "1.1" python-versions = "^3.7" content-hash = "fc594a913a31e56985043766c99a9f125ef306e0ad06d271805d16c8680550d1" -[metadata.files] -alabaster = [] -asn1crypto = [] -atomicwrites = [] -attrs = [] -babel = [] -black = [] -blockfrost-python = [] -cbor2 = [] -certifi = [] -certvalidator = [] -cffi = [] -charset-normalizer = [] -click = [] -colorama = [] -cose = [] -coverage = [] -cryptography = [] -decorator = [] -docutils = [] -ecdsa = [] -ecpy = [] -execnet = [] -flake8 = [] -flask = [] -idna = [] -imagesize = [] -importlib-metadata = [] -iniconfig = [] -isort = [] -itsdangerous = [] -jinja2 = [] -markupsafe = [] -mccabe = [] -mnemonic = [] -mypy-extensions = [] -oscrypto = [] -packaging = [] -pathspec = [] -platformdirs = [] -pluggy = [] -py = [] -pycodestyle = [] -pycparser = [] -pyflakes = [] -pygments = [] -pynacl = [] -pyparsing = [] -pytest = [] -pytest-cov = [] -pytest-forked = [] -pytest-xdist = [] -pytz = [] -requests = [] -retry = [] -six = [] -snowballstemmer = [] -sphinx = [] -sphinx-copybutton = [] -sphinx-rtd-theme = [] -sphinxcontrib-applehelp = [] -sphinxcontrib-devhelp = [] -sphinxcontrib-htmlhelp = [] -sphinxcontrib-jsmath = [] -sphinxcontrib-qthelp = [] -sphinxcontrib-serializinghtml = [] -tomli = [] -typed-ast = [] -typeguard = [] -typing-extensions = [] -urllib3 = [] -websocket-client = [] -werkzeug = [] -zipp = [] -content-hash = "307e249d28b57105bc35754290143985bfc2ca09af10c7d92647a847b88beac6" - [metadata.files] alabaster = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, From 6c428b9056e8bafd053cbd1ac2fa4555238752ef Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sat, 17 Sep 2022 21:49:44 -0400 Subject: [PATCH 29/36] modify poetry lock --- poetry.lock | 710 +++++++++------------------------------------------- 1 file changed, 115 insertions(+), 595 deletions(-) diff --git a/poetry.lock b/poetry.lock index 77419ffe..ea03cace 100644 --- a/poetry.lock +++ b/poetry.lock @@ -23,13 +23,13 @@ optional = false python-versions = ">=3.5" [package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] [[package]] -name = "Babel" +name = "babel" version = "2.10.3" description = "Internationalization utilities" category = "dev" @@ -82,12 +82,12 @@ optional = false python-versions = ">=3.7" [package.extras] -doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] test = ["pytest", "pytest-cov"] [[package]] name = "certifi" -version = "2022.6.15.2" +version = "2022.9.14" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -189,11 +189,11 @@ cffi = ">=1.12" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] +test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] name = "decorator" @@ -260,7 +260,7 @@ pycodestyle = ">=2.8.0,<2.9.0" pyflakes = ">=2.4.0,<2.5.0" [[package]] -name = "Flask" +name = "flask" version = "2.2.2" description = "A simple framework for building complex web applications." category = "dev" @@ -307,8 +307,8 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -327,10 +327,10 @@ optional = false python-versions = ">=3.6.1,<4.0" [package.extras] -colors = ["colorama (>=0.4.3,<0.5.0)"] pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] -requirements_deprecated_finder = ["pip-api", "pipreqs"] [[package]] name = "itsdangerous" @@ -341,7 +341,7 @@ optional = false python-versions = ">=3.7" [[package]] -name = "Jinja2" +name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." category = "dev" @@ -355,7 +355,7 @@ MarkupSafe = ">=2.0" i18n = ["Babel (>=2.7)"] [[package]] -name = "MarkupSafe" +name = "markupsafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "dev" @@ -425,8 +425,8 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] [[package]] name = "pluggy" @@ -484,7 +484,7 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] -name = "Pygments" +name = "pygments" version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" @@ -495,7 +495,7 @@ python-versions = ">=3.6" plugins = ["importlib-metadata"] [[package]] -name = "PyNaCl" +name = "pynacl" version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" category = "main" @@ -506,8 +506,8 @@ python-versions = ">=3.6" cffi = ">=1.4.1" [package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx_rtd_theme"] -tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] [[package]] name = "pyparsing" @@ -518,7 +518,7 @@ optional = false python-versions = ">=3.6.8" [package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +diagrams = ["railroad-diagrams", "jinja2"] [[package]] name = "pytest" @@ -554,7 +554,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] [[package]] name = "pytest-forked" @@ -624,19 +624,6 @@ python-versions = "*" decorator = ">=3.4.2" py = ">=1.4.26,<2.0.0" -[[package]] -name = "setuptools" -version = "65.3.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - [[package]] name = "six" version = "1.16.0" @@ -654,7 +641,7 @@ optional = false python-versions = "*" [[package]] -name = "Sphinx" +name = "sphinx" version = "4.3.2" description = "Python documentation generator" category = "dev" @@ -671,7 +658,6 @@ Jinja2 = ">=2.3" packaging = "*" Pygments = ">=2.0" requests = ">=2.5.0" -setuptools = "*" snowballstemmer = ">=1.1" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" @@ -682,8 +668,8 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "isort", "mypy (>=0.920)", "types-pkg-resources", "types-requests", "types-typed-ast"] -test = ["cython", "html5lib", "pytest", "pytest-cov", "typed-ast"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.920)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"] +test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] [[package]] name = "sphinx-copybutton" @@ -697,7 +683,7 @@ python-versions = ">=3.6" sphinx = ">=1.8" [package.extras] -rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme"] +rtd = ["sphinx-book-theme", "myst-nb", "ipython", "sphinx"] code_style = ["pre-commit (==2.12.1)"] [[package]] @@ -724,9 +710,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["docutils-stubs", "mypy", "flake8"] test = ["pytest"] - +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-devhelp" @@ -737,9 +722,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["docutils-stubs", "mypy", "flake8"] test = ["pytest"] - +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-htmlhelp" @@ -750,8 +734,8 @@ optional = false python-versions = ">=3.6" [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-jsmath" @@ -762,7 +746,7 @@ optional = false python-versions = ">=3.5" [package.extras] -test = ["flake8", "mypy", "pytest"] +test = ["mypy", "flake8", "pytest"] [[package]] name = "sphinxcontrib-qthelp" @@ -773,8 +757,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["docutils-stubs", "mypy", "flake8"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-serializinghtml" @@ -785,7 +769,7 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] [[package]] @@ -813,8 +797,8 @@ optional = false python-versions = ">=3.5.3" [package.extras] -doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["mypy", "pytest", "typing-extensions"] +doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["pytest", "typing-extensions", "mypy"] [[package]] name = "typing-extensions" @@ -833,8 +817,8 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -851,7 +835,7 @@ optional = ["wsaccel", "python-socks"] docs = ["sphinx-rtd-theme (>=0.5)", "Sphinx (>=3.4)"] [[package]] -name = "Werkzeug" +name = "werkzeug" version = "2.2.2" description = "The comprehensive WSGI web application library." category = "dev" @@ -873,549 +857,85 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "fc594a913a31e56985043766c99a9f125ef306e0ad06d271805d16c8680550d1" +content-hash = "ed7197524ef41e00c0d64fe14ff6d74be3ac2470d5177c79090dd8f02b051c95" [metadata.files] -alabaster = [ - {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, - {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, -] -asn1crypto = [ - {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, - {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, -] -attrs = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, -] -Babel = [ - {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, - {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, -] -black = [ - {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, - {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, - {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, - {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, - {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, - {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, - {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, - {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, - {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, - {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, - {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, - {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, - {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, - {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, - {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, - {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, - {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, - {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, - {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, -] -blockfrost-python = [ - {file = "blockfrost-python-0.5.1.tar.gz", hash = "sha256:23dfc62c5634b932baba6309080439e9d209b81cd22c8b021016d64f122ada10"}, - {file = "blockfrost_python-0.5.1-py3-none-any.whl", hash = "sha256:1602e1b047bfc4fb21023ca59837cee447803e86798742984c4e9450579f1b4b"}, -] -cbor2 = [ - {file = "cbor2-5.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8a643b19ace1584043bbf4e2d0b4fae8bebd6b6ffab14ea6478d3ff07f58e854"}, - {file = "cbor2-5.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e10f2f4fcf5ab6a8b24d22f7109f48cad8143f669795899370170d7b36ed309f"}, - {file = "cbor2-5.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de925608dc6d73cd1aab08800bff38f71f90459c15db3a71a67023b0fc697da"}, - {file = "cbor2-5.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62fc15bfe187e4994c457e6055687514c417d6099de62dd33ae766561f05847e"}, - {file = "cbor2-5.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3843a9bb970343e9c896aa71a34fa80983cd0ddec6eacdb2284b5e83f4ee7511"}, - {file = "cbor2-5.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b35c5d4d14fe804f718d5a5968a528970d2a7046aa87045538f189a98e5c7055"}, - {file = "cbor2-5.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:0a3a1b2f6b83ab4ce806df48360cc16d34cd315f17549dbda9fdd371bea04497"}, - {file = "cbor2-5.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b09ff6148a8cd529512479a1d6521fb7687fb03b448973933c3b03711d00bfc"}, - {file = "cbor2-5.4.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d21ccd1ec802e88dba1c373724a09538a0237116ab589c5301ca4c59478f7c10"}, - {file = "cbor2-5.4.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07975f956baddb8dfeca4966f1871fd2482cb36af24c461f763732a44675225"}, - {file = "cbor2-5.4.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9538ab1b4207e76ee02a52362d77e312921ec1dc75b6fb42182887d87d0ca53e"}, - {file = "cbor2-5.4.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cbca58220f52fd50d8985e4079e10c71196d538fb6685f157f608a29253409a4"}, - {file = "cbor2-5.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c617c7f94936d65ed9c8e99c6c03e3dc83313d69c6bfea810014ec658e9b1a9d"}, - {file = "cbor2-5.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70789805b9aebd215626188aa05bb09908ed51e3268d4db5ae6a08276efdbcb1"}, - {file = "cbor2-5.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0e4ae67a697c664b579b87c4ef9d60e26c146b95bff443a9a38abb16f6981ff0"}, - {file = "cbor2-5.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab6c934806759d453a9bb5318f2703c831e736be005ac35d5bd5cf2093ba57b1"}, - {file = "cbor2-5.4.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:981b9ffc4f2947a0f030e71ce5eac31334bc81369dd57c6c1273c94c6cdb0b5a"}, - {file = "cbor2-5.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cbe7cdeed26cd8ec2dcfed2b8876bc137ad8b9e0abb07aa5fb05770148a4b5c7"}, - {file = "cbor2-5.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bc8c5606aa0ae510bdb3c7d987f92df39ef87d09e0f0588a4d1daffd3cb0453"}, - {file = "cbor2-5.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5c50da4702ac5ca3a8e7cb9f34f62b4ea91bc81b76c2fba03888b366da299cd8"}, - {file = "cbor2-5.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37ae0ce5afe864d1a1c5b05becaf8aaca7b7131cb7b0b935d7e79b29fb1cea28"}, - {file = "cbor2-5.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2f30f7ef329ea6ec630ceabe5a539fed407b9c81e27e2322644e3efbbd1b2a76"}, - {file = "cbor2-5.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d549abea7115c8a0d7c61a31a895c031f902a7b4c875f9efd8ce41e466baf83a"}, - {file = "cbor2-5.4.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fab0e00c28305db59f7005150447d08dd13da6a82695a2132c28beba590fd2c"}, - {file = "cbor2-5.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:20291dad09cf9c4e5f434d376dd9d60f5ab5e066b308005f50e7c5e22e504214"}, - {file = "cbor2-5.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5aaf3406c9d661d11f87e792edb9a38561dba1441afba7fb883d6d963e67f32c"}, - {file = "cbor2-5.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:4e8590193fcbbb9477010ca0f094f6540a5e723965c90eea7a37edbe75f0ec4d"}, - {file = "cbor2-5.4.3.tar.gz", hash = "sha256:62b863c5ee6ced4032afe948f3c1484f375550995d3b8498145237fe28e546c2"}, -] -certifi = [ - {file = "certifi-2022.6.15.2-py3-none-any.whl", hash = "sha256:0aa1a42fbd57645fabeb6290a7687c21755b0344ecaeaa05f4e9f6207ae2e9a8"}, - {file = "certifi-2022.6.15.2.tar.gz", hash = "sha256:aa08c101214127b9b0472ca6338315113c9487d45376fd3e669201b477c71003"}, -] -certvalidator = [ - {file = "certvalidator-0.11.1-py2.py3-none-any.whl", hash = "sha256:77520b269f516d4fb0902998d5bd0eb3727fe153b659aa1cb828dcf12ea6b8de"}, - {file = "certvalidator-0.11.1.tar.gz", hash = "sha256:922d141c94393ab285ca34338e18dd4093e3ae330b1f278e96c837cb62cffaad"}, -] -cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, -] -cose = [ - {file = "cose-0.9.dev8-py3-none-any.whl", hash = "sha256:f1c3be98e50724e846e3a1d23efe19a150665a4f24917ac8bfbc8e5abb31ccb0"}, - {file = "cose-0.9.dev8.tar.gz", hash = "sha256:c48d1edcf7fbc564f4f4ac9d0daa52378ea9d26216e5c4bf4b324883ae5ef880"}, -] -coverage = [ - {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, - {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, - {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, - {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, - {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, - {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, - {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, - {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, - {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, - {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, - {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, - {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, - {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, - {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, - {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, - {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, -] -cryptography = [ - {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, - {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"}, - {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"}, - {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"}, - {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"}, - {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"}, - {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, -] -decorator = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] -docutils = [ - {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, - {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, -] -ecdsa = [ - {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, - {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, -] -execnet = [ - {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, - {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, -] -flake8 = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, -] -Flask = [ - {file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"}, - {file = "Flask-2.2.2.tar.gz", hash = "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b"}, -] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] -imagesize = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, -] -itsdangerous = [ - {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, - {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, -] -Jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] -MarkupSafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -oscrypto = [ - {file = "oscrypto-1.3.0-py2.py3-none-any.whl", hash = "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085"}, - {file = "oscrypto-1.3.0.tar.gz", hash = "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pathspec = [ - {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, - {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, -] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -pprintpp = [ - {file = "pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d"}, - {file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, -] -Pygments = [ - {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, - {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, -] -PyNaCl = [ - {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, - {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, -] -pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, -] -pytest-forked = [ - {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, - {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"}, -] -pytest-xdist = [ - {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, - {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, -] -pytz = [ - {file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"}, - {file = "pytz-2022.2.1.tar.gz", hash = "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"}, -] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, -] -retry = [ - {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, - {file = "retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4"}, -] -setuptools = [ - {file = "setuptools-65.3.0-py3-none-any.whl", hash = "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82"}, - {file = "setuptools-65.3.0.tar.gz", hash = "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -snowballstemmer = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] -Sphinx = [ - {file = "Sphinx-4.3.2-py3-none-any.whl", hash = "sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851"}, - {file = "Sphinx-4.3.2.tar.gz", hash = "sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c"}, -] -sphinx-copybutton = [ - {file = "sphinx-copybutton-0.5.0.tar.gz", hash = "sha256:a0c059daadd03c27ba750da534a92a63e7a36a7736dcf684f26ee346199787f6"}, - {file = "sphinx_copybutton-0.5.0-py3-none-any.whl", hash = "sha256:9684dec7434bd73f0eea58dda93f9bb879d24bff2d8b187b1f2ec08dfe7b5f48"}, -] -sphinx-rtd-theme = [ - {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, - {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, -] -sphinxcontrib-applehelp = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, -] -sphinxcontrib-devhelp = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] -sphinxcontrib-htmlhelp = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, -] -sphinxcontrib-jsmath = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] -sphinxcontrib-qthelp = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] -sphinxcontrib-serializinghtml = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] -typeguard = [ - {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, - {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, -] -typing-extensions = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, -] -urllib3 = [ - {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, - {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, -] -websocket-client = [ - {file = "websocket-client-1.4.1.tar.gz", hash = "sha256:f9611eb65c8241a67fb373bef040b3cf8ad377a9f6546a12b620b6511e8ea9ef"}, - {file = "websocket_client-1.4.1-py3-none-any.whl", hash = "sha256:398909eb7e261f44b8f4bd474785b6ec5f5b499d4953342fe9755e01ef624090"}, -] -Werkzeug = [ - {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"}, - {file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"}, -] -zipp = [ - {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, - {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, -] \ No newline at end of file +alabaster = [] +asn1crypto = [] +attrs = [] +babel = [] +black = [] +blockfrost-python = [] +cbor2 = [] +certifi = [] +certvalidator = [] +cffi = [] +charset-normalizer = [] +click = [] +colorama = [] +cose = [] +coverage = [] +cryptography = [] +decorator = [] +docutils = [] +ecdsa = [] +ecpy = [] +execnet = [] +flake8 = [] +flask = [] +idna = [] +imagesize = [] +importlib-metadata = [] +iniconfig = [] +isort = [] +itsdangerous = [] +jinja2 = [] +markupsafe = [] +mccabe = [] +mnemonic = [] +mypy-extensions = [] +oscrypto = [] +packaging = [] +pathspec = [] +platformdirs = [] +pluggy = [] +pprintpp = [] +py = [] +pycodestyle = [] +pycparser = [] +pyflakes = [] +pygments = [] +pynacl = [] +pyparsing = [] +pytest = [] +pytest-cov = [] +pytest-forked = [] +pytest-xdist = [] +pytz = [] +requests = [] +retry = [] +six = [] +snowballstemmer = [] +sphinx = [] +sphinx-copybutton = [] +sphinx-rtd-theme = [] +sphinxcontrib-applehelp = [] +sphinxcontrib-devhelp = [] +sphinxcontrib-htmlhelp = [] +sphinxcontrib-jsmath = [] +sphinxcontrib-qthelp = [] +sphinxcontrib-serializinghtml = [] +tomli = [] +typed-ast = [] +typeguard = [] +typing-extensions = [] +urllib3 = [] +websocket-client = [] +werkzeug = [] +zipp = [] From 80b4373ad5636e31070e6b8af143137b01cdf7a9 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Tue, 4 Oct 2022 23:22:09 -0400 Subject: [PATCH 30/36] made changes to hdwallet and incorporated into bip32 --- pycardano/crypto/bip32.py | 562 +++++++++++++++++++++++++++ pycardano/hdwallet.py | 485 ----------------------- pycardano/key.py | 15 +- test/pycardano/backend/test_bip32.py | 110 ++++++ test/pycardano/test_hdwallet.py | 70 ---- 5 files changed, 686 insertions(+), 556 deletions(-) delete mode 100644 pycardano/hdwallet.py create mode 100644 test/pycardano/backend/test_bip32.py delete mode 100644 test/pycardano/test_hdwallet.py diff --git a/pycardano/crypto/bip32.py b/pycardano/crypto/bip32.py index 3f097aab..039ab1e1 100644 --- a/pycardano/crypto/bip32.py +++ b/pycardano/crypto/bip32.py @@ -8,7 +8,12 @@ from __future__ import annotations import hashlib +import hmac +import unicodedata +from binascii import hexlify, unhexlify +from typing import Optional +from mnemonic import Mnemonic from nacl import bindings @@ -48,3 +53,560 @@ def from_private_key( def verify(self, signature, message): return bindings.crypto_sign_open(signature + message, self.public_key) + + +def _Fk(message, secret): + return hmac.new(secret, message, hashlib.sha512).digest() + + +class HDWallet: + """ + Hierarchical Deterministic Wallet for Cardano + """ + + def __init__( + self, + seed: Optional[bytes] = None, + mnemonic: Optional[str] = None, + passphrase: Optional[str] = None, + entropy: Optional[str] = None, + root_xprivate_key: Optional[bytes] = None, + root_public_key: Optional[bytes] = None, + root_chain_code: Optional[bytes] = None, + xprivate_key: Optional[bytes] = None, + public_key: Optional[bytes] = None, + chain_code: Optional[bytes] = None, + path: Optional[str] = None, + ): + + self._seed = seed + self._mnemonic = mnemonic + self._passphrase = passphrase + self._entropy = entropy + + self._root_xprivate_key = root_xprivate_key + self._root_public_key = root_public_key + self._root_chain_code = root_chain_code + + self._xprivate_key = xprivate_key + self._public_key = public_key + self._chain_code = chain_code + + self._path = path if path else "m" + + @classmethod + def from_seed( + cls, + seed: str, + entropy: Optional[str] = None, + passphrase: Optional[str] = None, + mnemonic: Optional[str] = None, + ) -> HDWallet: + """ + Create an HDWallet instance from master key. + + Args: + seed: Master key of 96 bytes from seed hex string. + + Returns: + HDWallet -- Hierarchical Deterministic Wallet instance. + """ + + seed = bytearray(bytes.fromhex(seed)) + seed_modified = cls._tweak_bits(seed) + + kL, c = seed_modified[:32], seed_modified[64:] + + # root public key + A = bindings.crypto_scalarmult_ed25519_base_noclamp(kL) + + return cls( + seed=seed_modified, + mnemonic=mnemonic, + entropy=entropy, + passphrase=passphrase, + root_xprivate_key=seed_modified[:64], + root_public_key=A, + root_chain_code=c, + xprivate_key=seed_modified[:64], + public_key=A, + chain_code=c, + ) + + @classmethod + def from_mnemonic(cls, mnemonic: str, passphrase: str = "") -> HDWallet: + """ + Create master key and HDWallet from Mnemonic words. + + Args: + mnemonic: Mnemonic words. + passphrase: Mnemonic passphrase or password, default to ``None``. + + Returns: + HDWallet -- Hierarchical Deterministic Wallet instance. + """ + + if not cls.is_mnemonic(mnemonic=mnemonic): + raise ValueError("Invalid mnemonic words.") + + mnemonic = unicodedata.normalize("NFKD", mnemonic) + passphrase = str(passphrase) if passphrase else "" + entropy = Mnemonic(language="english").to_entropy(words=mnemonic) + + seed = bytearray( + hashlib.pbkdf2_hmac( + "sha512", + password=passphrase.encode(), + salt=entropy, + iterations=4096, + dklen=96, + ) + ) + + return cls.from_seed( + seed=hexlify(seed).decode(), + mnemonic=mnemonic, + entropy=entropy, + passphrase=passphrase, + ) + + @classmethod + def from_entropy(cls, entropy: str, passphrase: str = None) -> HDWallet: + """ + Create master key and HDWallet from Mnemonic words. + + Args: + entropy: Entropy hex string. + passphrase: Mnemonic passphrase or password, default to ``None``. + + Returns: + HDWallet -- Hierarchical Deterministic Wallet instance. + """ + + if not cls.is_entropy(entropy): + raise ValueError("Invalid entropy") + + seed = bytearray( + hashlib.pbkdf2_hmac( + "sha512", password=passphrase, salt=entropy, iterations=4096, dklen=96 + ) + ) + return cls.from_seed(seed=hexlify(seed).decode(), entropy=entropy) + + @classmethod + def _tweak_bits(cls, seed: bytearray) -> bytes: + """ + Modify seed based on Icarus master node derivation scheme. + + The process follows + `CIP-0003#Wallet Key Generation `_. + + Process: + - clear the lowest 3 bits + - clear the highest 3 bits + - set the highest second bit + + Args: + seed: Seed in bytearray + + Returns: + modified seed in bytes. + """ + seed[0] &= 0b11111000 + seed[31] &= 0b00011111 + seed[31] |= 0b01000000 + + return bytes(seed) + + def _copy_hdwallet(self): + """ + Create a new instance of HDWallet + """ + + return HDWallet( + self._seed, + self._mnemonic, + self._passphrase, + self._entropy, + self._root_xprivate_key, + self._root_public_key, + self._root_chain_code, + self._xprivate_key, + self._public_key, + self._chain_code, + self._path, + ) + + def derive_from_path(self, path: str, private: bool = True) -> HDWallet: + """ + Derive keys from a path following CIP-1852 specifications. + + Args: + path: Derivation path for the key generation. + private: whether to derive private child keys or public child keys. + + Returns: + HDWallet instance with keys derived + + Examples: + >>> mnemonic_words = "test walk nut penalty hip pave soap entry language right filter choice" + >>> hdwallet = HDWallet.from_mnemonic(mnemonic_words) + >>> child_hdwallet = hdwallet.derive_from_path("m/1852'/1815'/0'/0/0") + >>> child_hdwallet.public_key.hex() + '73fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7d' + """ + + if path[:2] != "m/": + raise ValueError( + 'Bad path, please insert like this type of path "m/0\'/0"! ' + ) + + derived_hdwallet = self._copy_hdwallet() + + for index in path.lstrip("m/").split("/"): + if index.endswith("'"): + derived_hdwallet = self.derive_from_index( + derived_hdwallet, int(index[:-1]), private=private, hardened=True + ) + else: + derived_hdwallet = self.derive_from_index( + derived_hdwallet, int(index), private=private, hardened=False + ) + + return derived_hdwallet + + def derive_from_index( + self, + parent_wallet: HDWallet, + index: int, + private: bool = True, + hardened: bool = False, + ) -> HDWallet: + """ + Derive keys from index. + + Args: + index: Derivation index. + private: whether to derive private child keys or public child keys. + hardened: whether to derive hardened address. Default to False. + + Returns: + HDWallet instance with keys derived + + Examples: + >>> mnemonic_words = "test walk nut penalty hip pave soap entry language right filter choice" + >>> hdwallet = HDWallet.from_mnemonic(mnemonic_words) + >>> hdwallet_l1 = hdwallet.derive_from_index(parent_wallet=hdwallet, index=1852, hardened=True) + >>> hdwallet_l2 = hdwallet.derive_from_index(parent_wallet=hdwallet_l1, index=1815, hardened=True) + >>> hdwallet_l3 = hdwallet.derive_from_index(parent_wallet=hdwallet_l2, index=0, hardened=True) + >>> hdwallet_l4 = hdwallet.derive_from_index(parent_wallet=hdwallet_l3, index=0) + >>> hdwallet_l5 = hdwallet.derive_from_index(parent_wallet=hdwallet_l4, index=0) + >>> hdwallet_l5.public_key.hex() + '73fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7d' + """ + + if not isinstance(index, int): + raise ValueError("Bad index, Please import only integer number!") + + if not self._root_xprivate_key and not self._root_public_key: + raise ValueError("Missing root keys. Can't do derivation.") + + if hardened: + index += 2**31 + + # derive private child key + if private: + node = ( + parent_wallet._xprivate_key[:32], + parent_wallet._xprivate_key[32:], + parent_wallet._public_key, + parent_wallet._chain_code, + parent_wallet._path, + ) + derived_hdwallet = self._derive_private_child_key_by_index(node, index) + # derive public child key + else: + node = ( + parent_wallet._public_key, + parent_wallet._chain_code, + parent_wallet._path, + ) + derived_hdwallet = self._derive_public_child_key_by_index(node, index) + + return derived_hdwallet + + def _derive_private_child_key_by_index( + self, private_pnode: (bytes, bytes, bytes, bytes, str), index: int + ) -> HDWallet: + """ + Derive private child keys from parent node. + + PROCESS: + 1. encode i 4-bytes little endian, il = encode_U32LE(i) + 2. if i is less than 2^31 + - compute Z = HMAC-SHA512(key=c, Data=0x02 | A | il ) + - compute c_ = HMAC-SHA512(key=c, Data=0x03 | A | il ) + else + - compute Z = HMAC-SHA512(key=c, Data=0x00 | kL | kR | il ) + - compute c_ = HMAC-SHA512(key=c, Data=0x01 | kL | kR | il ) + 3. ci = lowest_32bytes(c_) + 4. set ZL = highest_28bytes(Z) + set ZR = lowest_32bytes(Z) + 5. compute kL_i: + zl_ = LEBytes_to_int(ZL) + kL_ = LEBytes_to_int(kL) + kLi_ = zl_*8 + kL_ + if kLi_ % order == 0: child does not exist + kL_i = int_to_LEBytes(kLi_) + 6. compute kR_i + zr_ = LEBytes_to_int(ZR) + kR_ = LEBytes_to_int(kR) + kRi_ = (zr_ + kRn_) % 2^256 + kR_i = int_to_LEBytes(kRi_) + 7. compute A + A = kLi_.G + 8. return ((kL_i,kR_i), A_i, c) + + Args: + private_pnode: ((kLP,kRP), AP, cP). (kLP,kRP) is 64 bytes parent private eddsa key, + AP is 32 btyes parent public key, cP is 32 btyes parent chain code. + index: child index to compute (hardened if >= 0x80000000) + + Returns: + HDWallet with child node derived. + + """ + + if not private_pnode: + return None + + # unpack argument + (kLP, kRP, AP, cP, path) = private_pnode + assert 0 <= index < 2**32 + + i_bytes = index.to_bytes(4, "little") + + # compute Z,c + if index < 2**31: + # regular child + Z = _Fk(b"\x02" + AP + i_bytes, cP) + c = _Fk(b"\x03" + AP + i_bytes, cP)[32:] + else: + # harderned child + Z = _Fk(b"\x00" + (kLP + kRP) + i_bytes, cP) + c = _Fk(b"\x01" + (kLP + kRP) + i_bytes, cP)[32:] + + ZL, ZR = Z[:28], Z[32:] + + # compute KLi + kLn = int.from_bytes(ZL, "little") * 8 + int.from_bytes(kLP, "little") + + # compute KRi + kRn = (int.from_bytes(ZR, "little") + int.from_bytes(kRP, "little")) % 2**256 + + kL = kLn.to_bytes(32, "little") + kR = kRn.to_bytes(32, "little") + + # compue Ai + A = bindings.crypto_scalarmult_ed25519_base_noclamp(kL) + + # compute path + path += "/" + str(index) + + derived_hdwallet = HDWallet( + xprivate_key=kL + kR, public_key=A, chain_code=c, path=path + ) + + return derived_hdwallet + + def _derive_public_child_key_by_index( + self, public_pnode: (bytes, bytes, str), index: int + ) -> HDWallet: + """ + Derive public child keys from parent node. + + Args: + public_pnode: (AP, cP). AP is 32 btyes parent public key, cP is 32 btyes parent chain code. + index: child index to compute (hardened if >= 0x80000000) + + Returns: + HDWallet with child node derived. + """ + + if not public_pnode: + return None + + # unpack argument + (AP, cP, path) = public_pnode + assert 0 <= index < 2**32 + + i_bytes = index.to_bytes(4, "little") + + # compute Z,c + if index < 2**31: + # regular child + Z = _Fk(b"\x02" + AP + i_bytes, cP) + c = _Fk(b"\x03" + AP + i_bytes, cP)[32:] + else: + # can't derive hardened child from public keys + raise ValueError("Cannot derive hardened index with public key") + + ZL = Z[:28] + + # compute ZLi + ZLint = int.from_bytes(ZL, "little") + + # compue Ai + A = bindings.crypto_core_ed25519_add( + AP, + bindings.crypto_scalarmult_ed25519_base_noclamp( + (8 * ZLint).to_bytes(32, "little") + ), + ) + + # compute path + path += "/" + str(index) + + derived_hdwallet = HDWallet(public_key=A, chain_code=c, path=path) + + return derived_hdwallet + + @property + def root_xprivate_key(self): + return self._root_xprivate_key + # return None if not self._root_xprivate_key else self._root_xprivate_key.hex() + + @property + def root_public_key(self): + return self._root_public_key + # return None if not self._root_public_key else self._root_public_key.hex() + + @property + def root_chain_code(self): + return self._root_chain_code + # return None if not self._root_chain_code else self._root_chain_code.hex() + + @property + def xprivate_key(self): + return self._xprivate_key + # return None if not self._xprivate_key else self._xprivate_key.hex() + + @property + def public_key(self): + return self._public_key + # return None if not self._public_key else self._public_key.hex() + + @property + def chain_code(self): + return self._chain_code + # return None if not self._chain_code else self._chain_code.hex() + + # def to_extended_signing_key(self) -> BIP32ED25519PrivateKey: + # """ + # Generate a extended signing key to sign transactions + # """ + # return BIP32ED25519PrivateKey(self._xprivate_key, self._chain_code) + + @staticmethod + def generate_mnemonic(language: str = "english", strength: int = 256) -> str: + """ + Generate mnemonic words. + + Args: + language (str): language for the mnemonic words. + strength (int): length of the mnemoic words. Valid values are 128/160/192/224/256. + + Returns: + mnemonic (str): mnemonic words. + """ + + if language and language not in [ + "english", + "french", + "italian", + "japanese", + "chinese_simplified", + "chinese_traditional", + "korean", + "spanish", + ]: + raise ValueError( + "invalid language, use only this options english, french, " + "italian, spanish, chinese_simplified, chinese_traditional, japanese or korean languages." + ) + if strength not in [128, 160, 192, 224, 256]: + raise ValueError( + "Strength should be one of the following " + "[128, 160, 192, 224, 256], but it is not (%d)." % strength + ) + + return Mnemonic(language=language).generate(strength=strength) + + @staticmethod + def is_mnemonic(mnemonic: str, language: Optional[str] = None) -> bool: + """ + Check if mnemonic words are valid. + + Args: + mnemonic (str): Mnemonic words in string format. + language (Optional[str]): Mnemonic language, default to None. + + Returns: + bool. Whether the input mnemonic words is valid. + """ + + if language and language not in [ + "english", + "french", + "italian", + "japanese", + "chinese_simplified", + "chinese_traditional", + "korean", + "spanish", + ]: + raise ValueError( + "invalid language, use only this options english, french, " + "italian, spanish, chinese_simplified, chinese_traditional, japanese or korean languages." + ) + try: + mnemonic = unicodedata.normalize("NFKD", mnemonic) + if language is None: + for _language in [ + "english", + "french", + "italian", + "chinese_simplified", + "chinese_traditional", + "japanese", + "korean", + "spanish", + ]: + valid = False + if Mnemonic(language=_language).check(mnemonic=mnemonic) is True: + valid = True + break + return valid + else: + return Mnemonic(language=language).check(mnemonic=mnemonic) + except ValueError: + print( + "The input mnemonic words are not valid. Words should be in string format seperated by space." + ) + + @staticmethod + def is_entropy(entropy: str) -> bool: + """ + Check entropy hex string. + + Args: + entropy: entropy converted from mnemonic words. + + Returns: + bool. Whether entropy is valid or not. + """ + + try: + return len(unhexlify(entropy)) in [16, 20, 24, 28, 32] + except ValueError: + print("The input entropy is not valid.") diff --git a/pycardano/hdwallet.py b/pycardano/hdwallet.py deleted file mode 100644 index 5bdbb382..00000000 --- a/pycardano/hdwallet.py +++ /dev/null @@ -1,485 +0,0 @@ -import hashlib -import hmac -import unicodedata -from binascii import hexlify, unhexlify -from typing import Optional, Union - -from ecpy.curves import Curve -from mnemonic import Mnemonic - - -def _Fk(message, secret): - return hmac.new(secret, message, hashlib.sha512).digest() - - -class HDWallet: - """ - Hierarchical Deterministic Wallet for Cardano - """ - - def __int__(self): - self._seed: Optional[bytes] = None - self._mnemonic: Optional[str] = None - self._passphrase: Optional[str] = None - self._entropy: Optional[str] = None - - self._root_xprivate_key: Optional[tuple] = None - self._root_public_key: Optional[bytes] = None - self._root_chain_code: Optional[bytes] = None - self._xprivate_key: Optional[tuple] = None - self._public_key: Optional[bytes] = None - self._chain_code: Optional[bytes] = None - self._path: str = "m" - self._depth: int = 0 - self._index: int = 0 - - def from_seed(self, seed: str) -> "HDWallet": - """ - Create an HDWallet instance from master key. - - Args: - seed: Master key of 96 bytes from seed hex string. - - Returns: - HDWallet -- Hierarchical Deterministic Wallet instance. - """ - - seed = bytearray(bytes.fromhex(seed)) - seed_modified = self._tweak_bits(seed) - self._seed = seed_modified - - kL, kR, c = seed_modified[:32], seed_modified[32:64], seed_modified[64:] - - # root public key - cv25519 = Curve.get_curve("Ed25519") - k_scalar = int.from_bytes(bytes(kL), "little") - P = k_scalar * cv25519.generator - A = cv25519.encode_point(P) - - # set root keys and parent keys - self._root_xprivate_key = self._xprivate_key = (kL, kR) - self._root_public_key = self._public_key = A - self._root_chain_code = self._chain_code = c - - return self - - def from_mnemonic( - self, mnemonic: Union[str, list], passphrase: str = "" - ) -> "HDWallet": - """ - Create master key and HDWallet from Mnemonic words. - - Args: - mnemonic: Mnemonic words. - passphrase: Mnemonic passphrase or password, default to ``None``. - - Returns: - HDWallet -- Hierarchical Deterministic Wallet instance. - """ - - if not self.is_mnemonic(mnemonic=mnemonic): - raise ValueError("Invalid mnemonic words.") - - self._mnemonic = unicodedata.normalize("NFKD", mnemonic) - self._passphrase = str(passphrase) if passphrase else "" - - entropy = Mnemonic(language="english").to_entropy(words=mnemonic) - self._entropy = hexlify(entropy).decode() - - seed = bytearray( - hashlib.pbkdf2_hmac( - "sha512", - password=passphrase.encode(), - salt=entropy, - iterations=4096, - dklen=96, - ) - ) - - return self.from_seed(seed=hexlify(seed).decode()) - - def from_entropy(self, entropy: str, passphrase: str = None) -> "HDWallet": - """ - Create master key and HDWallet from Mnemonic words. - - Args: - entropy: Entropy hex string. - passphrase: Mnemonic passphrase or password, default to ``None``. - - Returns: - HDWallet -- Hierarchical Deterministic Wallet instance. - """ - - if not self.is_entropy(entropy): - raise ValueError("Invalid entropy") - - self._entropy = entropy - - seed = bytearray( - hashlib.pbkdf2_hmac( - "sha512", password=passphrase, salt=entropy, iterations=4096, dklen=96 - ) - ) - return self.from_seed(seed=hexlify(seed).decode()) - - def _tweak_bits(self, seed: bytearray) -> bytearray: - """ - Modify seed based on Icarus master node derivation scheme. - - The process follows - `CIP-0003#Wallet Key Generation `_. - - Process: - - clear the lowest 3 bits - - clear the highest 3 bits - - set the highest second bit - - Args: - seed: Seed in bytearray - - Returns: - modified bytearray seed. - """ - seed[0] &= 0b11111000 - seed[31] &= 0b00011111 - seed[31] |= 0b01000000 - - return seed - - def derive_from_path(self, path: str, private: bool = True) -> "HDWallet": - """ - Derive keys from a path following CIP-1852 specifications. - - Args: - path: Derivation path for the key generation. - private: whether to derive private child keys or public child keys. - - Returns: - HDWallet instance with keys derived - - Examples: - >>> mnemonic_words = "test walk nut penalty hip pave soap entry language right filter choice" - >>> hdwallet = HDWallet().from_mnemonic(mnemonic_words) - >>> hdwallet.derive_from_path("m/1852'/1815'/0'/0/0") - >>> hdwallet.public_key - '73fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7d' - """ - - if path[:2] != "m/": - raise ValueError( - 'Bad path, please insert like this type of path "m/0\'/0"! ' - ) - - for index in path.lstrip("m/").split("/"): - if index.endswith("'"): - self.derive_from_index(int(index[:-1]), private=private, hardened=True) - else: - self.derive_from_index(int(index), private=private, hardened=False) - - def derive_from_index( - self, index: int, private: bool = True, hardened: bool = False - ) -> "HDWallet": - """ - Derive keys from index. - - Args: - index: Derivation index. - private: whether to derive private child keys or public child keys. - hardened: whether to derive hardened address. Default to False. - - Returns: - HDWallet instance with keys derived - - Examples: - >>> mnemonic_words = "test walk nut penalty hip pave soap entry language right filter choice" - >>> hdwallet = HDWallet().from_mnemonic(mnemonic_words) - >>> hdwallet.derive_from_index(index=1852, hardened=True) - >>> hdwallet.derive_from_index(index=1815, hardened=True) - >>> hdwallet.derive_from_index(index=0, hardened=True) - >>> hdwallet.derive_from_index(index=0) - >>> hdwallet.derive_from_index(index=0) - >>> hdwallet.public_key - '73fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7d' - """ - - if not isinstance(index, int): - raise ValueError("Bad index, Please import only integer number!") - - if not self._root_xprivate_key and not self._root_public_key: - raise ValueError("Missing root keys. Can't do derivation.") - - if hardened: - index += 2**31 - - # derive private child key - if private: - node = (self._xprivate_key, self._public_key, self._chain_code) - self._derive_private_child_key_by_index(node, index) - # derive public child key - else: - node = (self._public_key, self._chain_code) - self._derive_public_child_key_by_index(node, index) - - def _derive_private_child_key_by_index( - self, private_pnode: ((bytes, bytes), bytes, bytes), index: int - ) -> Optional["HDWallet"]: - """ - Derive private child keys from parent node. - - PROCESS: - 1. encode i 4-bytes little endian, il = encode_U32LE(i) - 2. if i is less than 2^31 - - compute Z = HMAC-SHA512(key=c, Data=0x02 | A | il ) - - compute c_ = HMAC-SHA512(key=c, Data=0x03 | A | il ) - else - - compute Z = HMAC-SHA512(key=c, Data=0x00 | kL | kR | il ) - - compute c_ = HMAC-SHA512(key=c, Data=0x01 | kL | kR | il ) - 3. ci = lowest_32bytes(c_) - 4. set ZL = highest_28bytes(Z) - set ZR = lowest_32bytes(Z) - 5. compute kL_i: - zl_ = LEBytes_to_int(ZL) - kL_ = LEBytes_to_int(kL) - kLi_ = zl_*8 + kL_ - if kLi_ % order == 0: child does not exist - kL_i = int_to_LEBytes(kLi_) - 6. compute kR_i - zr_ = LEBytes_to_int(ZR) - kR_ = LEBytes_to_int(kR) - kRi_ = (zr_ + kRn_) % 2^256 - kR_i = int_to_LEBytes(kRi_) - 7. compute A - A = kLi_.G - 8. return ((kL_i,kR_i), A_i, c) - - Args: - private_pnode: ((kLP,kRP), AP, cP). (kLP,kRP) is 64 bytes parent private eddsa key, - AP is 32 btyes parent public key, cP is 32 btyes parent chain code. - index: child index to compute (hardened if >= 0x80000000) - - Returns: - HDWallet with child node derived. - - """ - - if not private_pnode: - return None - - # unpack argument - ((kLP, kRP), AP, cP) = private_pnode - assert 0 <= index < 2**32 - - i_bytes = index.to_bytes(4, "little") - - # compute Z,c - if index < 2**31: - # regular child - Z = _Fk(b"\x02" + AP + i_bytes, cP) - c = _Fk(b"\x03" + AP + i_bytes, cP)[32:] - else: - # harderned child - Z = _Fk(b"\x00" + (kLP + kRP) + i_bytes, cP) - c = _Fk(b"\x01" + (kLP + kRP) + i_bytes, cP)[32:] - - ZL, ZR = Z[:28], Z[32:] - - # compute KLi - kLn = int.from_bytes(ZL, "little") * 8 + int.from_bytes(kLP, "little") - - # compute KRi - kRn = (int.from_bytes(ZR, "little") + int.from_bytes(kRP, "little")) % 2**256 - - kL = kLn.to_bytes(32, "little") - kR = kRn.to_bytes(32, "little") - - # compue Ai - cv25519 = Curve.get_curve("Ed25519") - k_scalar = int.from_bytes(kL, "little") - P = k_scalar * cv25519.generator - A = cv25519.encode_point(P) - - self._xprivate_key = (kL, kR) - self._public_key = A - self._chain_code = c - - return self - - def _derive_public_child_key_by_index( - self, public_pnode: (bytes, bytes), index: int - ) -> Optional["HDWallet"]: - """ - Derive public child keys from parent node. - - Args: - public_pnode: (AP, cP). AP is 32 btyes parent public key, cP is 32 btyes parent chain code. - index: child index to compute (hardened if >= 0x80000000) - - Returns: - HDWallet with child node derived. - """ - - if not public_pnode: - return None - - # unpack argument - (AP, cP) = public_pnode - assert 0 <= index < 2**32 - - i_bytes = index.to_bytes(4, "little") - - # compute Z,c - if index < 2**31: - # regular child - Z = _Fk(b"\x02" + AP + i_bytes, cP) - c = _Fk(b"\x03" + AP + i_bytes, cP)[32:] - else: - # can't derive hardened child from public keys - return None - - ZL = Z[:28] - - # compute ZLi - ZLint = int.from_bytes(ZL, "little") - ZLint_x_8 = 8 * ZLint - - # compue Ai - cv25519 = Curve.get_curve("Ed25519") - P = ZLint_x_8 * cv25519.generator - Q = cv25519.decode_point(AP) - PQ = P + Q - A = cv25519.encode_point(PQ) - - self._public_key = A - self._chain_code = c - - return self - - @property - def root_xprivate_key(self): - return (self._root_xprivate_key[0].hex(), self._root_xprivate_key[1].hex()) - - @property - def root_public_key(self): - return None if not self._root_public_key else self._root_public_key.hex() - - @property - def root_chain_code(self): - return None if not self._root_chain_code else self._root_chain_code.hex() - - @property - def xprivate_key(self): - return ( - (None, None) - if not self._xprivate_key - else (self._xprivate_key[0].hex(), self._xprivate_key[1].hex()) - ) - - @property - def public_key(self): - return None if not self._public_key else self._public_key.hex() - - @property - def chain_code(self): - return None if not self._chain_code else self._chain_code.hex() - - @staticmethod - def generate_mnemonic(language: str = "english", strength: int = 256) -> str: - """ - Generate mnemonic words. - - Args: - language (str): language for the mnemonic words. - strength (int): length of the mnemoic words. Valid values are 128/160/192/224/256. - - Returns: - mnemonic (str): mnemonic words. - """ - - if language and language not in [ - "english", - "french", - "italian", - "japanese", - "chinese_simplified", - "chinese_traditional", - "korean", - "spanish", - ]: - raise ValueError( - "invalid language, use only this options english, french, " - "italian, spanish, chinese_simplified, chinese_traditional, japanese or korean languages." - ) - if strength not in [128, 160, 192, 224, 256]: - raise ValueError( - "Strength should be one of the following " - "[128, 160, 192, 224, 256], but it is not (%d)." % strength - ) - - return Mnemonic(language=language).generate(strength=strength) - - @staticmethod - def is_mnemonic(mnemonic: str, language: Optional[str] = None) -> bool: - """ - Check if mnemonic words are valid. - - Args: - mnemonic (str): Mnemonic words in string format. - language (Optional[str]): Mnemonic language, default to None. - - Returns: - bool. Whether the input mnemonic words is valid. - """ - - if language and language not in [ - "english", - "french", - "italian", - "japanese", - "chinese_simplified", - "chinese_traditional", - "korean", - "spanish", - ]: - raise ValueError( - "invalid language, use only this options english, french, " - "italian, spanish, chinese_simplified, chinese_traditional, japanese or korean languages." - ) - try: - mnemonic = unicodedata.normalize("NFKD", mnemonic) - if language is None: - for _language in [ - "english", - "french", - "italian", - "chinese_simplified", - "chinese_traditional", - "japanese", - "korean", - "spanish", - ]: - valid = False - if Mnemonic(language=_language).check(mnemonic=mnemonic) is True: - valid = True - break - return valid - else: - return Mnemonic(language=language).check(mnemonic=mnemonic) - except ValueError: - print("The input mnemonic words are not valid. Words should be in string format seperated by space.") - - @staticmethod - def is_entropy(entropy: str) -> bool: - """ - Check entropy hex string. - - Args: - entropy: entropy converted from mnemonic words. - - Returns: - bool. Whether entropy is valid or not. - """ - - try: - return len(unhexlify(entropy)) in [16, 20, 24, 28, 32] - except ValueError: - print("The input entropy is not valid.") diff --git a/pycardano/key.py b/pycardano/key.py index 48d9bd06..87587a7d 100644 --- a/pycardano/key.py +++ b/pycardano/key.py @@ -10,7 +10,7 @@ from nacl.public import PrivateKey from nacl.signing import SigningKey as NACLSigningKey -from pycardano.crypto.bip32 import BIP32ED25519PrivateKey +from pycardano.crypto.bip32 import BIP32ED25519PrivateKey, HDWallet from pycardano.exception import InvalidKeyTypeException from pycardano.hash import VERIFICATION_KEY_HASH_SIZE, VerificationKeyHash from pycardano.serialization import CBORSerializable @@ -185,6 +185,19 @@ def to_verification_key(self) -> ExtendedVerificationKey: self.description.replace("Signing", "Verification"), ) + @classmethod + def from_hdwallet(cls, hdwallet: HDWallet) -> ExtendedSigningKey: + if hdwallet.xprivate_key is None or hdwallet.chain_code is None: + raise InvalidKeyTypeException( + "The hdwallet doesn't contain extended private key or chain code info." + ) + + return Key( + payload=hdwallet.xprivate_key + hdwallet.public_key + hdwallet.chain_code, + type="PaymentExtendedSigningKeyShelley_ed25519_bip32", + description="Payment Signing Key", + ) + class ExtendedVerificationKey(Key): def hash(self) -> VerificationKeyHash: diff --git a/test/pycardano/backend/test_bip32.py b/test/pycardano/backend/test_bip32.py new file mode 100644 index 00000000..21097ed0 --- /dev/null +++ b/test/pycardano/backend/test_bip32.py @@ -0,0 +1,110 @@ +from pycardano.address import Address +from pycardano.crypto.bip32 import HDWallet +from pycardano.key import PaymentVerificationKey +from pycardano.network import Network + +# Tests copied from: https://github.com/Emurgo/cardano-serialization-lib/blob/master/rust/src/address.rs + +MNEMONIC_12 = "test walk nut penalty hip pave soap entry language right filter choice" +MNEMONIC_15 = "art forum devote street sure rather head chuckle guard poverty release quote oak craft enemy" +MNEMONIC_24 = "excess behave track soul table wear ocean cash stay nature item turtle palm soccer lunch horror start stumble month panic right must lock dress" + + +def test_mnemonic(): + wrong_mnemonic = "test walk nut penalty hip pave soap entry language right filter" + assert not HDWallet.is_mnemonic(wrong_mnemonic) + + +def test_mnemonic_generation(): + mnemonic_words = HDWallet.generate_mnemonic(strength=128) + assert HDWallet.is_mnemonic(mnemonic_words) + + +def test_payment_address_12_reward(): + hdwallet = HDWallet.from_mnemonic(MNEMONIC_12) + hdwallet_stake = hdwallet.derive_from_path("m/1852'/1815'/0'/2/0") + stake_public_key = hdwallet_stake.public_key + stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) + + assert ( + Address( + payment_part=None, staking_part=stake_vk.hash(), network=Network.TESTNET + ).encode() + == "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl" + ) + + +def test_payment_address_12_reward2(): + hdwallet = HDWallet.from_mnemonic(MNEMONIC_12) + hdwalletl_l1 = hdwallet.derive_from_index( + hdwallet, 1852, private=True, hardened=True + ) + hdwalletl_l2 = hdwallet.derive_from_index( + hdwalletl_l1, 1815, private=True, hardened=True + ) + hdwalletl_l3 = hdwallet.derive_from_index( + hdwalletl_l2, 0, private=True, hardened=True + ) + hdwalletl_l4 = hdwallet.derive_from_index( + hdwalletl_l3, 2, private=False, hardened=False + ) + hdwallet_stake = hdwallet.derive_from_index( + hdwalletl_l4, 0, private=False, hardened=False + ) + stake_public_key = hdwallet_stake.public_key + stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) + + assert ( + Address( + payment_part=None, staking_part=stake_vk.hash(), network=Network.TESTNET + ).encode() + == "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl" + ) + + +def test_payment_address_12_base(): + hdwallet = HDWallet.from_mnemonic(MNEMONIC_12) + hdwallet_spend = hdwallet.derive_from_path("m/1852'/1815'/0'/0/0") + spend_public_key = hdwallet_spend.public_key + spend_vk = PaymentVerificationKey.from_primitive(spend_public_key) + + hdwallet_stake = hdwallet.derive_from_path("m/1852'/1815'/0'/2/0") + stake_public_key = hdwallet_stake.public_key + stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) + + assert ( + Address(spend_vk.hash(), stake_vk.hash(), network=Network.TESTNET).encode() + == "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp" + ) + + +def test_payment_address_15_base(): + hdwallet = HDWallet.from_mnemonic(MNEMONIC_15) + hdwallet_spend = hdwallet.derive_from_path("m/1852'/1815'/0'/0/0") + spend_public_key = hdwallet_spend.public_key + spend_vk = PaymentVerificationKey.from_primitive(spend_public_key) + + hdwallet_stake = hdwallet.derive_from_path("m/1852'/1815'/0'/2/0") + stake_public_key = hdwallet_stake.public_key + stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) + + assert ( + Address(spend_vk.hash(), stake_vk.hash(), network=Network.MAINNET).encode() + == "addr1q9u5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qld6xc3" + ) + + +def test_payment_address_24_base(): + hdwallet = HDWallet.from_mnemonic(MNEMONIC_24) + hdwallet_spend = hdwallet.derive_from_path("m/1852'/1815'/0'/0/0") + spend_public_key = hdwallet_spend.public_key + spend_vk = PaymentVerificationKey.from_primitive(spend_public_key) + + hdwallet_stake = hdwallet.derive_from_path("m/1852'/1815'/0'/2/0") + stake_public_key = hdwallet_stake.public_key + stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) + + assert ( + Address(spend_vk.hash(), stake_vk.hash(), network=Network.MAINNET).encode() + == "addr1qyy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmn8k8ttq8f3gag0h89aepvx3xf69g0l9pf80tqv7cve0l33sdn8p3d" + ) diff --git a/test/pycardano/test_hdwallet.py b/test/pycardano/test_hdwallet.py deleted file mode 100644 index 80b605d7..00000000 --- a/test/pycardano/test_hdwallet.py +++ /dev/null @@ -1,70 +0,0 @@ -from pycardano.address import Address -from pycardano.hdwallet import HDWallet -from pycardano.key import PaymentVerificationKey -from pycardano.network import Network - - -# Tests copied from: https://github.com/Emurgo/cardano-serialization-lib/blob/master/rust/src/address.rs - -MNEMONIC_12 = "test walk nut penalty hip pave soap entry language right filter choice" -MNEMONIC_15 = "art forum devote street sure rather head chuckle guard poverty release quote oak craft enemy" - -def test_mnemonic(): - wrong_mnemonic = "test walk nut penalty hip pave soap entry language right filter" - hdwallet = HDWallet() - assert not hdwallet.is_mnemonic(wrong_mnemonic) - - -def test_mnemonic_generation(): - hdwallet = HDWallet() - mnemonic_words = hdwallet.generate_mnemonic(strength=128) - assert hdwallet.is_mnemonic(mnemonic_words) - - -def test_payment_address_12_reward(): - hdwallet_stake = HDWallet() - hdwallet_stake.from_mnemonic(MNEMONIC_12) - hdwallet_stake.derive_from_path("m/1852'/1815'/0'/2/0") - stake_public_key = hdwallet_stake.public_key - stake_vk = PaymentVerificationKey.from_primitive(bytes.fromhex(stake_public_key)) - - assert ( - Address( - payment_part=None, staking_part=stake_vk.hash(), network=Network.TESTNET - ).encode() - == "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl" - ) - - -def test_payment_address_12_base(): - hdwallet_spend = HDWallet().from_mnemonic(MNEMONIC_12) - hdwallet_spend.derive_from_path("m/1852'/1815'/0'/0/0") - spend_public_key = hdwallet_spend.public_key - spend_vk = PaymentVerificationKey.from_primitive(bytes.fromhex(spend_public_key)) - - hdwallet_stake = HDWallet().from_mnemonic(MNEMONIC_12) - hdwallet_stake.derive_from_path("m/1852'/1815'/0'/2/0") - stake_public_key = hdwallet_stake.public_key - stake_vk = PaymentVerificationKey.from_primitive(bytes.fromhex(stake_public_key)) - - assert ( - Address(spend_vk.hash(), stake_vk.hash(), network=Network.TESTNET).encode() - == "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp" - ) - - -def test_payment_address_15_base(): - hdwallet_spend = HDWallet().from_mnemonic(MNEMONIC_15) - hdwallet_spend.derive_from_path("m/1852'/1815'/0'/0/0") - spend_public_key = hdwallet_spend.public_key - spend_vk = PaymentVerificationKey.from_primitive(bytes.fromhex(spend_public_key)) - - hdwallet_stake = HDWallet().from_mnemonic(MNEMONIC_15) - hdwallet_stake.derive_from_path("m/1852'/1815'/0'/2/0") - stake_public_key = hdwallet_stake.public_key - stake_vk = PaymentVerificationKey.from_primitive(bytes.fromhex(stake_public_key)) - - assert ( - Address(spend_vk.hash(), stake_vk.hash(), network=Network.MAINNET).encode() - == "addr1q9u5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qld6xc3" - ) From d62e2a18e7e172d564fd2a568c9a574febdd2a59 Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Tue, 4 Oct 2022 23:46:01 -0400 Subject: [PATCH 31/36] remove commented code block --- pycardano/crypto/bip32.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pycardano/crypto/bip32.py b/pycardano/crypto/bip32.py index 039ab1e1..a26c5650 100644 --- a/pycardano/crypto/bip32.py +++ b/pycardano/crypto/bip32.py @@ -474,38 +474,26 @@ def _derive_public_child_key_by_index( @property def root_xprivate_key(self): return self._root_xprivate_key - # return None if not self._root_xprivate_key else self._root_xprivate_key.hex() @property def root_public_key(self): return self._root_public_key - # return None if not self._root_public_key else self._root_public_key.hex() @property def root_chain_code(self): return self._root_chain_code - # return None if not self._root_chain_code else self._root_chain_code.hex() @property def xprivate_key(self): return self._xprivate_key - # return None if not self._xprivate_key else self._xprivate_key.hex() @property def public_key(self): return self._public_key - # return None if not self._public_key else self._public_key.hex() @property def chain_code(self): return self._chain_code - # return None if not self._chain_code else self._chain_code.hex() - - # def to_extended_signing_key(self) -> BIP32ED25519PrivateKey: - # """ - # Generate a extended signing key to sign transactions - # """ - # return BIP32ED25519PrivateKey(self._xprivate_key, self._chain_code) @staticmethod def generate_mnemonic(language: str = "english", strength: int = 256) -> str: From 1e8d8ecf56ecd43f10809811d1b57b12b307b31d Mon Sep 17 00:00:00 2001 From: parallels Date: Wed, 2 Nov 2022 21:46:02 -0400 Subject: [PATCH 32/36] support utxo query with kupo --- integration-test/docker-compose.yml | 2 +- integration-test/test/base.py | 4 ++-- pycardano/backend/ogmios.py | 32 +++++++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/integration-test/docker-compose.yml b/integration-test/docker-compose.yml index f185fff9..efb181f5 100644 --- a/integration-test/docker-compose.yml +++ b/integration-test/docker-compose.yml @@ -73,7 +73,7 @@ services: max-file: "10" kupo: - image: cardanosolutions/kupo + image: cardanosolutions/kupo:v2.1.0 environment: NETWORK: "${NETWORK:-local}" diff --git a/integration-test/test/base.py b/integration-test/test/base.py index 230f5167..ae6b457d 100644 --- a/integration-test/test/base.py +++ b/integration-test/test/base.py @@ -21,9 +21,9 @@ class TestBase: OGMIOS_WS = "ws://localhost:1337" - KUPO_URL = "http://localhost:1442/v1/matches" + KUPO_URL = "http://localhost:1442" - chain_context = OgmiosChainContext(OGMIOS_WS, Network.TESTNET) + chain_context = OgmiosChainContext(OGMIOS_WS, Network.TESTNET, kupo_url=KUPO_URL) check_chain_context(chain_context) diff --git a/pycardano/backend/ogmios.py b/pycardano/backend/ogmios.py index 163a3e6d..236e5809 100644 --- a/pycardano/backend/ogmios.py +++ b/pycardano/backend/ogmios.py @@ -236,8 +236,8 @@ def _utxos_kupo(self, address: str) -> List[UTxO]: if self._kupo_url is None: raise AssertionError("kupo_url object attribute has not been assigned properly.") - address_url = self._kupo_url + "/" + address - results = requests.get(address_url).json() + kupo_utxo_url = self._kupo_url + "/matches/" + address + results = requests.get(kupo_utxo_url).json() utxos = [] @@ -254,17 +254,43 @@ def _utxos_kupo(self, address: str) -> List[UTxO]: lovelace_amount = result["value"]["coins"] + script = None + script_hash = result.get("script_hash", None) + if script_hash: + kupo_script_url = self._kupo_url + "/scripts/" + script_hash + script = requests.get(kupo_script_url).json() + if script["language"] == "plutus:v2": + script = PlutusV2Script( + cbor2.loads(bytes.fromhex(script["script"])) + ) + elif script["language"] == "plutus:v1": + script = PlutusV1Script( + cbor2.loads(bytes.fromhex(script["script"])) + ) + else: + raise ValueError("Unknown plutus script type") + + datum = None datum_hash = ( DatumHash.from_primitive(result["datum_hash"]) if result["datum_hash"] else None ) + if datum_hash: + kupo_datum_url = self._kupo_url + "/datums/" + result["datum_hash"] + datum_result = requests.get(kupo_datum_url).json() + if datum_result and datum_result['datum'] != datum_hash: + #datum = RawCBOR(bytes.fromhex(datum_result["datum"])) + datum = cbor2.loads(bytes.fromhex(datum_result["datum"])) + datum_hash = None if not result["value"]["assets"]: tx_out = TransactionOutput( Address.from_primitive(address), amount=lovelace_amount, datum_hash=datum_hash, + datum=datum, + script=script, ) else: multi_assets = MultiAsset() @@ -281,6 +307,8 @@ def _utxos_kupo(self, address: str) -> List[UTxO]: Address.from_primitive(address), amount=Value(lovelace_amount, multi_assets), datum_hash=datum_hash, + datum=datum, + script=script, ) utxos.append(UTxO(tx_in, tx_out)) else: From 24b5de8fa3b83b08fee008fe62723538fcda3027 Mon Sep 17 00:00:00 2001 From: parallels Date: Wed, 2 Nov 2022 21:47:39 -0400 Subject: [PATCH 33/36] move test_bip32 --- test/pycardano/backend/test_bip32.py | 110 --------------------------- 1 file changed, 110 deletions(-) delete mode 100644 test/pycardano/backend/test_bip32.py diff --git a/test/pycardano/backend/test_bip32.py b/test/pycardano/backend/test_bip32.py deleted file mode 100644 index 21097ed0..00000000 --- a/test/pycardano/backend/test_bip32.py +++ /dev/null @@ -1,110 +0,0 @@ -from pycardano.address import Address -from pycardano.crypto.bip32 import HDWallet -from pycardano.key import PaymentVerificationKey -from pycardano.network import Network - -# Tests copied from: https://github.com/Emurgo/cardano-serialization-lib/blob/master/rust/src/address.rs - -MNEMONIC_12 = "test walk nut penalty hip pave soap entry language right filter choice" -MNEMONIC_15 = "art forum devote street sure rather head chuckle guard poverty release quote oak craft enemy" -MNEMONIC_24 = "excess behave track soul table wear ocean cash stay nature item turtle palm soccer lunch horror start stumble month panic right must lock dress" - - -def test_mnemonic(): - wrong_mnemonic = "test walk nut penalty hip pave soap entry language right filter" - assert not HDWallet.is_mnemonic(wrong_mnemonic) - - -def test_mnemonic_generation(): - mnemonic_words = HDWallet.generate_mnemonic(strength=128) - assert HDWallet.is_mnemonic(mnemonic_words) - - -def test_payment_address_12_reward(): - hdwallet = HDWallet.from_mnemonic(MNEMONIC_12) - hdwallet_stake = hdwallet.derive_from_path("m/1852'/1815'/0'/2/0") - stake_public_key = hdwallet_stake.public_key - stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) - - assert ( - Address( - payment_part=None, staking_part=stake_vk.hash(), network=Network.TESTNET - ).encode() - == "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl" - ) - - -def test_payment_address_12_reward2(): - hdwallet = HDWallet.from_mnemonic(MNEMONIC_12) - hdwalletl_l1 = hdwallet.derive_from_index( - hdwallet, 1852, private=True, hardened=True - ) - hdwalletl_l2 = hdwallet.derive_from_index( - hdwalletl_l1, 1815, private=True, hardened=True - ) - hdwalletl_l3 = hdwallet.derive_from_index( - hdwalletl_l2, 0, private=True, hardened=True - ) - hdwalletl_l4 = hdwallet.derive_from_index( - hdwalletl_l3, 2, private=False, hardened=False - ) - hdwallet_stake = hdwallet.derive_from_index( - hdwalletl_l4, 0, private=False, hardened=False - ) - stake_public_key = hdwallet_stake.public_key - stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) - - assert ( - Address( - payment_part=None, staking_part=stake_vk.hash(), network=Network.TESTNET - ).encode() - == "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl" - ) - - -def test_payment_address_12_base(): - hdwallet = HDWallet.from_mnemonic(MNEMONIC_12) - hdwallet_spend = hdwallet.derive_from_path("m/1852'/1815'/0'/0/0") - spend_public_key = hdwallet_spend.public_key - spend_vk = PaymentVerificationKey.from_primitive(spend_public_key) - - hdwallet_stake = hdwallet.derive_from_path("m/1852'/1815'/0'/2/0") - stake_public_key = hdwallet_stake.public_key - stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) - - assert ( - Address(spend_vk.hash(), stake_vk.hash(), network=Network.TESTNET).encode() - == "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp" - ) - - -def test_payment_address_15_base(): - hdwallet = HDWallet.from_mnemonic(MNEMONIC_15) - hdwallet_spend = hdwallet.derive_from_path("m/1852'/1815'/0'/0/0") - spend_public_key = hdwallet_spend.public_key - spend_vk = PaymentVerificationKey.from_primitive(spend_public_key) - - hdwallet_stake = hdwallet.derive_from_path("m/1852'/1815'/0'/2/0") - stake_public_key = hdwallet_stake.public_key - stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) - - assert ( - Address(spend_vk.hash(), stake_vk.hash(), network=Network.MAINNET).encode() - == "addr1q9u5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qld6xc3" - ) - - -def test_payment_address_24_base(): - hdwallet = HDWallet.from_mnemonic(MNEMONIC_24) - hdwallet_spend = hdwallet.derive_from_path("m/1852'/1815'/0'/0/0") - spend_public_key = hdwallet_spend.public_key - spend_vk = PaymentVerificationKey.from_primitive(spend_public_key) - - hdwallet_stake = hdwallet.derive_from_path("m/1852'/1815'/0'/2/0") - stake_public_key = hdwallet_stake.public_key - stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) - - assert ( - Address(spend_vk.hash(), stake_vk.hash(), network=Network.MAINNET).encode() - == "addr1qyy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmn8k8ttq8f3gag0h89aepvx3xf69g0l9pf80tqv7cve0l33sdn8p3d" - ) From d4168ab18835ab44dbe1274bd81ba144994680c7 Mon Sep 17 00:00:00 2001 From: parallels Date: Fri, 4 Nov 2022 10:12:41 -0400 Subject: [PATCH 34/36] update utxo query with kupo --- integration-test/test/test_plutus.py | 2 +- pycardano/backend/ogmios.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/integration-test/test/test_plutus.py b/integration-test/test/test_plutus.py index a60274d3..c1dd9eb8 100644 --- a/integration-test/test/test_plutus.py +++ b/integration-test/test/test_plutus.py @@ -293,7 +293,7 @@ def test_plutus_v2_ref_script(self): for utxo in self.chain_context.utxos(str(script_address)): if not utxo.output.script and ( utxo.output.datum_hash == datum_hash(datum) - or utxo.output.datum == datum + or datum_hash(utxo.output.datum) == datum_hash(datum) ): utxo_to_spend = utxo break diff --git a/pycardano/backend/ogmios.py b/pycardano/backend/ogmios.py index 236e5809..52ba27a0 100644 --- a/pycardano/backend/ogmios.py +++ b/pycardano/backend/ogmios.py @@ -2,7 +2,7 @@ import json import time from enum import Enum -from typing import Any, Dict, List, Optional, Union, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union import cbor2 import requests @@ -234,7 +234,9 @@ def _utxos_kupo(self, address: str) -> List[UTxO]: List[UTxO]: A list of UTxOs. """ if self._kupo_url is None: - raise AssertionError("kupo_url object attribute has not been assigned properly.") + raise AssertionError( + "kupo_url object attribute has not been assigned properly." + ) kupo_utxo_url = self._kupo_url + "/matches/" + address results = requests.get(kupo_utxo_url).json() @@ -279,9 +281,8 @@ def _utxos_kupo(self, address: str) -> List[UTxO]: if datum_hash: kupo_datum_url = self._kupo_url + "/datums/" + result["datum_hash"] datum_result = requests.get(kupo_datum_url).json() - if datum_result and datum_result['datum'] != datum_hash: - #datum = RawCBOR(bytes.fromhex(datum_result["datum"])) - datum = cbor2.loads(bytes.fromhex(datum_result["datum"])) + if datum_result and datum_result["datum"] != datum_hash: + datum = RawCBOR(bytes.fromhex(datum_result["datum"])) datum_hash = None if not result["value"]["assets"]: From 1b3c2df4d2efc270d3f481aa55ab2100830741e9 Mon Sep 17 00:00:00 2001 From: parallels Date: Fri, 4 Nov 2022 10:13:09 -0400 Subject: [PATCH 35/36] update plutus guide with vasil --- docs/source/guides/plutus.rst | 68 +++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/docs/source/guides/plutus.rst b/docs/source/guides/plutus.rst index e8a55025..50fdb99a 100644 --- a/docs/source/guides/plutus.rst +++ b/docs/source/guides/plutus.rst @@ -161,3 +161,71 @@ Taker/Unlocker provides collateral. Collateral has been introduced in Alonzo tra The funds locked in script address is successfully retrieved to the taker address. +------------- +Vasil Upgrade +------------- +As part of the Basho phase of Cardano roadmap, the Vasil upgrade brings new capabilities on Plutus, namely reference inputs, inline datums, reference scripts, collateral output and Plutus V2 primitives. + +- **Reference inputs** (`CIP-31 `_): This upgrade enables data sharing on-chain. Previously, datums were carried in transaction outputs; they stored and provided access to information on the blockchain. However, to access information in this datum, one had to spend the output that the datum was attached to. This required the re-creation of a spent output. The addition of reference inputs now allows developers to look at the datum without extra steps. This facilitates access to information stored on the blockchain without the need for spending and re-creating UTXOs. This can be useful for oracles and other use cases where state need to be inspected. + +- **Inline datums** (`CIP-32 `_): Transaction datums were previously attached to outputs as hashes. With the implementation of inline datums, developers can now create scripts and attach datums directly to outputs instead of using their hashes. This simplifies how datums are used – a user can see the actual datum rather than supply it to match the given hash. + +- **Reference scripts** (`CIP-33 `_): In Alonzo, when spending an output locked within a Plutus script, one had to include the script in the spending transaction. This increased the size of the script and caused certain delays in its processing. The reference scripts upgrade allows developers to reference a script without including it in each transaction. This significantly reduces transaction size, improves throughput, and reduces script execution costs (since the script only needs to be paid for once). + +- **Explicit collateral output** (`CIP-40 `_): Transactions that call Plutus smart contracts are required to put up collateral to cover the potential cost of smart contract execution failure. If contract execution fails during phase 2 validation, all the funds stored in the chose UTXO for the collateral will be lost. After Vasil, user can specify a change address for the script collateral. If the script fails phase-2 validation, only the collateral amount will be taken, and the remaining funds will be sent to the change address. + +- **Plutus V2 scripts**: The Vasil upgrade includes a new cost model that's lower than before, and developers will be able to see redeemers for all inputs rather than just the one being passed to the currently executing script. + +Using the same FortyTwo example, now in Vasil, we show how reference scripts can be used. Reference script exists at a particular transaction output, and it can be used to witness UTxO at the corresponding script address:: + + >>> builder = TransactionBuilder(context) + >>> builder.add_input_address(giver_address) + >>> datum = 42 + >>> # Include scripts in the script address + >>> builder.add_output( + >>> TransactionOutput(script_address, 50000000, script=forty_two_script) + >>> ) + +With reference script, actual script doesn't need to be included in the transaction anymore in order to spend UTxO sitting at script address:: + + >>> utxo_to_spend = None + >>> # Spend the utxo that has datum/datum hash but no script + >>> for utxo in chain_context.utxos(str(script_address)): + >>> if not utxo.output.script and ( + >>> utxo.output.datum_hash == datum_hash(datum) + >>> or utxo.output.datum == datum + >>> ): + >>> utxo_to_spend = utxo + >>> break + + >>> builder = TransactionBuilder(context) + >>> builder.add_script_input(utxo_to_spend, datum=datum, redeemer=redeemer) + >>> take_output = TransactionOutput(taker_address, 25123456) + >>> builder.add_output(take_output) + >>> signed_tx = builder.build_and_sign([extended_payment_skey], taker_address) + +Again, with the same example, we show that you can send funds to script address with inline datums directly:: + + >>> builder = TransactionBuilder(context) + >>> builder.add_input_address(giver_address) + >>> datum = 42 + >>> builder.add_output( + >>> TransactionOutput(script_address, 50000000, datum=datum, script=forty_two_script) + >>> ) + +With inline datum, we no longer have to include a datum within our transaction for our plutus spending scripts. Instead we can specify the transaction output where our datum exists to be used in conjunction with our Plutus spending script. This reduces the overall size of our transaction:: + + >>> utxo_to_spend = None + >>> # Speed the utxo that has both inline script and inline datum + >>> for utxo in chain_context.utxos(str(script_address)): + >>> if utxo.output.datum and utxo.output.script: + >>> utxo_to_spend = utxo + >>> break + + >>> builder = TransactionBuilder(context) + >>> builder.add_script_input(utxo_to_spend, redeemer=redeemer) + >>> take_output = TransactionOutput(taker_address, 25123456) + >>> builder.add_output(take_output) + >>> signed_tx = builder.build_and_sign([extended_payment_skey], taker_address) + + From a77430d56833dcc67a94fdefec560f35d66e248f Mon Sep 17 00:00:00 2001 From: Yuanheng Wang Date: Sat, 19 Nov 2022 11:00:57 -0500 Subject: [PATCH 36/36] fixing ExtendedSigning key from_hdwallet func --- pycardano/key.py | 10 ++++++++-- test/pycardano/backend/test_bip32.py | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/pycardano/key.py b/pycardano/key.py index 87587a7d..a632d110 100644 --- a/pycardano/key.py +++ b/pycardano/key.py @@ -192,9 +192,15 @@ def from_hdwallet(cls, hdwallet: HDWallet) -> ExtendedSigningKey: "The hdwallet doesn't contain extended private key or chain code info." ) - return Key( + # return Key( + # payload=hdwallet.xprivate_key + hdwallet.public_key + hdwallet.chain_code, + # key_type="PaymentExtendedSigningKeyShelley_ed25519_bip32", + # description="Payment Signing Key", + # ) + + return cls( payload=hdwallet.xprivate_key + hdwallet.public_key + hdwallet.chain_code, - type="PaymentExtendedSigningKeyShelley_ed25519_bip32", + key_type="PaymentExtendedSigningKeyShelley_ed25519_bip32", description="Payment Signing Key", ) diff --git a/test/pycardano/backend/test_bip32.py b/test/pycardano/backend/test_bip32.py index 21097ed0..1dfeaea9 100644 --- a/test/pycardano/backend/test_bip32.py +++ b/test/pycardano/backend/test_bip32.py @@ -1,6 +1,6 @@ from pycardano.address import Address from pycardano.crypto.bip32 import HDWallet -from pycardano.key import PaymentVerificationKey +from pycardano.key import ExtendedSigningKey, PaymentVerificationKey from pycardano.network import Network # Tests copied from: https://github.com/Emurgo/cardano-serialization-lib/blob/master/rust/src/address.rs @@ -108,3 +108,21 @@ def test_payment_address_24_base(): Address(spend_vk.hash(), stake_vk.hash(), network=Network.MAINNET).encode() == "addr1qyy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmn8k8ttq8f3gag0h89aepvx3xf69g0l9pf80tqv7cve0l33sdn8p3d" ) + + +def test_payment_address_24_base_2(): + hdwallet = HDWallet.from_mnemonic(MNEMONIC_24) + hdwallet_spend = hdwallet.derive_from_path("m/1852'/1815'/0'/0/0") + spend_extended_sk = ExtendedSigningKey.from_hdwallet(hdwallet_spend) + spend_extended_vk = spend_extended_sk.to_verification_key() + + hdwallet_stake = hdwallet.derive_from_path("m/1852'/1815'/0'/2/0") + stake_extended_sk = ExtendedSigningKey.from_hdwallet(hdwallet_stake) + stake_extended_vk = stake_extended_sk.to_verification_key() + + assert ( + Address( + spend_extended_vk.hash(), stake_extended_vk.hash(), network=Network.MAINNET + ).encode() + == "addr1qyy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmn8k8ttq8f3gag0h89aepvx3xf69g0l9pf80tqv7cve0l33sdn8p3d" + )