From f690c16f27215eff0546275871e8b76637b4d1c5 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 29 Jun 2022 23:49:50 +0200 Subject: [PATCH 001/130] Add beginnings of easy class --- pycardano/easy.py | 438 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 pycardano/easy.py diff --git a/pycardano/easy.py b/pycardano/easy.py new file mode 100644 index 00000000..169079a8 --- /dev/null +++ b/pycardano/easy.py @@ -0,0 +1,438 @@ +from dataclasses import dataclass, field +from pathlib import Path +from typing import Literal, Optional, Union +from pycardano.address import Address + +from pycardano.backend.base import ChainContext +from pycardano.backend.blockfrost import BlockFrostChainContext +from pycardano.key import ( + PaymentKeyPair, + PaymentSigningKey, + PaymentVerificationKey, + SigningKey, + VerificationKey, +) +from pycardano.logging import logger +from pycardano.nativescript import NativeScript +from pycardano.network import Network +from pycardano.transaction import UTxO + + +class _Amount: + """Base class for Cardano currency amounts.""" + + def __init__(self, amount=0, amount_type="lovelace"): + + self._amount = amount + self._amount_type = amount_type + + if self._amount_type == "lovelace": + self.lovelace = int(self._amount) + self.ada = self._amount / 1000000 + else: + self.lovelace = int(self._amount * 1000000) + self.ada = self._amount + + self._amount_dict = {"lovelace": self.lovelace, "ada": self.ada} + + @property + def amount(self): + + if self._amount_type == "lovelace": + return self.lovelace + else: + return self.ada + + def __eq__(self, other): + if isinstance(other, (int, float)): + return self.amount == other + elif isinstance(other, _Amount): + return self.lovelace == other.lovelace + else: + raise TypeError("Must compare with a number or another Cardano amount") + + def __ne__(self, other): + if isinstance(other, (int, float)): + return self.amount != other + elif isinstance(other, _Amount): + return self.lovelace != other.lovelace + else: + raise TypeError("Must compare with a number or another Cardano amount") + + def __gt__(self, other): + if isinstance(other, (int, float)): + return self.amount > other + elif isinstance(other, _Amount): + return self.lovelace > other.lovelace + else: + raise TypeError("Must compare with a number or another Cardano amount") + + def __lt__(self, other): + if isinstance(other, (int, float)): + return self.amount < other + elif isinstance(other, _Amount): + return self.lovelace < other.lovelace + else: + raise TypeError("Must compare with a number or another Cardano amount") + + def __ge__(self, other): + if isinstance(other, (int, float)): + return self.amount >= other + elif isinstance(other, _Amount): + return self.lovelace >= other.lovelace + else: + raise TypeError("Must compare with a number or another Cardano amount") + + def __le__(self, other): + if isinstance(other, (int, float)): + return self.amount <= other + elif isinstance(other, _Amount): + return self.lovelace <= other.lovelace + else: + raise TypeError("Must compare with a number or another Cardano amount") + + def __int__(self): + return int(self.amount) + + def __str__(self): + return str(self.amount) + + def __hash__(self): + return hash((self._amount, self._amount_type)) + + def __bool__(self): + return bool(self._amount) + + def __getitem__(self, key): + return self._amount_dict[key] + + # Math + def __add__(self, other): + if isinstance(other, (int, float)): + return self.__class__(self.amount + other) + elif isinstance(other, _Amount): + return self.__class__(self.amount + other[self._amount_type]) + else: + raise TypeError("Must compute with a number or another Cardano amount") + + def __radd__(self, other): + if isinstance(other, (int, float)): + return self.__class__(self.amount + other) + elif isinstance(other, _Amount): + return self.__class__(self.amount + other[self._amount_type]) + else: + raise TypeError("Must compute with a number or another Cardano amount") + + def __sub__(self, other): + if isinstance(other, (int, float)): + return self.__class__(self.amount - other) + elif isinstance(other, _Amount): + return self.__class__(self.amount - other[self._amount_type]) + else: + raise TypeError("Must compute with a number or another Cardano amount") + + def __rsub__(self, other): + if isinstance(other, (int, float)): + return self.__class__(self.amount - other) + elif isinstance(other, _Amount): + return self.__class__(self.amount - other[self._amount_type]) + else: + raise TypeError("Must compute with a number or another Cardano amount") + + def __mul__(self, other): + if isinstance(other, (int, float)): + return self.__class__(self.amount * other) + elif isinstance(other, _Amount): + return self.__class__(self.amount * other[self._amount_type]) + else: + raise TypeError("Must compute with a number or another Cardano amount") + + def __rmul__(self, other): + if isinstance(other, (int, float)): + return self.__class__(self.amount * other) + elif isinstance(other, _Amount): + return self.__class__(self.amount * other[self._amount_type]) + else: + raise TypeError("Must compute with a number or another Cardano amount") + + def __truediv__(self, other): + if isinstance(other, (int, float)): + return self.__class__(self.amount / other) + elif isinstance(other, _Amount): + return self.__class__(self.amount / other[self._amount_type]) + else: + raise TypeError("Must compute with a number or another Cardano amount") + + def __floordiv__(self, other): + if isinstance(other, (int, float)): + return self.__class__(self.amount // other) + elif isinstance(other, _Amount): + return self.__class__(self.amount // other[self._amount_type]) + else: + raise TypeError("Must compute with a number or another Cardano amount") + + def __rtruediv__(self, other): + if isinstance(other, (int, float)): + return self.__class__(self.amount / other) + elif isinstance(other, _Amount): + return self.__class__(self.amount / other[self._amount_type]) + else: + raise TypeError("Must compute with a number or another Cardano amount") + + def __rfloordiv__(self, other): + if isinstance(other, (int, float)): + return self.__class__(self.amount // other) + elif isinstance(other, _Amount): + return self.__class__(self.amount // other[self._amount_type]) + else: + raise TypeError("Must compute with a number or another Cardano amount") + + def __neg__(self): + return self.__class__(-self.amount) + + def __pos__(self): + return self.__class__(+self.amount) + + def __abs__(self): + return self.__class__(abs(self.amount)) + + def __round__(self): + return self.__class__(round(self.amount)) + + +class Lovelace(_Amount): + def __init__(self, amount=0): + super().__init__(amount, "lovelace") + + def __repr__(self): + return f"Lovelace({self.lovelace})" + + def as_lovelace(self): + return Lovelace(self.lovelace) + + def as_ada(self): + return Ada(self.ada) + + +class Ada(_Amount): + def __init__(self, amount=0): + super().__init__(amount, "ada") + + def __repr__(self): + return f"Ada({self.ada})" + + def as_lovelace(self): + return Lovelace(self.lovelace) + + def ad_ada(self): + return Ada(self.ada) + + +@dataclass(unsafe_hash=True) +class Token: + policy: Union[NativeScript, str] + amount: int + name: Optional[str] = field(default="") + hex_name: Optional[str] = field(default="") + metadata: Optional[dict] = field(default=None, compare=False) + + def __post_init__(self): + + if not isinstance(self.amount, int): + raise TypeError("Expected token amount to be of type: integer.") + + if self.hex_name: + if isinstance(self.hex_name, str): + self.name = bytes.fromhex(self.hex_name).decode("utf-8") + + elif isinstance(self.name, str): + self.hex_name = bytes(self.name.encode("utf-8")).hex() + + def __str__(self): + return self.name + + +@dataclass +class Wallet: + """An address for which you own the keys or will later create them.""" + + name: str + address: Optional[Union[Address, str]] = None + keys_dir: Optional[Union[str, Path]] = field(repr=False, default=Path("./priv")) + network: Optional[Literal["mainnet", "testnet"]] = "mainnet" + + # generally added later + lovelace: Optional[Lovelace] = field(repr=False, default=Lovelace(0)) + ada: Optional[Ada] = field(repr=True, default=Ada(0)) + signing_key: Optional[SigningKey] = field(repr=False, default=None) + verification_key: Optional[VerificationKey] = field(repr=False, default=None) + uxtos: Optional[list] = field(repr=False, default_factory=list) + policy: Optional[NativeScript] = field(repr=False, default=None) + + def __post_init__(self): + + # convert address into pycardano format + if isinstance(self.address, str): + self.address = Address.from_primitive(self.address) + + if isinstance(self.keys_dir, str): + self.keys_dir = Path(self.keys_dir) + + # if not address was provided, get keys + if not self.address: + self._load_or_create_key_pair() + # otherwise derive the network from the address provided + else: + self.network = self.address.network.name.lower() + + def query_utxos(self, context: ChainContext): + + try: + self.utxos = context.utxos(str(self.address)) + except Exception as e: + logger.debug(f"Error getting UTxOs. Address has likely not transacted yet. Details: {e}") + self.utxos = [] + + # calculate total ada + if self.utxos: + + self.lovelace = Lovelace(sum([utxo.output.amount.coin for utxo in self.utxos])) + self.ada = self.lovelace.as_ada() + + # add up all the tokens + self._get_tokens() + + logger.debug( + f"Wallet {self.name} has {len(self.utxos)} UTxOs containing a total of {self.ada} ₳." + ) + + else: + logger.debug(f"Wallet {self.name} has no UTxOs.") + + self.lovelace = Lovelace(0) + self.ada = Ada(0) + + + @property + def stake_address(self): + + if isinstance(self.address, str): + address = Address.from_primitive(self.address) + else: + address = self.address + + return Address.from_primitive( + bytes.fromhex(f"e{address.network.value}" + str(address.staking_part)) + ) + + @property + def verification_key_hash(self): + return str(self.address.payment_part) + + + @property + def tokens(self): + return self._token_list + + @property + def tokens_dict(self): + return self._token_dict + + + def _load_or_create_key_pair(self): + + if not self.keys_dir.exists(): + self.keys_dir.mkdir(parents=True, exist_ok=True) + logger.info(f"Creating directory {self.keys_dir}.") + + skey_path = self.keys_dir / f"{self.name}.skey" + vkey_path = self.keys_dir / f"{self.name}.vkey" + + if skey_path.exists(): + skey = PaymentSigningKey.load(str(skey_path)) + vkey = PaymentVerificationKey.from_signing_key(skey) + logger.info(f"Wallet {self.name} found.") + else: + key_pair = PaymentKeyPair.generate() + key_pair.signing_key.save(str(skey_path)) + key_pair.verification_key.save(str(vkey_path)) + skey = key_pair.signing_key + vkey = key_pair.verification_key + logger.info(f"New wallet {self.name} created in {self.keys_dir}.") + + self.signing_key = skey + self.verification_key = vkey + + self.address = Address(vkey.hash(), network=Network[self.network.upper()]) + + def _get_tokens(self): + + # loop through the utxos and sum up all tokens + tokens = {} + + for utxo in self.utxos: + + for script_hash, assets in utxo.output.amount.multi_asset.items(): + + policy_id = str(script_hash) + + for asset, amount in assets.items(): + + asset_name = asset.to_primitive().decode("utf-8") + + if not tokens.get(policy_id): + tokens[policy_id] = {} + + if not tokens[policy_id].get(asset_name): + tokens[policy_id][asset_name] = amount + else: + current_amount = tokens[policy_id][asset_name] + tokens[policy_id][asset_name] = current_amount + amount + + # Convert asset dictionary into Tokens + token_list = [] + for policy_id, assets in tokens.items(): + for asset, amount in assets.items(): + token_list.append(Token(policy_id, amount=amount, name=asset)) + + self._token_dict = tokens + self._token_list = token_list + + def get_utxo_creators(self, context: ChainContext): + + for utxo in self.utxos: + utxo.creator = get_utxo_creator(utxo, context) + + +# helpers +def get_utxo_creator(utxo: UTxO, context: ChainContext): + + if isinstance(context, BlockFrostChainContext): + utxo_creator = ( + context.api.transaction_utxos(str(utxo.input.transaction_id)) + .inputs[0] + .address + ) + + return utxo_creator + + +def get_stake_address(address: Union[str, Address]): + + if isinstance(address, str): + address = Address.from_primitive(address) + + return Address.from_primitive( + bytes.fromhex(f"e{address.network.value}" + str(address.staking_part)) + ) + + +def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")): + + + if isinstance(wallet_path, str): + wallet_path = Path(wallet_path) + + wallets = [skey.stem for skey in list(wallet_path.glob("*.skey"))] + + return wallets From c76a8ff003438f4cb7d74bf4b65ef91047e92363 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 29 Jun 2022 23:59:11 +0200 Subject: [PATCH 002/130] Rename base Amount class --- pycardano/easy.py | 56 +++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 169079a8..7fe5c6e5 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -18,7 +18,7 @@ from pycardano.transaction import UTxO -class _Amount: +class Amount: """Base class for Cardano currency amounts.""" def __init__(self, amount=0, amount_type="lovelace"): @@ -46,7 +46,7 @@ def amount(self): def __eq__(self, other): if isinstance(other, (int, float)): return self.amount == other - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.lovelace == other.lovelace else: raise TypeError("Must compare with a number or another Cardano amount") @@ -54,7 +54,7 @@ def __eq__(self, other): def __ne__(self, other): if isinstance(other, (int, float)): return self.amount != other - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.lovelace != other.lovelace else: raise TypeError("Must compare with a number or another Cardano amount") @@ -62,7 +62,7 @@ def __ne__(self, other): def __gt__(self, other): if isinstance(other, (int, float)): return self.amount > other - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.lovelace > other.lovelace else: raise TypeError("Must compare with a number or another Cardano amount") @@ -70,7 +70,7 @@ def __gt__(self, other): def __lt__(self, other): if isinstance(other, (int, float)): return self.amount < other - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.lovelace < other.lovelace else: raise TypeError("Must compare with a number or another Cardano amount") @@ -78,7 +78,7 @@ def __lt__(self, other): def __ge__(self, other): if isinstance(other, (int, float)): return self.amount >= other - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.lovelace >= other.lovelace else: raise TypeError("Must compare with a number or another Cardano amount") @@ -86,7 +86,7 @@ def __ge__(self, other): def __le__(self, other): if isinstance(other, (int, float)): return self.amount <= other - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.lovelace <= other.lovelace else: raise TypeError("Must compare with a number or another Cardano amount") @@ -110,7 +110,7 @@ def __getitem__(self, key): def __add__(self, other): if isinstance(other, (int, float)): return self.__class__(self.amount + other) - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.__class__(self.amount + other[self._amount_type]) else: raise TypeError("Must compute with a number or another Cardano amount") @@ -118,7 +118,7 @@ def __add__(self, other): def __radd__(self, other): if isinstance(other, (int, float)): return self.__class__(self.amount + other) - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.__class__(self.amount + other[self._amount_type]) else: raise TypeError("Must compute with a number or another Cardano amount") @@ -126,7 +126,7 @@ def __radd__(self, other): def __sub__(self, other): if isinstance(other, (int, float)): return self.__class__(self.amount - other) - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.__class__(self.amount - other[self._amount_type]) else: raise TypeError("Must compute with a number or another Cardano amount") @@ -134,7 +134,7 @@ def __sub__(self, other): def __rsub__(self, other): if isinstance(other, (int, float)): return self.__class__(self.amount - other) - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.__class__(self.amount - other[self._amount_type]) else: raise TypeError("Must compute with a number or another Cardano amount") @@ -142,7 +142,7 @@ def __rsub__(self, other): def __mul__(self, other): if isinstance(other, (int, float)): return self.__class__(self.amount * other) - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.__class__(self.amount * other[self._amount_type]) else: raise TypeError("Must compute with a number or another Cardano amount") @@ -150,7 +150,7 @@ def __mul__(self, other): def __rmul__(self, other): if isinstance(other, (int, float)): return self.__class__(self.amount * other) - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.__class__(self.amount * other[self._amount_type]) else: raise TypeError("Must compute with a number or another Cardano amount") @@ -158,7 +158,7 @@ def __rmul__(self, other): def __truediv__(self, other): if isinstance(other, (int, float)): return self.__class__(self.amount / other) - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.__class__(self.amount / other[self._amount_type]) else: raise TypeError("Must compute with a number or another Cardano amount") @@ -166,7 +166,7 @@ def __truediv__(self, other): def __floordiv__(self, other): if isinstance(other, (int, float)): return self.__class__(self.amount // other) - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.__class__(self.amount // other[self._amount_type]) else: raise TypeError("Must compute with a number or another Cardano amount") @@ -174,7 +174,7 @@ def __floordiv__(self, other): def __rtruediv__(self, other): if isinstance(other, (int, float)): return self.__class__(self.amount / other) - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.__class__(self.amount / other[self._amount_type]) else: raise TypeError("Must compute with a number or another Cardano amount") @@ -182,7 +182,7 @@ def __rtruediv__(self, other): def __rfloordiv__(self, other): if isinstance(other, (int, float)): return self.__class__(self.amount // other) - elif isinstance(other, _Amount): + elif isinstance(other, Amount): return self.__class__(self.amount // other[self._amount_type]) else: raise TypeError("Must compute with a number or another Cardano amount") @@ -200,7 +200,7 @@ def __round__(self): return self.__class__(round(self.amount)) -class Lovelace(_Amount): +class Lovelace(Amount): def __init__(self, amount=0): super().__init__(amount, "lovelace") @@ -214,7 +214,7 @@ def as_ada(self): return Ada(self.ada) -class Ada(_Amount): +class Ada(Amount): def __init__(self, amount=0): super().__init__(amount, "ada") @@ -259,13 +259,13 @@ class Wallet: name: str address: Optional[Union[Address, str]] = None keys_dir: Optional[Union[str, Path]] = field(repr=False, default=Path("./priv")) - network: Optional[Literal["mainnet", "testnet"]] = "mainnet" + network: Optional[Literal["mainnet", "testnet"]] = "mainnet" # generally added later lovelace: Optional[Lovelace] = field(repr=False, default=Lovelace(0)) ada: Optional[Ada] = field(repr=True, default=Ada(0)) - signing_key: Optional[SigningKey] = field(repr=False, default=None) - verification_key: Optional[VerificationKey] = field(repr=False, default=None) + signing_key: Optional[SigningKey] = field(repr=False, default=None) + verification_key: Optional[VerificationKey] = field(repr=False, default=None) uxtos: Optional[list] = field(repr=False, default_factory=list) policy: Optional[NativeScript] = field(repr=False, default=None) @@ -290,13 +290,17 @@ def query_utxos(self, context: ChainContext): try: self.utxos = context.utxos(str(self.address)) except Exception as e: - logger.debug(f"Error getting UTxOs. Address has likely not transacted yet. Details: {e}") + logger.debug( + f"Error getting UTxOs. Address has likely not transacted yet. Details: {e}" + ) self.utxos = [] # calculate total ada if self.utxos: - self.lovelace = Lovelace(sum([utxo.output.amount.coin for utxo in self.utxos])) + self.lovelace = Lovelace( + sum([utxo.output.amount.coin for utxo in self.utxos]) + ) self.ada = self.lovelace.as_ada() # add up all the tokens @@ -312,7 +316,6 @@ def query_utxos(self, context: ChainContext): self.lovelace = Lovelace(0) self.ada = Ada(0) - @property def stake_address(self): @@ -329,7 +332,6 @@ def stake_address(self): def verification_key_hash(self): return str(self.address.payment_part) - @property def tokens(self): return self._token_list @@ -338,7 +340,6 @@ def tokens(self): def tokens_dict(self): return self._token_dict - def _load_or_create_key_pair(self): if not self.keys_dir.exists(): @@ -429,7 +430,6 @@ def get_stake_address(address: Union[str, Address]): def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")): - if isinstance(wallet_path, str): wallet_path = Path(wallet_path) From 58647e71728c2f102cf339f4ca03f684ce2ff11f Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 2 Jul 2022 00:33:29 +0200 Subject: [PATCH 003/130] Update Imports --- pycardano/easy.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 7fe5c6e5..783acf83 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -1,10 +1,17 @@ from dataclasses import dataclass, field +import json +import logging +from os import getenv from pathlib import Path -from typing import Literal, Optional, Union +from time import sleep +from typing import List, Literal, Optional, Type, Union +from pycardano import transaction from pycardano.address import Address from pycardano.backend.base import ChainContext from pycardano.backend.blockfrost import BlockFrostChainContext +from pycardano.exception import PyCardanoException +from pycardano.hash import TransactionId from pycardano.key import ( PaymentKeyPair, PaymentSigningKey, @@ -15,7 +22,12 @@ from pycardano.logging import logger from pycardano.nativescript import NativeScript from pycardano.network import Network -from pycardano.transaction import UTxO +from pycardano.transaction import TransactionOutput, UTxO, Value +from pycardano.txbuilder import TransactionBuilder + + +# set logging level to info +logger.setLevel(logging.INFO) class Amount: From f27b750a14bf288fa5875eea7af39ae90cc3a580 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 2 Jul 2022 00:33:48 +0200 Subject: [PATCH 004/130] Check Token metadata for issues --- pycardano/easy.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 783acf83..39c92a49 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -246,7 +246,7 @@ class Token: amount: int name: Optional[str] = field(default="") hex_name: Optional[str] = field(default="") - metadata: Optional[dict] = field(default=None, compare=False) + metadata: Optional[dict] = field(default_factory=dict, compare=False) def __post_init__(self): @@ -260,9 +260,56 @@ def __post_init__(self): elif isinstance(self.name, str): self.hex_name = bytes(self.name.encode("utf-8")).hex() + self._check_metadata(to_check=self.metadata, top_level=True) + def __str__(self): return self.name + def _check_metadata( + self, to_check: Union[dict, list, str], top_level: bool = False + ): + """Screen the input metadata for potential issues. + Used recursively to check inside all dicts and lists of the metadata. + Use top_level=True only for the full metadata dictionary in order to check that + it is JSON serializable. + """ + + if isinstance(to_check, dict): + for key, value in to_check.items(): + + if len(str(key)) > 64: + raise MetadataFormattingException( + f"Metadata key is too long (> 64 characters): {key}\nConsider splitting into an array of shorter strings." + ) + + if isinstance(value, dict) or isinstance(value, list): + self._check_metadata(to_check=value) + + elif len(str(value)) > 64: + raise MetadataFormattingException( + f"Metadata field is too long (> 64 characters): {key}: {value}\nConsider splitting into an array of shorter strings." + ) + + elif isinstance(to_check, list): + + for item in to_check: + if len(str(item)) > 64: + raise MetadataFormattingException( + f"Metadata field is too long (> 64 characters): {item}\nConsider splitting into an array of shorter strings." + ) + + elif isinstance(to_check, str): + if len(to_check) > 64: + raise MetadataFormattingException( + f"Metadata field is too long (> 64 characters): {item}\nConsider splitting into an array of shorter strings." + ) + + if top_level: + try: + json.dumps(to_check) + except TypeError as e: + raise MetadataFormattingException(f"Cannot format metadata: {e}") + @dataclass class Wallet: From c6646de73ac9087e5379d9772c1b6a64aa844d09 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 2 Jul 2022 00:34:32 +0200 Subject: [PATCH 005/130] Automatically add blockfrost context if correct env vars are set --- pycardano/easy.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 39c92a49..67e30012 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -327,6 +327,7 @@ class Wallet: verification_key: Optional[VerificationKey] = field(repr=False, default=None) uxtos: Optional[list] = field(repr=False, default_factory=list) policy: Optional[NativeScript] = field(repr=False, default=None) + context: Optional[BlockFrostChainContext] = field(repr=False, default=None) def __post_init__(self): @@ -344,7 +345,22 @@ def __post_init__(self): else: self.network = self.address.network.name.lower() - def query_utxos(self, context: ChainContext): + # try to automatically create blockfrost context + if not self.context: + if self.network.lower() == "mainnet": + if getenv("BLOCKFROST_ID"): + self.context = BlockFrostChainContext(getenv("BLOCKFROST_ID")) + elif getenv("BLOCKFROST_ID_TESTNET"): + self.context = BlockFrostChainContext(getenv("BLOCKFROST_ID_TESTNET")) + + if self.context: + self.query_utxos() + + logger.info(self.__repr__()) + + def query_utxos(self, context: Optional[ChainContext] = None): + + context = self._find_context(context) try: self.utxos = context.utxos(str(self.address)) @@ -425,6 +441,18 @@ def _load_or_create_key_pair(self): self.address = Address(vkey.hash(), network=Network[self.network.upper()]) + def _find_context(self, context: Optional[ChainContext] = None): + """Helper function to ensure that a context is always provided when needed. + By default will return self.context unless a context variable has been specifically specified. + """ + + if not context and not self.context: + raise TypeError("Please pass `context` or set Wallet.context.") + elif not self.context: + return context + else: + return self.context + def _get_tokens(self): # loop through the utxos and sum up all tokens @@ -458,7 +486,9 @@ def _get_tokens(self): self._token_dict = tokens self._token_list = token_list - def get_utxo_creators(self, context: ChainContext): + def get_utxo_creators(self, context: Optional[ChainContext] = None): + + context = self._find_context(context) for utxo in self.utxos: utxo.creator = get_utxo_creator(utxo, context) From 2c5c5d6948099f5e2ee607966acc61d6835410b2 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 2 Jul 2022 00:35:07 +0200 Subject: [PATCH 006/130] Add methods for sending transactions --- pycardano/easy.py | 149 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/pycardano/easy.py b/pycardano/easy.py index 67e30012..45f22e0f 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -493,6 +493,105 @@ def get_utxo_creators(self, context: Optional[ChainContext] = None): for utxo in self.utxos: utxo.creator = get_utxo_creator(utxo, context) + def send_ada( + self, + to: Union[str, Address], + amount: Union[Ada, Lovelace, int], + utxos: Optional[Union[UTxO, List[UTxO]]] = [], + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, + ): + + context = self._find_context(context) + + # streamline inputs + if isinstance(to, str): + to = Address.from_primitive(to) + + if not isinstance(amount, Ada) and not isinstance(amount, Lovelace): + raise TypeError( + "Please provide amount as either `Ada(amount)` or `Lovelace(amount)`." + ) + + if utxos: + if isinstance(utxos, UTxO): + utxos = [utxos] + + builder = TransactionBuilder(context) + + builder.add_input_address(self.address) + + if utxos: + for utxo in utxos: + builder.add_input(utxo) + + builder.add_output( + TransactionOutput(to, Value.from_primitive([amount.as_lovelace().amount])) + ) + + signed_tx = builder.build_and_sign( + [self.signing_key], change_address=self.address + ) + + context.submit_tx(signed_tx.to_cbor()) + + if await_confirmation: + confirmed = wait_for_confirmation(str(signed_tx.id), self.context) + self.query_utxos() + + return str(signed_tx.id) + + def send_utxo( + self, + to: Union[str, Address], + utxos: Union[UTxO, List[UTxO]], + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, + ): + + # streamline inputs + context = self._find_context(context) + + if isinstance(to, str): + to = Address.from_primitive(to) + + if isinstance(utxos, UTxO): + utxos = [utxos] + + builder = TransactionBuilder(context) + + builder.add_input_address(self.address) + + for utxo in utxos: + builder.add_input(utxo) + + signed_tx = builder.build_and_sign( + [self.signing_key], + change_address=to, + merge_change=True, + ) + + context.submit_tx(signed_tx.to_cbor()) + + if await_confirmation: + confirmed = wait_for_confirmation(str(signed_tx.id), self.context) + self.query_utxos() + + return str(signed_tx.id) + + def empty_wallet( + self, + to: Union[str, Address], + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, + ): + + return self.send_utxo( + to=to, + utxos=self.utxos, + await_confirmation=await_confirmation, + context=context, + ) # helpers def get_utxo_creator(utxo: UTxO, context: ChainContext): @@ -506,6 +605,11 @@ def get_utxo_creator(utxo: UTxO, context: ChainContext): return utxo_creator + else: + logger.warn( + "Fetching UTxO creators (sender) is only possible with Blockfrost Chain Context." + ) + def get_stake_address(address: Union[str, Address]): @@ -525,3 +629,48 @@ def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")): wallets = [skey.stem for skey in list(wallet_path.glob("*.skey"))] return wallets + + +def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext): + + if isinstance(context, BlockFrostChainContext): + + from blockfrost import ApiError + + try: + transaction_info = context.api.transaction(str(tx_id)) + confirmed = True + except ApiError: + confirmed = False + transaction_info = {} + + return confirmed + + else: + logger.warn( + "Confirming transactions is is only possible with Blockfrost Chain Context." + ) + + +def wait_for_confirmation( + tx_id: Union[str, TransactionId], context: ChainContext, delay: Optional[int] = 10 +): + + if not isinstance(context, BlockFrostChainContext): + logger.warn( + "Confirming transactions is is only possible with Blockfrost Chain Context." + ) + return + + confirmed = False + while not confirmed: + confirmed = confirm_tx(tx_id, context) + if not confirmed: + sleep(delay) + + return confirmed + + +# Exceptions +class MetadataFormattingException(PyCardanoException): + pass From ae84ee0ba11b287c061505ab35a411acd6fc11be Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 2 Jul 2022 22:43:05 +0200 Subject: [PATCH 007/130] Start adding a mint token class --- pycardano/easy.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pycardano/easy.py b/pycardano/easy.py index 45f22e0f..c9e62c7e 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -593,6 +593,34 @@ def empty_wallet( context=context, ) + def mint_tokens( + self, + to: Union[str, Address], + amount: Union[Ada, Lovelace, int], + mints: Union[Token, List[Token]], + utxos: Optional[Union[UTxO, List[UTxO]]] = [], + signers: Optional[Union['Wallet', List['Wallet']]] = [], + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, + ): + """Under construction.""" + + # streamline inputs + context = self._find_context(context) + + if isinstance(to, str): + to = Address.from_primitive(to) + + if isinstance(utxos, UTxO): + utxos = [utxos] + + builder = TransactionBuilder(context) + + builder.add_input_address(self.address) + + # TBC... + + # helpers def get_utxo_creator(utxo: UTxO, context: ChainContext): From 21ee7a85d771b3aa7958c4cb8dde40cf250867fe Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 4 Jul 2022 17:05:35 +0200 Subject: [PATCH 008/130] Add TokenPolicy class --- pycardano/easy.py | 136 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index c9e62c7e..18f03800 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -240,9 +240,143 @@ def ad_ada(self): return Ada(self.ada) +@dataclass(unsafe_hash=True) +class TokenPolicy: + name: str + policy: Optional[Union[NativeScript, dict]] = field(repr=False, default=None) + policy_dir: Optional[Union[str, Path]] = field( + repr=False, default=Path("./priv/policies") + ) + + def __post_init__(self): + + # streamline inputs + if isinstance(self.policy_dir, str): + self.policy_dir = Path(self.policy_dir) + + if not self.policy_dir.exists(): + self.policy_dir.mkdir(parents=True, exist_ok=True) + + # look for the policy + if Path(self.policy_dir / f"{self.name}.script").exists(): + with open( + Path(self.policy_dir / f"{self.name}.script"), "r" + ) as policy_file: + self.policy = NativeScript.from_dict(json.load(policy_file)) + + elif isinstance(self.policy, dict): + self.policy = NativeScript.from_dict(self.policy) + + @property + def policy_id(self): + + if self.policy: + return str(self.policy.hash()) + + @property + def expiration_slot(self): + """Get the expiration slot for a simple minting policy, + like one generated by generate_minting_policy + """ + + if self.policy: + scripts = getattr(self.policy, "native_scripts", None) + + if scripts: + for script in scripts: + if script._TYPE == 5: + return script.after + + def get_expiration_timestamp(self, context: ChainContext): + """Get the expiration timestamp for a simple minting policy, + like one generated by generate_minting_policy + """ + + if self.expiration_slot: + + seconds_diff = self.expiration_slot - context.last_block_slot + + return datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta( + seconds=seconds_diff + ) + + def is_expired(self, context: ChainContext): + """Get the expiration timestamp for a simple minting policy, + like one generated by generate_minting_policy + """ + + if self.expiration_slot: + + seconds_diff = self.expiration_slot - context.last_block_slot + + return seconds_diff < 0 + + def generate_minting_policy( + self, + signers: Union["Wallet", Address, List["Wallet"], List[Address]], + expiration: Optional[Union[datetime.datetime, int]] = None, + context: Optional[ChainContext] = None, + ): + + script_filepath = Path(self.policy_dir / f"{self.name}.script") + + if script_filepath.exists() or self.policy: + raise FileExistsError(f"Policy named {self.name} already exists") + + if isinstance(expiration, datetime.datetime) and not context: + raise AttributeError( + "If input expiration is provided as a datetime, please also provide a context." + ) + + # get pub key hashes + if not isinstance(signers, list): + signers = [signers] + + pub_keys = [ScriptPubkey(self._get_pub_key_hash(signer)) for signer in signers] + + # calculate when to lock + if expiration: + if isinstance(expiration, int): # assume this is directly the block no. + must_before_slot = InvalidHereAfter(expiration) + elif isinstance(expiration, datetime.datetime): + if expiration.tzinfo: + time_until_expiration = expiration - datetime.datetime.now( + datetime.datetime.utc + ) + else: + time_until_expiration = expiration - datetime.datetime.now() + + last_block_slot = context.last_block_slot + + must_before_slot = InvalidHereAfter( + last_block_slot + int(time_until_expiration.total_seconds()) + ) + + policy = ScriptAll(pub_keys + [must_before_slot]) + + else: + policy = ScriptAll(pub_keys) + + # save policy to file + with open(script_filepath, "w") as script_file: + json.dump(policy.to_dict(), script_file, indent=4) + + self.policy = policy + + @staticmethod + def _get_pub_key_hash(signer=Union["Wallet", Address]): + + if hasattr(signer, "verification_key"): + return signer.verification_key.hash() + elif isinstance(signer, Address): + return str(signer.payment_part) + else: + raise TypeError("Input signer must be of type Wallet or Address.") + + @dataclass(unsafe_hash=True) class Token: - policy: Union[NativeScript, str] + policy: Union[NativeScript, TokenPolicy] amount: int name: Optional[str] = field(default="") hex_name: Optional[str] = field(default="") From 0e270886496296e3c08454feeb8a8aaf053646e5 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 4 Jul 2022 17:05:53 +0200 Subject: [PATCH 009/130] Add bytes_name() to Token --- pycardano/easy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pycardano/easy.py b/pycardano/easy.py index 18f03800..da44b29b 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -444,6 +444,10 @@ def _check_metadata( except TypeError as e: raise MetadataFormattingException(f"Cannot format metadata: {e}") + @property + def bytes_name(self): + return bytes(self.name.encode("utf-8")) + @dataclass class Wallet: From a025a9590eed4b587e85cd7e39e66088c194798c Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 4 Jul 2022 17:06:37 +0200 Subject: [PATCH 010/130] Add message option to transactions --- pycardano/easy.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index da44b29b..7639ad28 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -634,8 +634,9 @@ def get_utxo_creators(self, context: Optional[ChainContext] = None): def send_ada( self, to: Union[str, Address], - amount: Union[Ada, Lovelace, int], + amount: Union[Ada, Lovelace], utxos: Optional[Union[UTxO, List[UTxO]]] = [], + message: Optional[Union[str, List[str]]] = None, await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, ): @@ -667,6 +668,10 @@ def send_ada( TransactionOutput(to, Value.from_primitive([amount.as_lovelace().amount])) ) + if message: + metadata = {674: format_message(message)} + builder.auxiliary_data = AuxiliaryData(AlonzoMetadata(metadata=Metadata(metadata))) + signed_tx = builder.build_and_sign( [self.signing_key], change_address=self.address ) @@ -683,6 +688,7 @@ def send_utxo( self, to: Union[str, Address], utxos: Union[UTxO, List[UTxO]], + message: Optional[Union[str, List[str]]] = None, await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, ): @@ -703,6 +709,10 @@ def send_utxo( for utxo in utxos: builder.add_input(utxo) + if message: + metadata = {674: format_message(message)} + builder.auxiliary_data = AuxiliaryData(AlonzoMetadata(metadata=Metadata(metadata))) + signed_tx = builder.build_and_sign( [self.signing_key], change_address=to, @@ -720,6 +730,7 @@ def send_utxo( def empty_wallet( self, to: Union[str, Address], + message: Optional[Union[str, List[str]]] = None, await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, ): @@ -727,6 +738,7 @@ def empty_wallet( return self.send_utxo( to=to, utxos=self.utxos, + message=message, await_confirmation=await_confirmation, context=context, ) @@ -787,6 +799,24 @@ def get_stake_address(address: Union[str, Address]): ) +def format_message(message: Union[str, List[str]]): + + if isinstance(message, str): + message = [message] + + for line in message: + if len(line) > 64: + raise MetadataFormattingException( + f"Message field is too long (> 64 characters): {line}\nConsider splitting into an array of shorter strings." + ) + if not isinstance(line, str): + raise MetadataFormattingException( + f"Message Field must be of type `str`: {line}" + ) + + return {"msg": message} + + def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")): if isinstance(wallet_path, str): From 8aa8d71064ae67c5026c0fd1b985de82e00d0517 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 4 Jul 2022 17:07:47 +0200 Subject: [PATCH 011/130] Add working token minter --- pycardano/easy.py | 131 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 122 insertions(+), 9 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 7639ad28..b0f3cc57 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -1,11 +1,11 @@ from dataclasses import dataclass, field +import datetime import json import logging from os import getenv from pathlib import Path from time import sleep -from typing import List, Literal, Optional, Type, Union -from pycardano import transaction +from typing import List, Literal, Optional, Union from pycardano.address import Address from pycardano.backend.base import ChainContext @@ -20,10 +20,24 @@ VerificationKey, ) from pycardano.logging import logger -from pycardano.nativescript import NativeScript +from pycardano.metadata import AlonzoMetadata, AuxiliaryData, Metadata +from pycardano.nativescript import ( + InvalidHereAfter, + NativeScript, + ScriptAll, + ScriptPubkey, +) from pycardano.network import Network -from pycardano.transaction import TransactionOutput, UTxO, Value +from pycardano.transaction import ( + Asset, + AssetName, + MultiAsset, + TransactionOutput, + UTxO, + Value, +) from pycardano.txbuilder import TransactionBuilder +from pycardano.utils import min_lovelace # set logging level to info @@ -670,7 +684,9 @@ def send_ada( if message: metadata = {674: format_message(message)} - builder.auxiliary_data = AuxiliaryData(AlonzoMetadata(metadata=Metadata(metadata))) + builder.auxiliary_data = AuxiliaryData( + AlonzoMetadata(metadata=Metadata(metadata)) + ) signed_tx = builder.build_and_sign( [self.signing_key], change_address=self.address @@ -711,7 +727,9 @@ def send_utxo( if message: metadata = {674: format_message(message)} - builder.auxiliary_data = AuxiliaryData(AlonzoMetadata(metadata=Metadata(metadata))) + builder.auxiliary_data = AuxiliaryData( + AlonzoMetadata(metadata=Metadata(metadata)) + ) signed_tx = builder.build_and_sign( [self.signing_key], @@ -746,10 +764,11 @@ def empty_wallet( def mint_tokens( self, to: Union[str, Address], - amount: Union[Ada, Lovelace, int], mints: Union[Token, List[Token]], + amount: Optional[Union[Ada, Lovelace]] = None, utxos: Optional[Union[UTxO, List[UTxO]]] = [], - signers: Optional[Union['Wallet', List['Wallet']]] = [], + other_signers: Optional[Union["Wallet", List["Wallet"]]] = [], + message: Optional[Union[str, List[str]]] = None, await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, ): @@ -761,14 +780,108 @@ def mint_tokens( if isinstance(to, str): to = Address.from_primitive(to) + if amount and not isinstance(amount, Ada) and not isinstance(amount, Lovelace): + raise TypeError( + "Please provide amount as either `Ada(amount)` or `Lovelace(amount)`." + ) + + if not isinstance(mints, list): + mints = [mints] + if isinstance(utxos, UTxO): utxos = [utxos] + if not isinstance(other_signers, list): + other_signers = [other_signers] + + # sort assets by policy_id + mints_dict = {} + mint_metadata = {} + native_scripts = [] + for token in mints: + if isinstance(token.policy, NativeScript): + policy_hash = token.policy.hash() + elif isinstance(token.policy, TokenPolicy): + policy_hash = token.policy.policy.hash() + + policy_id = str(policy_hash) + + if not mints_dict.get(policy_hash): + mints_dict[policy_hash] = {} + mint_metadata[policy_id] = {} + + if isinstance(token.policy, NativeScript): + native_scripts.append(token.policy) + else: + native_scripts.append(token.policy.policy) + + mints_dict[policy_hash][token.name] = token + if token.metadata: + mint_metadata[policy_id][token.name] = token.metadata + + asset_mints = MultiAsset() + + for policy_hash, tokens in mints_dict.items(): + + assets = Asset() + for token in tokens.values(): + assets[AssetName(token.bytes_name)] = int(token.amount) + + asset_mints[policy_hash] = assets + + # create mint metadata + mint_metadata = {721: mint_metadata} + + # add message + if message: + mint_metadata[674] = format_message(message) + + # Place metadata in AuxiliaryData, the format acceptable by a transaction. + auxiliary_data = AuxiliaryData(AlonzoMetadata(metadata=Metadata(mint_metadata))) + + # build the transaction builder = TransactionBuilder(context) + # add transaction inputs + if utxos: + for utxo in utxos: + builder.add_input(utxo) + builder.add_input_address(self.address) - # TBC... + # set builder ttl to the min of the included policies + builder.ttl = min( + [TokenPolicy("", policy).expiration_slot for policy in native_scripts] + ) + + builder.mint = asset_mints + builder.native_scripts = native_scripts + builder.auxiliary_data = auxiliary_data + + if not amount: # sent min amount if none specified + amount = Lovelace(min_lovelace(Value(0, asset_mints), context)) + print("Min value =", amount) + + builder.add_output(TransactionOutput(to, Value(amount.lovelace, asset_mints))) + + if other_signers: + signing_keys = [wallet.signing_key for wallet in other_signers] + [ + self.signing_key + ] + else: + signing_keys = [self.signing_key] + + signed_tx = builder.build_and_sign(signing_keys, change_address=self.address) + + print(signed_tx.to_cbor()) + + context.submit_tx(signed_tx.to_cbor()) + + if await_confirmation: + confirmed = wait_for_confirmation(str(signed_tx.id), self.context) + self.query_utxos() + + return str(signed_tx.id) # helpers From 14ad5f0f8a415d13f3d0745015ec73151ce6dc60 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 4 Jul 2022 17:10:51 +0200 Subject: [PATCH 012/130] Add id property to token policy --- pycardano/easy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pycardano/easy.py b/pycardano/easy.py index b0f3cc57..201016f0 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -287,6 +287,10 @@ def policy_id(self): if self.policy: return str(self.policy.hash()) + @property + def id(self): + return self.policy_id + @property def expiration_slot(self): """Get the expiration slot for a simple minting policy, From 85e3838add7957f038e35e5c3d0f85674ab99701 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 4 Jul 2022 17:14:32 +0200 Subject: [PATCH 013/130] Remove policy datafield from Wallet --- pycardano/easy.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 201016f0..3b06c881 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -469,7 +469,13 @@ def bytes_name(self): @dataclass class Wallet: - """An address for which you own the keys or will later create them.""" + """An address for which you own the keys or will later create them. + TODO: + - burn tokens + - generate manual transactions + - multi-output transactions + - multi-sig transactions + """ name: str address: Optional[Union[Address, str]] = None @@ -482,7 +488,6 @@ class Wallet: signing_key: Optional[SigningKey] = field(repr=False, default=None) verification_key: Optional[VerificationKey] = field(repr=False, default=None) uxtos: Optional[list] = field(repr=False, default_factory=list) - policy: Optional[NativeScript] = field(repr=False, default=None) context: Optional[BlockFrostChainContext] = field(repr=False, default=None) def __post_init__(self): From 17c1ce9079dce866bdb18cdafbc711e450446b44 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 4 Jul 2022 18:03:25 +0200 Subject: [PATCH 014/130] Add method to fetch conchain metadata for Tokens --- pycardano/easy.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 3b06c881..193dcc7a 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -6,8 +6,10 @@ from pathlib import Path from time import sleep from typing import List, Literal, Optional, Union -from pycardano.address import Address +from blockfrost import ApiError + +from pycardano.address import Address from pycardano.backend.base import ChainContext from pycardano.backend.blockfrost import BlockFrostChainContext from pycardano.exception import PyCardanoException @@ -466,6 +468,25 @@ def _check_metadata( def bytes_name(self): return bytes(self.name.encode("utf-8")) + + def get_onchain_metadata(self, context: ChainContext): + + if not isinstance(context, BlockFrostChainContext): + logger.warn( + "Getting onchain metadata is is only possible with Blockfrost Chain Context." + ) + return {} + + try: + metadata = context.api.asset(self.policy.id + self.hex_name).onchain_metadata.to_dict() + except ApiError as e: + logger.error(f"Could not get onchain data, likely this asset has not been minted yet\n Blockfrost Error: {e}") + metadata = {} + + return metadata + + + @dataclass class Wallet: @@ -949,6 +970,17 @@ def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")): return wallets +def get_all_policies(policy_path: Union[str, Path] = Path("./priv/policies")): + + + if isinstance(policy_path, str): + policy_path = Path(policy_path) + + policies = [TokenPolicy(skey.stem) for skey in list(policy_path.glob("*.script"))] + + return policies + + def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext): if isinstance(context, BlockFrostChainContext): From c87d90674911eda3f4d08308b4530e08c5443679 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 4 Jul 2022 18:03:43 +0200 Subject: [PATCH 015/130] Remove unneeded import --- pycardano/easy.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 193dcc7a..6bffbb7e 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -985,8 +985,6 @@ def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext): if isinstance(context, BlockFrostChainContext): - from blockfrost import ApiError - try: transaction_info = context.api.transaction(str(tx_id)) confirmed = True From 72c98ad1f5cab75d9ab545ae12a65ea97079afa7 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 4 Jul 2022 18:06:52 +0200 Subject: [PATCH 016/130] Add method to get all wallet token metadata --- pycardano/easy.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pycardano/easy.py b/pycardano/easy.py index 6bffbb7e..ff86f05b 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -675,6 +675,14 @@ def get_utxo_creators(self, context: Optional[ChainContext] = None): for utxo in self.utxos: utxo.creator = get_utxo_creator(utxo, context) + def get_token_metadata(self, context: Optional[ChainContext] = None): + + context = self._find_context(context) + + for token in self.tokens: + token.get_onchain_metadata(context) + + def send_ada( self, to: Union[str, Address], From a3164de4de8185e9c18eceb397a7379fa0c566ce Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 4 Jul 2022 18:07:15 +0200 Subject: [PATCH 017/130] Automatically load in all wallet policies for which it is a signer --- pycardano/easy.py | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index ff86f05b..c0ba92be 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -307,6 +307,22 @@ def expiration_slot(self): if script._TYPE == 5: return script.after + @property + def required_signatures(self): + """List the public key hashes of all required signers""" + + required_signatures = [] + + if self.policy: + scripts = getattr(self.policy, "native_scripts", None) + + if scripts: + for script in scripts: + if script._TYPE == 0: + required_signatures.append(script.key_hash) + + return required_signatures + def get_expiration_timestamp(self, context: ChainContext): """Get the expiration timestamp for a simple minting policy, like one generated by generate_minting_policy @@ -483,6 +499,7 @@ def get_onchain_metadata(self, context: ChainContext): logger.error(f"Could not get onchain data, likely this asset has not been minted yet\n Blockfrost Error: {e}") metadata = {} + self.metadata = metadata return metadata @@ -491,11 +508,24 @@ def get_onchain_metadata(self, context: ChainContext): @dataclass class Wallet: """An address for which you own the keys or will later create them. + Already does: + - Generate keys + - Load keys + - Fetch utxos + - Send ada + - Send specific UTxOs + - Mint tokens + - Attach messages to transactions TODO: - burn tokens + - automatically load in token polices where wallet is a signer + - stake wallet + - withdraw rewards - generate manual transactions - - multi-output transactions - - multi-sig transactions + - that can do all of the above at once + - multi-output transactions + - multi-sig transactions + - sign messages """ name: str @@ -660,10 +690,16 @@ def _get_tokens(self): tokens[policy_id][asset_name] = current_amount + amount # Convert asset dictionary into Tokens + # find all policies in which the wallet is a signer + my_policies = {policy.id: policy for policy in get_all_policies(self.keys_dir/"policies") if self.verification_key.hash() in policy.required_signatures} + token_list = [] for policy_id, assets in tokens.items(): for asset, amount in assets.items(): - token_list.append(Token(policy_id, amount=amount, name=asset)) + if policy_id in my_policies.keys(): + token_list.append(Token(my_policies[policy_id], amount=amount, name=asset)) + else: + token_list.append(Token(policy_id, amount=amount, name=asset)) self._token_dict = tokens self._token_list = token_list From d6e248967aae13f1e5231c47da7cff8270a7a5f9 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 4 Jul 2022 18:26:50 +0200 Subject: [PATCH 018/130] Get Wallet utxos block times and sort --- pycardano/easy.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index c0ba92be..1699c266 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -2,6 +2,7 @@ import datetime import json import logging +import operator from os import getenv from pathlib import Path from time import sleep @@ -514,11 +515,14 @@ class Wallet: - Fetch utxos - Send ada - Send specific UTxOs + - Get senders of all UTxOs + - Get metadata for all held tokens + - Get utxo block times and sort utxos - Mint tokens + - Automatically load in token polices where wallet is a signer - Attach messages to transactions TODO: - burn tokens - - automatically load in token polices where wallet is a signer - stake wallet - withdraw rewards - generate manual transactions @@ -711,6 +715,26 @@ def get_utxo_creators(self, context: Optional[ChainContext] = None): for utxo in self.utxos: utxo.creator = get_utxo_creator(utxo, context) + + def get_utxo_block_times(self, context: Optional[ChainContext] = None): + + context = self._find_context(context) + + for utxo in self.utxos: + utxo.block_time = get_utxo_block_time(utxo, context) + + self.sort_utxos() + + + def sort_utxos(self, by="block_time"): + + if self.utxos: + if hasattr(self.utxos[0], by): + self.utxos.sort(key=operator.attrgetter(by)) + else: + logger.warn(f"Not all utxos have the attribute `{by}`.") + + def get_token_metadata(self, context: Optional[ChainContext] = None): context = self._find_context(context) @@ -976,6 +1000,22 @@ def get_utxo_creator(utxo: UTxO, context: ChainContext): ) +def get_utxo_block_time(utxo: UTxO, context: ChainContext): + + if isinstance(context, BlockFrostChainContext): + block_time = ( + context.api.transaction(str(utxo.input.transaction_id)) + .block_time + ) + + return block_time + + else: + logger.warn( + "Fetching UTxO block time is only possible with Blockfrost Chain Context." + ) + + def get_stake_address(address: Union[str, Address]): if isinstance(address, str): From ed5d42ba27be82080491987b23cc3dcbf8bfb196 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 6 Jul 2022 11:25:47 +0200 Subject: [PATCH 019/130] Add burning capabilities to mint_tokens --- pycardano/easy.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 1699c266..1c613027 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -906,7 +906,6 @@ def mint_tokens( if not mints_dict.get(policy_hash): mints_dict[policy_hash] = {} - mint_metadata[policy_id] = {} if isinstance(token.policy, NativeScript): native_scripts.append(token.policy) @@ -914,28 +913,39 @@ def mint_tokens( native_scripts.append(token.policy.policy) mints_dict[policy_hash][token.name] = token - if token.metadata: + if token.metadata and token.amount > 0: + if not mint_metadata.get(policy_id): + mint_metadata[policy_id] = {} mint_metadata[policy_id][token.name] = token.metadata - asset_mints = MultiAsset() + mint_multiasset = MultiAsset() + all_assets = MultiAsset() for policy_hash, tokens in mints_dict.items(): + mint_assets = Asset() assets = Asset() for token in tokens.values(): assets[AssetName(token.bytes_name)] = int(token.amount) - asset_mints[policy_hash] = assets + if token.amount > 0: + mint_assets[AssetName(token.bytes_name)] = int(token.amount) + + if mint_assets: + mint_multiasset[policy_hash] = mint_assets + all_assets[policy_hash] = assets # create mint metadata - mint_metadata = {721: mint_metadata} + if mint_metadata: + mint_metadata[721] = mint_metadata # add message if message: mint_metadata[674] = format_message(message) # Place metadata in AuxiliaryData, the format acceptable by a transaction. - auxiliary_data = AuxiliaryData(AlonzoMetadata(metadata=Metadata(mint_metadata))) + if mint_metadata: + auxiliary_data = AuxiliaryData(AlonzoMetadata(metadata=Metadata(mint_metadata))) # build the transaction builder = TransactionBuilder(context) @@ -952,15 +962,19 @@ def mint_tokens( [TokenPolicy("", policy).expiration_slot for policy in native_scripts] ) - builder.mint = asset_mints + print(mint_multiasset) + print(all_assets) + builder.mint = all_assets builder.native_scripts = native_scripts - builder.auxiliary_data = auxiliary_data + if mint_metadata: + builder.auxiliary_data = auxiliary_data if not amount: # sent min amount if none specified - amount = Lovelace(min_lovelace(Value(0, asset_mints), context)) + amount = Lovelace(min_lovelace(Value(0, mint_multiasset), context)) print("Min value =", amount) - builder.add_output(TransactionOutput(to, Value(amount.lovelace, asset_mints))) + if mint_multiasset: + builder.add_output(TransactionOutput(to, Value(amount.lovelace, mint_multiasset))) if other_signers: signing_keys = [wallet.signing_key for wallet in other_signers] + [ From dc2d9eba9eed228e9317a7b8d5dd0fc11999d7e6 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 15 Jul 2022 00:00:12 +0200 Subject: [PATCH 020/130] Add full manual transactions --- pycardano/easy.py | 343 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 303 insertions(+), 40 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 1c613027..b51a6351 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -13,8 +13,9 @@ from pycardano.address import Address from pycardano.backend.base import ChainContext from pycardano.backend.blockfrost import BlockFrostChainContext +from pycardano.cip.cip8 import sign from pycardano.exception import PyCardanoException -from pycardano.hash import TransactionId +from pycardano.hash import ScriptHash, TransactionId from pycardano.key import ( PaymentKeyPair, PaymentSigningKey, @@ -260,7 +261,7 @@ def ad_ada(self): @dataclass(unsafe_hash=True) class TokenPolicy: name: str - policy: Optional[Union[NativeScript, dict]] = field(repr=False, default=None) + policy: Optional[Union[NativeScript, dict, str]] = field(repr=False, default=None) policy_dir: Optional[Union[str, Path]] = field( repr=False, default=Path("./priv/policies") ) @@ -287,8 +288,10 @@ def __post_init__(self): @property def policy_id(self): - if self.policy: + if not isinstance(self.policy, str): return str(self.policy.hash()) + else: + return self.policy @property def id(self): @@ -300,7 +303,7 @@ def expiration_slot(self): like one generated by generate_minting_policy """ - if self.policy: + if not isinstance(self.policy, str): scripts = getattr(self.policy, "native_scripts", None) if scripts: @@ -314,7 +317,7 @@ def required_signatures(self): required_signatures = [] - if self.policy: + if not isinstance(self.policy, str): scripts = getattr(self.policy, "native_scripts", None) if scripts: @@ -485,7 +488,10 @@ def _check_metadata( def bytes_name(self): return bytes(self.name.encode("utf-8")) - + @property + def policy_id(self): + return self.policy.policy_id + def get_onchain_metadata(self, context: ChainContext): if not isinstance(context, BlockFrostChainContext): @@ -495,15 +501,39 @@ def get_onchain_metadata(self, context: ChainContext): return {} try: - metadata = context.api.asset(self.policy.id + self.hex_name).onchain_metadata.to_dict() + metadata = context.api.asset( + self.policy.id + self.hex_name + ).onchain_metadata.to_dict() except ApiError as e: - logger.error(f"Could not get onchain data, likely this asset has not been minted yet\n Blockfrost Error: {e}") + logger.error( + f"Could not get onchain data, likely this asset has not been minted yet\n Blockfrost Error: {e}" + ) metadata = {} self.metadata = metadata return metadata - - + + +@dataclass(unsafe_hash=True) +class Output: + address: Union["Wallet", Address, str] + amount: Union[Lovelace, Ada, int] + tokens: Optional[Union[Token, List[Token]]] = field(default_factory=list) + + def __post_init__(self): + + if isinstance(self.amount, int): + self.amount = Lovelace(self.amount) + + if self.tokens: + if not isinstance(self.tokens, list): + self.tokens = [self.tokens] + + if isinstance(self.address, str): + self.address = Address(self.address) + + elif isinstance(self.address, Wallet): + self.address = self.address.address @dataclass @@ -518,18 +548,18 @@ class Wallet: - Get senders of all UTxOs - Get metadata for all held tokens - Get utxo block times and sort utxos - - Mint tokens + - Mint / Burn tokens - Automatically load in token polices where wallet is a signer - Attach messages to transactions - TODO: - - burn tokens - - stake wallet - - withdraw rewards - - generate manual transactions + - sign messages + - generate manual transactions - that can do all of the above at once + - custom metadata fields - multi-output transactions - - multi-sig transactions - - sign messages + TODO: + - stake wallet + - withdraw rewards + - multi-sig transactions """ name: str @@ -695,15 +725,21 @@ def _get_tokens(self): # Convert asset dictionary into Tokens # find all policies in which the wallet is a signer - my_policies = {policy.id: policy for policy in get_all_policies(self.keys_dir/"policies") if self.verification_key.hash() in policy.required_signatures} + my_policies = { + policy.id: policy + for policy in get_all_policies(self.keys_dir / "policies") + if self.verification_key.hash() in policy.required_signatures + } token_list = [] for policy_id, assets in tokens.items(): for asset, amount in assets.items(): if policy_id in my_policies.keys(): - token_list.append(Token(my_policies[policy_id], amount=amount, name=asset)) + token_list.append( + Token(my_policies[policy_id], amount=amount, name=asset) + ) else: - token_list.append(Token(policy_id, amount=amount, name=asset)) + token_list.append(Token(TokenPolicy(name=policy_id[:8], policy=policy_id), amount=amount, name=asset)) self._token_dict = tokens self._token_list = token_list @@ -715,17 +751,15 @@ def get_utxo_creators(self, context: Optional[ChainContext] = None): for utxo in self.utxos: utxo.creator = get_utxo_creator(utxo, context) - def get_utxo_block_times(self, context: Optional[ChainContext] = None): context = self._find_context(context) for utxo in self.utxos: utxo.block_time = get_utxo_block_time(utxo, context) - + self.sort_utxos() - def sort_utxos(self, by="block_time"): if self.utxos: @@ -734,7 +768,6 @@ def sort_utxos(self, by="block_time"): else: logger.warn(f"Not all utxos have the attribute `{by}`.") - def get_token_metadata(self, context: Optional[ChainContext] = None): context = self._find_context(context) @@ -742,6 +775,14 @@ def get_token_metadata(self, context: Optional[ChainContext] = None): for token in self.tokens: token.get_onchain_metadata(context) + def sign_message(self, message, attach_cose_key=False): + + return sign( + message, + self.signing_key, + attach_cose_key=attach_cose_key, + network=self.address.network, + ) def send_ada( self, @@ -893,6 +934,7 @@ def mint_tokens( other_signers = [other_signers] # sort assets by policy_id + all_metadata = {} mints_dict = {} mint_metadata = {} native_scripts = [] @@ -900,7 +942,7 @@ def mint_tokens( if isinstance(token.policy, NativeScript): policy_hash = token.policy.hash() elif isinstance(token.policy, TokenPolicy): - policy_hash = token.policy.policy.hash() + policy_hash = ScriptHash.from_primitive(token.policy_id) policy_id = str(policy_hash) @@ -937,15 +979,17 @@ def mint_tokens( # create mint metadata if mint_metadata: - mint_metadata[721] = mint_metadata + all_metadata[721] = mint_metadata # add message if message: - mint_metadata[674] = format_message(message) + all_metadata[674] = format_message(message) # Place metadata in AuxiliaryData, the format acceptable by a transaction. - if mint_metadata: - auxiliary_data = AuxiliaryData(AlonzoMetadata(metadata=Metadata(mint_metadata))) + if all_metadata: + auxiliary_data = AuxiliaryData( + AlonzoMetadata(metadata=Metadata(all_metadata)) + ) # build the transaction builder = TransactionBuilder(context) @@ -962,11 +1006,9 @@ def mint_tokens( [TokenPolicy("", policy).expiration_slot for policy in native_scripts] ) - print(mint_multiasset) - print(all_assets) builder.mint = all_assets builder.native_scripts = native_scripts - if mint_metadata: + if all_metadata: builder.auxiliary_data = auxiliary_data if not amount: # sent min amount if none specified @@ -974,7 +1016,9 @@ def mint_tokens( print("Min value =", amount) if mint_multiasset: - builder.add_output(TransactionOutput(to, Value(amount.lovelace, mint_multiasset))) + builder.add_output( + TransactionOutput(to, Value(amount.lovelace, mint_multiasset)) + ) if other_signers: signing_keys = [wallet.signing_key for wallet in other_signers] + [ @@ -985,7 +1029,186 @@ def mint_tokens( signed_tx = builder.build_and_sign(signing_keys, change_address=self.address) - print(signed_tx.to_cbor()) + context.submit_tx(signed_tx.to_cbor()) + + if await_confirmation: + confirmed = wait_for_confirmation(str(signed_tx.id), self.context) + self.query_utxos() + + return str(signed_tx.id) + + def manual( + self, + inputs: Union[ + "Wallet", + Address, + UTxO, + str, + List["Wallet"], + List[Address], + List[UTxO], + List[str], + ], + outputs: Union[Output, List[Output]], + mints: Optional[Union[Token, List[Token]]] = [], + signers: Optional[Union["Wallet", List["Wallet"]]] = [], + message: Optional[Union[str, List[str]]] = None, + other_metadata: Optional[dict] = {}, + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, + ): + + # streamline inputs + context = self._find_context(context) + + if not isinstance(inputs, list): + inputs = [inputs] + + if not isinstance(outputs, list): + outputs = [outputs] + + if not isinstance(mints, list): + mints = [mints] + + if not isinstance(signers, list): + signers = [signers] + + all_metadata = {} + + # sort out mints + mints_dict = {} + mint_metadata = {} + native_scripts = [] + for token in mints: + if isinstance(token.policy, NativeScript): + policy_hash = token.policy.hash() + elif isinstance(token.policy, TokenPolicy): + policy_hash = ScriptHash.from_primitive(token.policy_id) + + policy_id = str(policy_hash) + + if not mints_dict.get(policy_hash): + mints_dict[policy_hash] = {} + + if isinstance(token.policy, NativeScript): + native_scripts.append(token.policy) + else: + native_scripts.append(token.policy.policy) + + mints_dict[policy_hash][token.name] = token + if token.metadata and token.amount > 0: + if not mint_metadata.get(policy_id): + mint_metadata[policy_id] = {} + mint_metadata[policy_id][token.name] = token.metadata + + mint_multiasset = MultiAsset() + all_assets = MultiAsset() + + for policy_hash, tokens in mints_dict.items(): + + mint_assets = Asset() + assets = Asset() + for token in tokens.values(): + assets[AssetName(token.bytes_name)] = int(token.amount) + + if token.amount > 0: + mint_assets[AssetName(token.bytes_name)] = int(token.amount) + + if mint_assets: + mint_multiasset[policy_hash] = mint_assets + all_assets[policy_hash] = assets + + # create mint metadata + if mint_metadata: + all_metadata[721] = mint_metadata + + # add message + if message: + all_metadata[674] = format_message(message) + + # add custom metadata + if other_metadata: + for k, v in other_metadata.items(): + check_metadata(v) + all_metadata[k] = v + + # Place metadata in AuxiliaryData, the format acceptable by a transaction. + if all_metadata: + print(all_metadata) + auxiliary_data = AuxiliaryData( + AlonzoMetadata(metadata=Metadata(all_metadata)) + ) + + # build the transaction + builder = TransactionBuilder(context) + + # add transaction inputs + for input_thing in inputs: + if isinstance(input_thing, Address) or isinstance(input_thing, str): + builder.add_input_address(input_thing) + elif isinstance(input_thing, Wallet): + builder.add_input_address(input_thing.address) + elif isinstance(input_thing, UTxO): + builder.add_input(input_thing) + + # set builder ttl to the min of the included policies + if mints: + builder.ttl = min( + [TokenPolicy("", policy).expiration_slot for policy in native_scripts] + ) + + builder.mint = all_assets + builder.native_scripts = native_scripts + + if all_metadata: + builder.auxiliary_data = auxiliary_data + + # format tokens and lovelace of outputs + for output in outputs: + multi_asset = {} + if output.tokens: + multi_asset = MultiAsset() + output_policies = {} + for token in output.tokens: + if not output_policies.get(token.policy_id): + output_policies[token.policy_id] = {} + + if output_policies[token.policy_id].get(token.name): + output_policies[token.policy_id][token.name] += token.amount + else: + output_policies[token.policy_id][token.name] = token.amount + + for policy, token_info in output_policies.items(): + + asset = Asset() + + for token_name, token_amount in token_info.items(): + + asset[AssetName(str.encode(token_name))] = token_amount + + multi_asset[ScriptHash.from_primitive(policy)] = asset + + if not output.amount.lovelace: # Calculate min lovelace if necessary + output.amount = Lovelace( + min_lovelace(Value(0, mint_multiasset), context) + ) + + builder.add_output( + TransactionOutput( + output.address, Value(output.amount.lovelace, multi_asset) + ) + ) + + if signers: + signing_keys = [wallet.signing_key for wallet in signers] + [ + self.signing_key + ] + else: + signing_keys = [self.signing_key] + + signed_tx = builder.build_and_sign(signing_keys, change_address=self.address) + + print(signed_tx) context.submit_tx(signed_tx.to_cbor()) @@ -1017,10 +1240,7 @@ def get_utxo_creator(utxo: UTxO, context: ChainContext): def get_utxo_block_time(utxo: UTxO, context: ChainContext): if isinstance(context, BlockFrostChainContext): - block_time = ( - context.api.transaction(str(utxo.input.transaction_id)) - .block_time - ) + block_time = context.api.transaction(str(utxo.input.transaction_id)).block_time return block_time @@ -1058,6 +1278,50 @@ def format_message(message: Union[str, List[str]]): return {"msg": message} +def check_metadata(to_check: Union[dict, list, str], top_level: bool = False): + """Screen the input metadata for potential issues. + Used recursively to check inside all dicts and lists of the metadata. + Use top_level=True only for the full metadata dictionary in order to check that + it is JSON serializable. + """ + + if isinstance(to_check, dict): + for key, value in to_check.items(): + + if len(str(key)) > 64: + raise MetadataFormattingException( + f"Metadata key is too long (> 64 characters): {key}\nConsider splitting into an array of shorter strings." + ) + + if isinstance(value, dict) or isinstance(value, list): + check_metadata(to_check=value) + + elif len(str(value)) > 64: + raise MetadataFormattingException( + f"Metadata field is too long (> 64 characters): {key}: {value}\nConsider splitting into an array of shorter strings." + ) + + elif isinstance(to_check, list): + + for item in to_check: + if len(str(item)) > 64: + raise MetadataFormattingException( + f"Metadata field is too long (> 64 characters): {item}\nConsider splitting into an array of shorter strings." + ) + + elif isinstance(to_check, str): + if len(to_check) > 64: + raise MetadataFormattingException( + f"Metadata field is too long (> 64 characters): {item}\nConsider splitting into an array of shorter strings." + ) + + if top_level: + try: + json.dumps(to_check) + except TypeError as e: + raise MetadataFormattingException(f"Cannot format metadata: {e}") + + def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")): if isinstance(wallet_path, str): @@ -1070,7 +1334,6 @@ def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")): def get_all_policies(policy_path: Union[str, Path] = Path("./priv/policies")): - if isinstance(policy_path, str): policy_path = Path(policy_path) From d63666fcbb80820dc73efd3f36cbf721bd273a7c Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 17 Jul 2022 17:59:38 +0200 Subject: [PATCH 021/130] Allow return of signed tx without submission --- pycardano/easy.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index b51a6351..018143f6 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -907,6 +907,7 @@ def mint_tokens( amount: Optional[Union[Ada, Lovelace]] = None, utxos: Optional[Union[UTxO, List[UTxO]]] = [], other_signers: Optional[Union["Wallet", List["Wallet"]]] = [], + change_address: Optional[Union["Wallet", Address, str]] = None, message: Optional[Union[str, List[str]]] = None, await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, @@ -933,6 +934,14 @@ def mint_tokens( if not isinstance(other_signers, list): other_signers = [other_signers] + if not change_address: + change_address = self.address + else: + if isinstance(change_address, str): + change_address = Address.from_primitive(change_address) + elif not isinstance(change_address, Address): + change_address = change_address.address + # sort assets by policy_id all_metadata = {} mints_dict = {} @@ -1052,8 +1061,11 @@ def manual( outputs: Union[Output, List[Output]], mints: Optional[Union[Token, List[Token]]] = [], signers: Optional[Union["Wallet", List["Wallet"]]] = [], + change_address: Optional[Union["Wallet", Address, str]] = None, + merge_change: Optional[bool] = True, message: Optional[Union[str, List[str]]] = None, other_metadata: Optional[dict] = {}, + submit: Optional[bool] = True, await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, ): @@ -1073,6 +1085,14 @@ def manual( if not isinstance(signers, list): signers = [signers] + if not change_address: + change_address = self.address + else: + if isinstance(change_address, str): + change_address = Address.from_primitive(change_address) + elif not isinstance(change_address, Address): + change_address = change_address.address + all_metadata = {} # sort out mints @@ -1206,9 +1226,15 @@ def manual( else: signing_keys = [self.signing_key] - signed_tx = builder.build_and_sign(signing_keys, change_address=self.address) - print(signed_tx) + signed_tx = builder.build_and_sign( + signing_keys, change_address=change_address, merge_change=merge_change + ) + + if not submit: + return signed_tx.to_cbor() + + # print(signed_tx) context.submit_tx(signed_tx.to_cbor()) From 942cdff6e4e7c90ed84c52c7c90d767aef2c5fce Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 17 Jul 2022 17:59:59 +0200 Subject: [PATCH 022/130] Automatically create stake credentials --- pycardano/easy.py | 72 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 018143f6..ad929400 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -21,6 +21,7 @@ PaymentSigningKey, PaymentVerificationKey, SigningKey, + StakeKeyPair, VerificationKey, ) from pycardano.logging import logger @@ -565,6 +566,7 @@ class Wallet: name: str address: Optional[Union[Address, str]] = None keys_dir: Optional[Union[str, Path]] = field(repr=False, default=Path("./priv")) + use_stake: Optional[bool] = field(repr=False, default=True) network: Optional[Literal["mainnet", "testnet"]] = "mainnet" # generally added later @@ -586,18 +588,22 @@ def __post_init__(self): # if not address was provided, get keys if not self.address: - self._load_or_create_key_pair() + self._load_or_create_key_pair(stake=self.use_stake) # otherwise derive the network from the address provided else: self.network = self.address.network.name.lower() + self.signing_key = None + self.verification_key = None + self.stake_signing_key = None + self.stake_verification_key = None # try to automatically create blockfrost context if not self.context: if self.network.lower() == "mainnet": if getenv("BLOCKFROST_ID"): - self.context = BlockFrostChainContext(getenv("BLOCKFROST_ID")) + self.context = BlockFrostChainContext(getenv("BLOCKFROST_ID"), network=Network.MAINNET) elif getenv("BLOCKFROST_ID_TESTNET"): - self.context = BlockFrostChainContext(getenv("BLOCKFROST_ID_TESTNET")) + self.context = BlockFrostChainContext(getenv("BLOCKFROST_ID_TESTNET"), network=Network.TESTNET) if self.context: self.query_utxos() @@ -637,22 +643,28 @@ def query_utxos(self, context: Optional[ChainContext] = None): self.lovelace = Lovelace(0) self.ada = Ada(0) + @property + def payment_address(self): + + return Address(payment_part=self.address.payment_part, network=self.address.network) + + @property def stake_address(self): - if isinstance(self.address, str): - address = Address.from_primitive(self.address) + if self.stake_signing_key or self.address.staking_part: + return Address(staking_part=self.address.staking_part, network=self.address.network) else: - address = self.address - - return Address.from_primitive( - bytes.fromhex(f"e{address.network.value}" + str(address.staking_part)) - ) + return None @property def verification_key_hash(self): return str(self.address.payment_part) + @property + def stake_verification_key_hash(self): + return str(self.address.staking_part) + @property def tokens(self): return self._token_list @@ -661,7 +673,7 @@ def tokens(self): def tokens_dict(self): return self._token_dict - def _load_or_create_key_pair(self): + def _load_or_create_key_pair(self, stake=True): if not self.keys_dir.exists(): self.keys_dir.mkdir(parents=True, exist_ok=True) @@ -669,10 +681,17 @@ def _load_or_create_key_pair(self): skey_path = self.keys_dir / f"{self.name}.skey" vkey_path = self.keys_dir / f"{self.name}.vkey" + stake_skey_path = self.keys_dir / f"{self.name}.stake.skey" + stake_vkey_path = self.keys_dir / f"{self.name}.stake.vkey" if skey_path.exists(): skey = PaymentSigningKey.load(str(skey_path)) vkey = PaymentVerificationKey.from_signing_key(skey) + + if stake and stake_skey_path.exists(): + stake_skey = PaymentSigningKey.load(str(stake_skey_path)) + stake_vkey = PaymentVerificationKey.from_signing_key(stake_skey) + logger.info(f"Wallet {self.name} found.") else: key_pair = PaymentKeyPair.generate() @@ -680,12 +699,33 @@ def _load_or_create_key_pair(self): key_pair.verification_key.save(str(vkey_path)) skey = key_pair.signing_key vkey = key_pair.verification_key + + if stake: + stake_key_pair = StakeKeyPair.generate() + stake_key_pair.signing_key.save(str(stake_skey_path)) + stake_key_pair.verification_key.save(str(stake_vkey_path)) + stake_skey = stake_key_pair.signing_key + stake_vkey = stake_key_pair.verification_key + logger.info(f"New wallet {self.name} created in {self.keys_dir}.") self.signing_key = skey self.verification_key = vkey - self.address = Address(vkey.hash(), network=Network[self.network.upper()]) + if stake: + self.stake_signing_key = stake_skey + self.stake_verification_key = stake_vkey + else: + self.stake_signing_key = None + self.stake_verification_key = None + + if stake: + self.address = Address( + vkey.hash(), stake_vkey.hash(), network=Network[self.network.upper()] + ) + else: + self.address = Address(vkey.hash(), network=Network[self.network.upper()]) + def _find_context(self, context: Optional[ChainContext] = None): """Helper function to ensure that a context is always provided when needed. @@ -739,7 +779,13 @@ def _get_tokens(self): Token(my_policies[policy_id], amount=amount, name=asset) ) else: - token_list.append(Token(TokenPolicy(name=policy_id[:8], policy=policy_id), amount=amount, name=asset)) + token_list.append( + Token( + TokenPolicy(name=policy_id[:8], policy=policy_id), + amount=amount, + name=asset, + ) + ) self._token_dict = tokens self._token_list = token_list From 1928fd561f603c241db600348711cbd95dc617dc Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 17 Jul 2022 18:00:17 +0200 Subject: [PATCH 023/130] Format with Black --- pycardano/easy.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index ad929400..a1ab6425 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -601,9 +601,13 @@ def __post_init__(self): if not self.context: if self.network.lower() == "mainnet": if getenv("BLOCKFROST_ID"): - self.context = BlockFrostChainContext(getenv("BLOCKFROST_ID"), network=Network.MAINNET) + self.context = BlockFrostChainContext( + getenv("BLOCKFROST_ID"), network=Network.MAINNET + ) elif getenv("BLOCKFROST_ID_TESTNET"): - self.context = BlockFrostChainContext(getenv("BLOCKFROST_ID_TESTNET"), network=Network.TESTNET) + self.context = BlockFrostChainContext( + getenv("BLOCKFROST_ID_TESTNET"), network=Network.TESTNET + ) if self.context: self.query_utxos() @@ -646,14 +650,17 @@ def query_utxos(self, context: Optional[ChainContext] = None): @property def payment_address(self): - return Address(payment_part=self.address.payment_part, network=self.address.network) - + return Address( + payment_part=self.address.payment_part, network=self.address.network + ) @property def stake_address(self): if self.stake_signing_key or self.address.staking_part: - return Address(staking_part=self.address.staking_part, network=self.address.network) + return Address( + staking_part=self.address.staking_part, network=self.address.network + ) else: return None @@ -726,7 +733,6 @@ def _load_or_create_key_pair(self, stake=True): else: self.address = Address(vkey.hash(), network=Network[self.network.upper()]) - def _find_context(self, context: Optional[ChainContext] = None): """Helper function to ensure that a context is always provided when needed. By default will return self.context unless a context variable has been specifically specified. @@ -1272,7 +1278,6 @@ def manual( else: signing_keys = [self.signing_key] - signed_tx = builder.build_and_sign( signing_keys, change_address=change_address, merge_change=merge_change ) From aeb0b1f1288dd68eb0fa2fe14344cd4fcb9a7fe4 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 17 Jul 2022 18:39:37 +0200 Subject: [PATCH 024/130] Support signing messages with stake keys --- pycardano/easy.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index a1ab6425..4bb82992 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -827,11 +827,24 @@ def get_token_metadata(self, context: Optional[ChainContext] = None): for token in self.tokens: token.get_onchain_metadata(context) - def sign_message(self, message, attach_cose_key=False): + def sign_message( + self, + message: str, + mode: Literal["payment", "stake"] = "payment", + attach_cose_key=False, + ): + + if mode == "payment": + signing_key = self.signing_key + elif mode == "stake": + if self.stake_signing_key: + signing_key = self.stake_signing_key + else: + raise TypeError(f"Wallet {self.name} does not have stake credentials.") return sign( message, - self.signing_key, + signing_key, attach_cose_key=attach_cose_key, network=self.address.network, ) From a37319c038c3c78ce9be362a4525740f42da51eb Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 17 Jul 2022 18:40:46 +0200 Subject: [PATCH 025/130] Update todo list --- pycardano/easy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 4bb82992..989aa01f 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -553,10 +553,10 @@ class Wallet: - Automatically load in token polices where wallet is a signer - Attach messages to transactions - sign messages - - generate manual transactions - - that can do all of the above at once - - custom metadata fields - - multi-output transactions + - generate manual transactions + - that can do all of the above at once + - custom metadata fields + - multi-output transactions TODO: - stake wallet - withdraw rewards From 30ea14b47c104055846e9313ca7d35c7b0d411b7 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 17 Jul 2022 18:41:12 +0200 Subject: [PATCH 026/130] Add script interaction --- pycardano/easy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pycardano/easy.py b/pycardano/easy.py index 989aa01f..d0e31cda 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -561,6 +561,8 @@ class Wallet: - stake wallet - withdraw rewards - multi-sig transactions + - interaction with native scripts + - interaction with plutus scripts """ name: str From 08a5997d66c8e71515577a66bb9946da5b9ebb4b Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 17 Jul 2022 23:48:44 +0200 Subject: [PATCH 027/130] Update when stake keys are generated --- pycardano/easy.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index d0e31cda..49d7a3f9 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -702,6 +702,7 @@ def _load_or_create_key_pair(self, stake=True): stake_vkey = PaymentVerificationKey.from_signing_key(stake_skey) logger.info(f"Wallet {self.name} found.") + else: key_pair = PaymentKeyPair.generate() key_pair.signing_key.save(str(skey_path)) @@ -709,15 +710,15 @@ def _load_or_create_key_pair(self, stake=True): skey = key_pair.signing_key vkey = key_pair.verification_key - if stake: - stake_key_pair = StakeKeyPair.generate() - stake_key_pair.signing_key.save(str(stake_skey_path)) - stake_key_pair.verification_key.save(str(stake_vkey_path)) - stake_skey = stake_key_pair.signing_key - stake_vkey = stake_key_pair.verification_key - logger.info(f"New wallet {self.name} created in {self.keys_dir}.") + if stake and not stake_skey_path.exists(): + stake_key_pair = StakeKeyPair.generate() + stake_key_pair.signing_key.save(str(stake_skey_path)) + stake_key_pair.verification_key.save(str(stake_vkey_path)) + stake_skey = stake_key_pair.signing_key + stake_vkey = stake_key_pair.verification_key + self.signing_key = skey self.verification_key = vkey From ea24571e48bf6a1cff05a02055b213881594e012 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 18 Jul 2022 00:14:10 +0200 Subject: [PATCH 028/130] Add Policy script property --- pycardano/easy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pycardano/easy.py b/pycardano/easy.py index 49d7a3f9..c28a7945 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -298,6 +298,10 @@ def policy_id(self): def id(self): return self.policy_id + @property + def script(self): + return self.policy + @property def expiration_slot(self): """Get the expiration slot for a simple minting policy, From 2bd2247d1c3353dc5c74a929aeab64ddce4a2eef Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 18 Jul 2022 00:14:24 +0200 Subject: [PATCH 029/130] Rename sign message to sign data --- pycardano/easy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index c28a7945..0251693c 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -834,7 +834,7 @@ def get_token_metadata(self, context: Optional[ChainContext] = None): for token in self.tokens: token.get_onchain_metadata(context) - def sign_message( + def sign_data( self, message: str, mode: Literal["payment", "stake"] = "payment", From f551bd14d96f4578f11cb52e6cfbfae6ea33d674 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 18 Jul 2022 00:15:00 +0200 Subject: [PATCH 030/130] Change print to debug statement --- pycardano/easy.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 0251693c..512476a1 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -1094,7 +1094,7 @@ def mint_tokens( if not amount: # sent min amount if none specified amount = Lovelace(min_lovelace(Value(0, mint_multiasset), context)) - print("Min value =", amount) + logger.debug("Min value =", amount) if mint_multiasset: builder.add_output( @@ -1226,7 +1226,6 @@ def manual( # Place metadata in AuxiliaryData, the format acceptable by a transaction. if all_metadata: - print(all_metadata) auxiliary_data = AuxiliaryData( AlonzoMetadata(metadata=Metadata(all_metadata)) ) @@ -1305,8 +1304,6 @@ def manual( if not submit: return signed_tx.to_cbor() - # print(signed_tx) - context.submit_tx(signed_tx.to_cbor()) if await_confirmation: From dabd052d3d614fb1cffb49893091216e3987983c Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 18 Jul 2022 00:15:46 +0200 Subject: [PATCH 031/130] Add example notebook --- examples/using_wallet_class.ipynb | 962 ++++++++++++++++++++++++++++++ 1 file changed, 962 insertions(+) create mode 100644 examples/using_wallet_class.ipynb diff --git a/examples/using_wallet_class.ipynb b/examples/using_wallet_class.ipynb new file mode 100644 index 00000000..3515d088 --- /dev/null +++ b/examples/using_wallet_class.ipynb @@ -0,0 +1,962 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using the `Wallet`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 275, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import pycardano as pyc\n", + "from pycardano.cip.cip8 import sign, verify\n", + "from pycardano.easy import Ada, Lovelace, Wallet, TokenPolicy, Token, Output\n", + "from pycardano.logging import logger\n", + "\n", + "import datetime as dt\n", + "\n", + "from pprint import pprint" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Create a wallet" + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "metadata": {}, + "outputs": [], + "source": [ + "wallet_name = \"test\"" + ] + }, + { + "cell_type": "code", + "execution_count": 260, + "metadata": {}, + "outputs": [], + "source": [ + "w = Wallet(wallet_name, network=\"testnet\")" + ] + }, + { + "cell_type": "code", + "execution_count": 131, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "addr_test1qqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l280yjxzjg0gspza08tumhczpwmy040fmfsgxrkcdk2r4hsqrc2azd" + ] + }, + "execution_count": 131, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "w.address" + ] + }, + { + "cell_type": "code", + "execution_count": 188, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "addr_test1vqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l2qmchv8f" + ] + }, + "execution_count": 188, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# address of the payment part\n", + "w.payment_address" + ] + }, + { + "cell_type": "code", + "execution_count": 189, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "stake_test1urhjfrpfy85gq3whn47dmupqhdj86h5a5cyrpmvxm9p6mcqtyhx4g" + ] + }, + "execution_count": 189, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# address of the staking part\n", + "w.stake_address" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Send ADA to wallet and query utxos" + ] + }, + { + "cell_type": "code", + "execution_count": 134, + "metadata": {}, + "outputs": [], + "source": [ + "w.query_utxos()" + ] + }, + { + "cell_type": "code", + "execution_count": 135, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "15.965142\n", + "15965142\n" + ] + } + ], + "source": [ + "print(w.ada)\n", + "print(w.lovelace)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Get the creator (sender) of each utxo" + ] + }, + { + "cell_type": "code", + "execution_count": 150, + "metadata": {}, + "outputs": [], + "source": [ + "w.get_utxo_creators()" + ] + }, + { + "cell_type": "code", + "execution_count": 152, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'addr_test1vqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l2qmchv8f'" + ] + }, + "execution_count": 152, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "w.utxos[0].creator" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Get the block height of each utxo and sort " + ] + }, + { + "cell_type": "code", + "execution_count": 154, + "metadata": {}, + "outputs": [], + "source": [ + "w.get_utxo_block_times()" + ] + }, + { + "cell_type": "code", + "execution_count": 156, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1658076307" + ] + }, + "execution_count": 156, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "w.utxos[0].block_time" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## View Tokens and get their metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 128, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Token(policy=TokenPolicy(name='6a4dfeee'), amount=1, name='PastaFun004', hex_name='506173746146756e303034', metadata={}),\n", + " Token(policy=TokenPolicy(name='6a4dfeee'), amount=2, name='PastaFun006', hex_name='506173746146756e303036', metadata={}),\n", + " Token(policy=TokenPolicy(name='6a4dfeee'), amount=4, name='PastaFun007', hex_name='506173746146756e303037', metadata={}),\n", + " Token(policy=TokenPolicy(name='6a4dfeee'), amount=2, name='PastaFun008', hex_name='506173746146756e303038', metadata={}),\n", + " Token(policy=TokenPolicy(name='83f8b74f'), amount=1, name='temptoken1', hex_name='74656d70746f6b656e31', metadata={})]" + ] + }, + "execution_count": 128, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "w.tokens" + ] + }, + { + "cell_type": "code", + "execution_count": 137, + "metadata": {}, + "outputs": [], + "source": [ + "w.get_token_metadata()" + ] + }, + { + "cell_type": "code", + "execution_count": 147, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Token(policy=TokenPolicy(name='6a4dfeee'), amount=1, name='PastaFun004', hex_name='506173746146756e303034', metadata={'name': 'Pasta Fun 004', 'image': 'ipfs://QmZ5WqN65x4dKueofyfXwiRhAoLSvue8gHp4VamKYyVKLJ', 'description': 'This is pasta!'})" + ] + }, + "execution_count": 147, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "w.tokens[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 144, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'6a4dfeee6fb7c02148c26575e594acb61dc2825ffadd0d09909c6b74'" + ] + }, + "execution_count": 144, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "w.tokens[0].policy_id" + ] + }, + { + "cell_type": "code", + "execution_count": 143, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'Pasta Fun 004',\n", + " 'image': 'ipfs://QmZ5WqN65x4dKueofyfXwiRhAoLSvue8gHp4VamKYyVKLJ',\n", + " 'description': 'This is pasta!'}" + ] + }, + "execution_count": 143, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "w.tokens[0].metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 149, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'6a4dfeee6fb7c02148c26575e594acb61dc2825ffadd0d09909c6b74': {'PastaFun004': 1,\n", + " 'PastaFun006': 2,\n", + " 'PastaFun007': 4,\n", + " 'PastaFun008': 2},\n", + " '83f8b74f0b49680c9944303afaf4333dac682135e7a05ad71bc66229': {'temptoken1': 1}}\n" + ] + } + ], + "source": [ + "pprint(w.tokens_dict)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sign a Message\n", + "- with either payment key or stake key" + ] + }, + { + "cell_type": "code", + "execution_count": 180, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "84584da301276761646472657373581d6010880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa804582073cd3b0641209194bae64953311072bab0897feef55e5df71e9e5d2f0f5aaea8a166686173686564f44b48656c6c6f20576f726c645840d53c57e6d4fb7cfaedb8225aa41b9f4f7742a85cb8f3e0638dec53ec0ce86553498d1d94fb9401d05f77d9c7bb9ca3d593ae490da69f248cb58e1dba8a47ea04\n" + ] + } + ], + "source": [ + "signed_message = w.sign_message(\"Hello World\")\n", + "print(signed_message)" + ] + }, + { + "cell_type": "code", + "execution_count": 158, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'84584da301276761646472657373581d60ef248c2921e88045d79d7cddf020bb647d5e9da60830ed86d943ade0045820a1128f3e7f44f3a3cffedf3497686ad014fc824f609daa0ccd25370e1da9b8f8a166686173686564f44b48656c6c6f20576f726c645840dcb087b86866eb465e6b0a8c4723f9482e93a14c76936f7240d8ac73335494f88cefae3d18647ebcda19648bfee556eafb506f4d0555571f8343f04bd2551905'" + ] + }, + "execution_count": 158, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "w.sign_message(\"Hello World\", mode=\"stake\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Verify Signed message" + ] + }, + { + "cell_type": "code", + "execution_count": 182, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'message': 'Hello World',\n", + " 'signing_address': addr_test1vqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l2qmchv8f,\n", + " 'verified': True}\n" + ] + } + ], + "source": [ + "verification = verify(signed_message)\n", + "pprint(verification)" + ] + }, + { + "cell_type": "code", + "execution_count": 186, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "addr_test1vqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l2qmchv8f\n", + "addr_test1vqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l2qmchv8f\n" + ] + } + ], + "source": [ + "print(w.payment_address)\n", + "print(verification[\"signing_address\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Send ADA\n", + "- for easy simple ADA-only transactions\n", + "- returns the transaction ID" + ] + }, + { + "cell_type": "code", + "execution_count": 164, + "metadata": {}, + "outputs": [], + "source": [ + "receiver = \"addr_test1qqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l280yjxzjg0gspza08tumhczpwmy040fmfsgxrkcdk2r4hsqrc2azd\"" + ] + }, + { + "cell_type": "code", + "execution_count": 165, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'12b115c85877452ac4acd41c4c03227466b8b6a4113a2a718e53f41ffa18a217'" + ] + }, + "execution_count": 165, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "w.send_ada(receiver, Ada(2.5))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Send entire UTxO(s)\n", + "- Useful for sending refunds back to sender\n", + "- With any transaction you can \"await_confirmation\". The context gets polled every N seconds and returns the transaction ID upon confirmation" + ] + }, + { + "cell_type": "code", + "execution_count": 167, + "metadata": {}, + "outputs": [], + "source": [ + "w.query_utxos()" + ] + }, + { + "cell_type": "code", + "execution_count": 171, + "metadata": {}, + "outputs": [], + "source": [ + "w.get_utxo_creators()" + ] + }, + { + "cell_type": "code", + "execution_count": 172, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'creator': 'addr_test1qqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l280yjxzjg0gspza08tumhczpwmy040fmfsgxrkcdk2r4hsqrc2azd',\n", + " 'input': {'index': 0,\n", + " 'transaction_id': TransactionId(hex='12b115c85877452ac4acd41c4c03227466b8b6a4113a2a718e53f41ffa18a217')},\n", + " 'output': {'address': addr_test1qqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l280yjxzjg0gspza08tumhczpwmy040fmfsgxrkcdk2r4hsqrc2azd,\n", + " 'amount': {'coin': 2500000, 'multi_asset': {}},\n", + " 'datum_hash': None}}\n" + ] + } + ], + "source": [ + "pprint(w.utxos[0])" + ] + }, + { + "cell_type": "code", + "execution_count": 170, + "metadata": {}, + "outputs": [], + "source": [ + "utxo_to_return = w.utxos[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 175, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0c4f49bbe6e6d9c7a252a9caf3e19d8599a00be001cb132ae5eb04eae69be269'" + ] + }, + "execution_count": 175, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "w.send_utxo(utxo_to_return.creator, utxos=utxo_to_return, await_confirmation=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Empty an entire wallet\n", + "- Useful for consolidating UTxOs or retiring the wallet\n", + "- Sends all UTxOs to one place\n", + "- Can attach a 674 metadata message to any transaction using `message`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "w.empty_wallet(receiver, message=\"Thank you!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Minting token Example" + ] + }, + { + "cell_type": "code", + "execution_count": 193, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a token policy\n", + "\n", + "policy = TokenPolicy(name=\"funTokens\")" + ] + }, + { + "cell_type": "code", + "execution_count": 197, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate expiring policy script\n", + "signers = [w]\n", + "expiration = dt.datetime(2022, 12, 12, 12, 12, 12)\n", + "\n", + "policy.generate_minting_policy(signers=w, expiration=expiration, context=w.context)" + ] + }, + { + "cell_type": "code", + "execution_count": 309, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Policy ID: b05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99\n" + ] + } + ], + "source": [ + "print(\"Policy ID:\", policy.id)" + ] + }, + { + "cell_type": "code", + "execution_count": 211, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[VerificationKeyHash(hex='10880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa8')]\n", + "Expiration Slot : 76470652\n", + "Expiration Timestamp: 2022-12-12 10:11:16.131225+00:00\n", + "Is expired: False\n" + ] + } + ], + "source": [ + "# other available info\n", + "\n", + "print(policy.required_signatures)\n", + "print(\"Expiration Slot :\", policy.expiration_slot)\n", + "print(\"Expiration Timestamp: \", policy.get_expiration_timestamp(w.context))\n", + "print(\"Is expired: \", policy.is_expired(w.context))" + ] + }, + { + "cell_type": "code", + "execution_count": 308, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'scripts': [{'keyHash': '10880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa8',\n", + " 'type': 'sig'},\n", + " {'slot': 76470652, 'type': 'before'}],\n", + " 'type': 'all'}\n" + ] + } + ], + "source": [ + "pprint(policy.script.to_dict())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Mint some tokens!" + ] + }, + { + "cell_type": "code", + "execution_count": 225, + "metadata": {}, + "outputs": [], + "source": [ + "metadata = {\n", + " \"name\": \"Fun Token 001\", \n", + " \"image\": \"ipfs://QmZ5WqN65x4dKueofyfXwiRhAoLSvue8gHp4VamKYyVKLJ\", \n", + " \"description\": \"This is a fun token.\"\n", + "}\n", + "\n", + "metadata2 = {\n", + " \"name\": \"Fun Token 002\", \n", + " \"image\": \"ipfs://QmZ5WqN65x4dKueofyfXwiRhAoLSvue8gHp4VamKYyVKLJ\", \n", + " \"description\": \"This is a second fun token.\"\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 226, + "metadata": {}, + "outputs": [], + "source": [ + "funToken1 = Token(policy, amount=1, name=\"FunToken001\", metadata=metadata)\n", + "funToken2 = Token(policy, amount=2, name=\"FunToken002\", metadata=metadata2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 227, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'c705213f6cf8f1277cdcb7bf954aa6aa2da457dd7d9f114db33038099db7e8eb'" + ] + }, + "execution_count": 227, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "w.mint_tokens(\n", + " receiver, \n", + " mints=[funToken1, funToken2], \n", + " amount=Ada(2.5), # Ada to attach to tokens\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Burn a token" + ] + }, + { + "cell_type": "code", + "execution_count": 261, + "metadata": {}, + "outputs": [], + "source": [ + "w.query_utxos()" + ] + }, + { + "cell_type": "code", + "execution_count": 263, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Token(policy=TokenPolicy(name='funTokens'), amount=1, name='FunToken001', hex_name='46756e546f6b656e303031', metadata={}),\n", + " Token(policy=TokenPolicy(name='funTokens'), amount=2, name='FunToken002', hex_name='46756e546f6b656e303032', metadata={})]" + ] + }, + "execution_count": 263, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# the policy is recognized and loaded in automatically\n", + "\n", + "w.tokens" + ] + }, + { + "cell_type": "code", + "execution_count": 269, + "metadata": {}, + "outputs": [], + "source": [ + "to_burn = w.tokens[1]" + ] + }, + { + "cell_type": "code", + "execution_count": 270, + "metadata": {}, + "outputs": [], + "source": [ + "# set amount to a negative number to burn\n", + "\n", + "to_burn.amount = -1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "w.mint_tokens(\n", + " receiver, \n", + " mints=[to_burn], \n", + " amount=Ada(4), # Ada to attach to tokens\n", + " await_confirmation=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 274, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Token(policy=TokenPolicy(name='funTokens'), amount=1, name='FunToken001', hex_name='46756e546f6b656e303031', metadata={}),\n", + " Token(policy=TokenPolicy(name='funTokens'), amount=1, name='FunToken002', hex_name='46756e546f6b656e303032', metadata={})]" + ] + }, + "execution_count": 274, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "w.tokens" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Draft a manual transaction\n", + "- Can give Wallets, addresses, string addresses, and UTxOs (or any combination thereof) as inputs.\n", + "- Must manually specify all outputs, plus a change address\n", + "- Can mint, burn, send ada, tokens, attach a message, attach any metadata\n", + "- Can return the signed transaction CBOR for sending to others in case of multisignature scenarios" + ] + }, + { + "cell_type": "code", + "execution_count": 321, + "metadata": {}, + "outputs": [], + "source": [ + "outputs = [\n", + " Output(w, Ada(2), funToken1),\n", + " Output(w, Lovelace(1700000), funToken2)\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 322, + "metadata": {}, + "outputs": [], + "source": [ + "tx = w.manual(\n", + " inputs=w,\n", + " outputs=outputs,\n", + " message=\"Fun times.\",\n", + " mints=[funToken1, funToken2],\n", + " other_metadata={111: \"Random stuff\"},\n", + " submit=False,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 323, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "84a60081825820c705213f6cf8f1277cdcb7bf954aa6aa2da457dd7d9f114db33038099db7e8eb0101828258390010880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa8ef248c2921e88045d79d7cddf020bb647d5e9da60830ed86d943ade0821a0084acc7a3581c6a4dfeee6fb7c02148c26575e594acb61dc2825ffadd0d09909c6b74a44b506173746146756e303034014b506173746146756e303036024b506173746146756e303037044b506173746146756e30303802581c83f8b74f0b49680c9944303afaf4333dac682135e7a05ad71bc66229a14a74656d70746f6b656e3101581cb05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99a14b46756e546f6b656e303031018258390010880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa8ef248c2921e88045d79d7cddf020bb647d5e9da60830ed86d943ade0821a0019f0a0a1581cb05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99a14b46756e546f6b656e30303202021a00030c61031a048ed97c075820ee0d472bcd8a990980bdbc8e8a968c384f43d66191e4d8f02776b57424657e8109a1581cb05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99a24b46756e546f6b656e303031014b46756e546f6b656e30303202a2008182582073cd3b0641209194bae64953311072bab0897feef55e5df71e9e5d2f0f5aaea85840d7f7e80d1dae76d3002d35684c8d5799cc27af2e5b5b8fe2486e9efb681f3c37ab8089d296036d834ae178168017d5eae0b3d45635655e7981cf2444d38ceb0a01818201828200581c10880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa882051a048ed97cf5d90103a100a3186f6c52616e646f6d2073747566661902a2a1636d7367816a46756e2074696d65732e1902d1a178386230353435396232643939343036353637623361363665333065313431356665323465323333643166653666303633633130663739643939a26b46756e546f6b656e303031a3646e616d656d46756e20546f6b656e2030303165696d6167657835697066733a2f2f516d5a3557714e36357834644b75656f6679665877695268416f4c53767565386748703456616d4b5979564b4c4a6b6465736372697074696f6e745468697320697320612066756e20746f6b656e2e6b46756e546f6b656e303032a3646e616d656d46756e20546f6b656e2030303265696d6167657835697066733a2f2f516d5a3557714e36357834644b75656f6679665877695268416f4c53767565386748703456616d4b5979564b4c4a6b6465736372697074696f6e781b546869732069732061207365636f6e642066756e20746f6b656e2e\n" + ] + } + ], + "source": [ + "print(tx)" + ] + }, + { + "cell_type": "code", + "execution_count": 324, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'auxiliary_data': AuxiliaryData(data=AlonzoMetadata(metadata={111: 'Random stuff', 674: {'msg': ['Fun times.']}, 721: {'b05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99': {'FunToken001': {'name': 'Fun Token 001', 'image': 'ipfs://QmZ5WqN65x4dKueofyfXwiRhAoLSvue8gHp4VamKYyVKLJ', 'description': 'This is a fun token.'}, 'FunToken002': {'name': 'Fun Token 002', 'image': 'ipfs://QmZ5WqN65x4dKueofyfXwiRhAoLSvue8gHp4VamKYyVKLJ', 'description': 'This is a second fun token.'}}}}, native_scripts=None, plutus_scripts=None)),\n", + " 'transaction_body': {'auxiliary_data_hash': AuxiliaryDataHash(hex='ee0d472bcd8a990980bdbc8e8a968c384f43d66191e4d8f02776b57424657e81'),\n", + " 'certificates': None,\n", + " 'collateral': None,\n", + " 'fee': 199777,\n", + " 'inputs': [{'index': 1,\n", + " 'transaction_id': TransactionId(hex='c705213f6cf8f1277cdcb7bf954aa6aa2da457dd7d9f114db33038099db7e8eb')}],\n", + " 'mint': {ScriptHash(hex='b05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99'): {AssetName(b'FunToken001'): 1, AssetName(b'FunToken002'): 2}},\n", + " 'network_id': None,\n", + " 'outputs': [{'address': addr_test1qqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l280yjxzjg0gspza08tumhczpwmy040fmfsgxrkcdk2r4hsqrc2azd,\n", + " 'amount': {'coin': 8694983,\n", + " 'multi_asset': {ScriptHash(hex='6a4dfeee6fb7c02148c26575e594acb61dc2825ffadd0d09909c6b74'): {AssetName(b'PastaFun004'): 1, AssetName(b'PastaFun006'): 2, AssetName(b'PastaFun007'): 4, AssetName(b'PastaFun008'): 2}, ScriptHash(hex='83f8b74f0b49680c9944303afaf4333dac682135e7a05ad71bc66229'): {AssetName(b'temptoken1'): 1}, ScriptHash(hex='b05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99'): {AssetName(b'FunToken001'): 1}}},\n", + " 'datum_hash': None},\n", + " {'address': addr_test1qqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l280yjxzjg0gspza08tumhczpwmy040fmfsgxrkcdk2r4hsqrc2azd,\n", + " 'amount': {'coin': 1700000,\n", + " 'multi_asset': {ScriptHash(hex='b05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99'): {AssetName(b'FunToken002'): 2}}},\n", + " 'datum_hash': None}],\n", + " 'required_signers': None,\n", + " 'script_data_hash': None,\n", + " 'ttl': 76470652,\n", + " 'update': None,\n", + " 'validity_start': None,\n", + " 'withdraws': None},\n", + " 'transaction_witness_set': {'bootstrap_witness': None,\n", + " 'native_scripts': [ScriptAll(_TYPE=1, native_scripts=[ScriptPubkey(_TYPE=0, key_hash=VerificationKeyHash(hex='10880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa8')), InvalidHereAfter(_TYPE=5, after=76470652)])],\n", + " 'plutus_data': None,\n", + " 'plutus_script': None,\n", + " 'redeemer': None,\n", + " 'vkey_witnesses': [{'signature': b\"\\xd7\\xf7\\xe8\\r\\x1d\\xaev\\xd3\\x00-5hL\\x8dW\\x99\\xcc'\\xaf.\"\n", + " b'[[\\x8f\\xe2Hn\\x9e\\xfbh\\x1f<7\\xab\\x80\\x89\\xd2\\x96\\x03m\\x83'\n", + " b'J\\xe1x\\x16\\x80\\x17\\xd5\\xea\\xe0\\xb3\\xd4V5e^y\\x81\\xcf$D'\n", + " b'\\xd3\\x8c\\xeb\\n',\n", + " 'vkey': {\"type\": \"\", \"description\": \"\", \"cborHex\": \"582073cd3b0641209194bae64953311072bab0897feef55e5df71e9e5d2f0f5aaea8\"}}]},\n", + " 'valid': True}\n" + ] + } + ], + "source": [ + "print(pyc.Transaction.from_cbor(tx))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "pycardano-dev", + "language": "python", + "name": "pycardano-dev" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + }, + "toc-autonumbering": true + }, + "nbformat": 4, + "nbformat_minor": 4 +} From 4e9019f63517f9883442b9d3e4b6986aca294bec Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Thu, 21 Jul 2022 13:19:17 +0200 Subject: [PATCH 032/130] Fix many type errors --- pycardano/easy.py | 230 ++++++++++++++++++++++++++-------------------- 1 file changed, 130 insertions(+), 100 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 512476a1..af9be02e 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -44,7 +44,6 @@ from pycardano.txbuilder import TransactionBuilder from pycardano.utils import min_lovelace - # set logging level to info logger.setLevel(logging.INFO) @@ -52,7 +51,7 @@ class Amount: """Base class for Cardano currency amounts.""" - def __init__(self, amount=0, amount_type="lovelace"): + def __init__(self, amount: Union[float, int] = 0, amount_type="lovelace"): self._amount = amount self._amount_type = amount_type @@ -279,7 +278,7 @@ def __post_init__(self): # look for the policy if Path(self.policy_dir / f"{self.name}.script").exists(): with open( - Path(self.policy_dir / f"{self.name}.script"), "r" + Path(self.policy_dir / f"{self.name}.script"), "r" ) as policy_file: self.policy = NativeScript.from_dict(json.load(policy_file)) @@ -338,7 +337,6 @@ def get_expiration_timestamp(self, context: ChainContext): """ if self.expiration_slot: - seconds_diff = self.expiration_slot - context.last_block_slot return datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta( @@ -351,16 +349,15 @@ def is_expired(self, context: ChainContext): """ if self.expiration_slot: - seconds_diff = self.expiration_slot - context.last_block_slot return seconds_diff < 0 def generate_minting_policy( - self, - signers: Union["Wallet", Address, List["Wallet"], List[Address]], - expiration: Optional[Union[datetime.datetime, int]] = None, - context: Optional[ChainContext] = None, + self, + signers: Union["Wallet", Address, List["Wallet"], List[Address]], + expiration: Optional[Union[datetime.datetime, int]] = None, + context: Optional[ChainContext] = None, ): script_filepath = Path(self.policy_dir / f"{self.name}.script") @@ -386,7 +383,7 @@ def generate_minting_policy( elif isinstance(expiration, datetime.datetime): if expiration.tzinfo: time_until_expiration = expiration - datetime.datetime.now( - datetime.datetime.utc + datetime.timezone.utc ) else: time_until_expiration = expiration - datetime.datetime.now() @@ -396,7 +393,10 @@ def generate_minting_policy( must_before_slot = InvalidHereAfter( last_block_slot + int(time_until_expiration.total_seconds()) ) + else: + must_before_slot = None + # noinspection PyTypeChecker policy = ScriptAll(pub_keys + [must_before_slot]) else: @@ -409,7 +409,7 @@ def generate_minting_policy( self.policy = policy @staticmethod - def _get_pub_key_hash(signer=Union["Wallet", Address]): + def _get_pub_key_hash(signer: Union["Wallet", Address]): if hasattr(signer, "verification_key"): return signer.verification_key.hash() @@ -445,7 +445,7 @@ def __str__(self): return self.name def _check_metadata( - self, to_check: Union[dict, list, str], top_level: bool = False + self, to_check: Union[dict, list, str], top_level: bool = False ): """Screen the input metadata for potential issues. Used recursively to check inside all dicts and lists of the metadata. @@ -458,7 +458,8 @@ def _check_metadata( if len(str(key)) > 64: raise MetadataFormattingException( - f"Metadata key is too long (> 64 characters): {key}\nConsider splitting into an array of shorter strings." + f"Metadata key is too long (> 64 characters): {key}\nConsider splitting into an array of " + f"shorter strings. " ) if isinstance(value, dict) or isinstance(value, list): @@ -466,7 +467,8 @@ def _check_metadata( elif len(str(value)) > 64: raise MetadataFormattingException( - f"Metadata field is too long (> 64 characters): {key}: {value}\nConsider splitting into an array of shorter strings." + f"Metadata field is too long (> 64 characters): {key}: {value}\nConsider splitting into an " + f"array of shorter strings. " ) elif isinstance(to_check, list): @@ -474,13 +476,15 @@ def _check_metadata( for item in to_check: if len(str(item)) > 64: raise MetadataFormattingException( - f"Metadata field is too long (> 64 characters): {item}\nConsider splitting into an array of shorter strings." + f"Metadata field is too long (> 64 characters): {item}\nConsider splitting into an array of " + f"shorter strings. " ) elif isinstance(to_check, str): if len(to_check) > 64: raise MetadataFormattingException( - f"Metadata field is too long (> 64 characters): {item}\nConsider splitting into an array of shorter strings." + f"Metadata field is too long (> 64 characters): {to_check}\nConsider splitting into an array of " + f"shorter strings. " ) if top_level: @@ -497,11 +501,11 @@ def bytes_name(self): def policy_id(self): return self.policy.policy_id - def get_onchain_metadata(self, context: ChainContext): + def get_on_chain_metadata(self, context: ChainContext): if not isinstance(context, BlockFrostChainContext): logger.warn( - "Getting onchain metadata is is only possible with Blockfrost Chain Context." + "Getting on-chain metadata is is only possible with Blockfrost Chain Context." ) return {} @@ -511,7 +515,7 @@ def get_onchain_metadata(self, context: ChainContext): ).onchain_metadata.to_dict() except ApiError as e: logger.error( - f"Could not get onchain data, likely this asset has not been minted yet\n Blockfrost Error: {e}" + f"Could not get on-chain data, likely this asset has not been minted yet\n Blockfrost Error: {e}" ) metadata = {} @@ -535,7 +539,7 @@ def __post_init__(self): self.tokens = [self.tokens] if isinstance(self.address, str): - self.address = Address(self.address) + self.address = Address.from_primitive(self.address) elif isinstance(self.address, Wallet): self.address = self.address.address @@ -573,7 +577,7 @@ class Wallet: address: Optional[Union[Address, str]] = None keys_dir: Optional[Union[str, Path]] = field(repr=False, default=Path("./priv")) use_stake: Optional[bool] = field(repr=False, default=True) - network: Optional[Literal["mainnet", "testnet"]] = "mainnet" + network: Optional[Literal["mainnet", "testnet", Network.MAINNET, Network.TESTNET]] = "mainnet" # generally added later lovelace: Optional[Lovelace] = field(repr=False, default=Lovelace(0)) @@ -597,6 +601,7 @@ def __post_init__(self): self._load_or_create_key_pair(stake=self.use_stake) # otherwise derive the network from the address provided else: + # noinspection PyTypeChecker self.network = self.address.network.name.lower() self.signing_key = None self.verification_key = None @@ -627,7 +632,7 @@ def query_utxos(self, context: Optional[ChainContext] = None): try: self.utxos = context.utxos(str(self.address)) except Exception as e: - logger.debug( + logger.warning( f"Error getting UTxOs. Address has likely not transacted yet. Details: {e}" ) self.utxos = [] @@ -643,12 +648,12 @@ def query_utxos(self, context: Optional[ChainContext] = None): # add up all the tokens self._get_tokens() - logger.debug( + logger.info( f"Wallet {self.name} has {len(self.utxos)} UTxOs containing a total of {self.ada} ₳." ) else: - logger.debug(f"Wallet {self.name} has no UTxOs.") + logger.info(f"Wallet {self.name} has no UTxOs.") self.lovelace = Lovelace(0) self.ada = Ada(0) @@ -697,6 +702,9 @@ def _load_or_create_key_pair(self, stake=True): stake_skey_path = self.keys_dir / f"{self.name}.stake.skey" stake_vkey_path = self.keys_dir / f"{self.name}.stake.vkey" + stake_skey = None + stake_vkey = None + if skey_path.exists(): skey = PaymentSigningKey.load(str(skey_path)) vkey = PaymentVerificationKey.from_signing_key(skey) @@ -722,16 +730,19 @@ def _load_or_create_key_pair(self, stake=True): stake_key_pair.verification_key.save(str(stake_vkey_path)) stake_skey = stake_key_pair.signing_key stake_vkey = stake_key_pair.verification_key + elif not stake: + stake_skey_path = None + stake_vkey_path = None self.signing_key = skey self.verification_key = vkey + self.signing_key_path = skey_path + self.verification_key_path = vkey_path - if stake: - self.stake_signing_key = stake_skey - self.stake_verification_key = stake_vkey - else: - self.stake_signing_key = None - self.stake_verification_key = None + self.stake_signing_key = stake_skey + self.stake_verification_key = stake_vkey + self.stake_signing_key_path = stake_skey_path + self.stake_verification_key_path = stake_vkey_path if stake: self.address = Address( @@ -742,7 +753,7 @@ def _load_or_create_key_pair(self, stake=True): def _find_context(self, context: Optional[ChainContext] = None): """Helper function to ensure that a context is always provided when needed. - By default will return self.context unless a context variable has been specifically specified. + By default, will return `self.context` unless a context variable has been specifically specified. """ if not context and not self.context: @@ -832,13 +843,13 @@ def get_token_metadata(self, context: Optional[ChainContext] = None): context = self._find_context(context) for token in self.tokens: - token.get_onchain_metadata(context) + token.get_on_chain_metadata(context) def sign_data( - self, - message: str, - mode: Literal["payment", "stake"] = "payment", - attach_cose_key=False, + self, + message: str, + mode: Literal["payment", "stake"] = "payment", + attach_cose_key=False, ): if mode == "payment": @@ -848,6 +859,8 @@ def sign_data( signing_key = self.stake_signing_key else: raise TypeError(f"Wallet {self.name} does not have stake credentials.") + else: + raise TypeError(f"Data signing mode must be `payment` or `stake`, not {mode}.") return sign( message, @@ -857,13 +870,13 @@ def sign_data( ) def send_ada( - self, - to: Union[str, Address], - amount: Union[Ada, Lovelace], - utxos: Optional[Union[UTxO, List[UTxO]]] = [], - message: Optional[Union[str, List[str]]] = None, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + self, + to: Union[str, Address], + amount: Union[Ada, Lovelace], + utxos: Optional[Union[UTxO, List[UTxO]]] = None, + message: Optional[Union[str, List[str]]] = None, + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, ): context = self._find_context(context) @@ -880,6 +893,8 @@ def send_ada( if utxos: if isinstance(utxos, UTxO): utxos = [utxos] + else: + utxos = [] builder = TransactionBuilder(context) @@ -912,12 +927,12 @@ def send_ada( return str(signed_tx.id) def send_utxo( - self, - to: Union[str, Address], - utxos: Union[UTxO, List[UTxO]], - message: Optional[Union[str, List[str]]] = None, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + self, + to: Union[str, Address], + utxos: Union[UTxO, List[UTxO]], + message: Optional[Union[str, List[str]]] = None, + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, ): # streamline inputs @@ -957,11 +972,11 @@ def send_utxo( return str(signed_tx.id) def empty_wallet( - self, - to: Union[str, Address], - message: Optional[Union[str, List[str]]] = None, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + self, + to: Union[str, Address], + message: Optional[Union[str, List[str]]] = None, + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, ): return self.send_utxo( @@ -973,16 +988,16 @@ def empty_wallet( ) def mint_tokens( - self, - to: Union[str, Address], - mints: Union[Token, List[Token]], - amount: Optional[Union[Ada, Lovelace]] = None, - utxos: Optional[Union[UTxO, List[UTxO]]] = [], - other_signers: Optional[Union["Wallet", List["Wallet"]]] = [], - change_address: Optional[Union["Wallet", Address, str]] = None, - message: Optional[Union[str, List[str]]] = None, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + self, + to: Union[str, Address], + mints: Union[Token, List[Token]], + amount: Optional[Union[Ada, Lovelace]] = None, + utxos: Optional[Union[UTxO, List[UTxO]]] = None, + other_signers: Optional[Union["Wallet", List["Wallet"]]] = None, + change_address: Optional[Union["Wallet", Address, str]] = None, + message: Optional[Union[str, List[str]]] = None, + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, ): """Under construction.""" @@ -1002,9 +1017,13 @@ def mint_tokens( if isinstance(utxos, UTxO): utxos = [utxos] + elif not utxos: + utxos = [] if not isinstance(other_signers, list): other_signers = [other_signers] + elif not other_signers: + other_signers = [] if not change_address: change_address = self.address @@ -1024,6 +1043,8 @@ def mint_tokens( policy_hash = token.policy.hash() elif isinstance(token.policy, TokenPolicy): policy_hash = ScriptHash.from_primitive(token.policy_id) + else: + policy_hash = None policy_id = str(policy_hash) @@ -1071,6 +1092,8 @@ def mint_tokens( auxiliary_data = AuxiliaryData( AlonzoMetadata(metadata=Metadata(all_metadata)) ) + else: + auxiliary_data = AuxiliaryData(Metadata()) # build the transaction builder = TransactionBuilder(context) @@ -1119,27 +1142,27 @@ def mint_tokens( return str(signed_tx.id) def manual( - self, - inputs: Union[ - "Wallet", - Address, - UTxO, - str, - List["Wallet"], - List[Address], - List[UTxO], - List[str], - ], - outputs: Union[Output, List[Output]], - mints: Optional[Union[Token, List[Token]]] = [], - signers: Optional[Union["Wallet", List["Wallet"]]] = [], - change_address: Optional[Union["Wallet", Address, str]] = None, - merge_change: Optional[bool] = True, - message: Optional[Union[str, List[str]]] = None, - other_metadata: Optional[dict] = {}, - submit: Optional[bool] = True, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + self, + inputs: Union[ + "Wallet", + Address, + UTxO, + str, + List["Wallet"], + List[Address], + List[UTxO], + List[str], + ], + outputs: Union[Output, List[Output]], + mints: Optional[Union[Token, List[Token]]] = None, + signers: Optional[Union["Wallet", List["Wallet"]]] = None, + change_address: Optional[Union["Wallet", Address, str]] = None, + merge_change: Optional[bool] = True, + message: Optional[Union[str, List[str]]] = None, + other_metadata=None, + submit: Optional[bool] = True, + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, ): # streamline inputs @@ -1153,9 +1176,13 @@ def manual( if not isinstance(mints, list): mints = [mints] + elif not mints: + mints = [] if not isinstance(signers, list): signers = [signers] + elif not signers: + signers = [] if not change_address: change_address = self.address @@ -1165,6 +1192,9 @@ def manual( elif not isinstance(change_address, Address): change_address = change_address.address + if other_metadata is None: + other_metadata = {} + all_metadata = {} # sort out mints @@ -1176,6 +1206,8 @@ def manual( policy_hash = token.policy.hash() elif isinstance(token.policy, TokenPolicy): policy_hash = ScriptHash.from_primitive(token.policy_id) + else: + policy_hash = None policy_id = str(policy_hash) @@ -1229,6 +1261,8 @@ def manual( auxiliary_data = AuxiliaryData( AlonzoMetadata(metadata=Metadata(all_metadata)) ) + else: + auxiliary_data = AuxiliaryData(Metadata()) # build the transaction builder = TransactionBuilder(context) @@ -1274,7 +1308,6 @@ def manual( asset = Asset() for token_name, token_amount in token_info.items(): - asset[AssetName(str.encode(token_name))] = token_amount multi_asset[ScriptHash.from_primitive(policy)] = asset @@ -1315,7 +1348,6 @@ def manual( # helpers def get_utxo_creator(utxo: UTxO, context: ChainContext): - if isinstance(context, BlockFrostChainContext): utxo_creator = ( context.api.transaction_utxos(str(utxo.input.transaction_id)) @@ -1332,7 +1364,6 @@ def get_utxo_creator(utxo: UTxO, context: ChainContext): def get_utxo_block_time(utxo: UTxO, context: ChainContext): - if isinstance(context, BlockFrostChainContext): block_time = context.api.transaction(str(utxo.input.transaction_id)).block_time @@ -1345,7 +1376,6 @@ def get_utxo_block_time(utxo: UTxO, context: ChainContext): def get_stake_address(address: Union[str, Address]): - if isinstance(address, str): address = Address.from_primitive(address) @@ -1355,14 +1385,14 @@ def get_stake_address(address: Union[str, Address]): def format_message(message: Union[str, List[str]]): - if isinstance(message, str): message = [message] for line in message: if len(line) > 64: raise MetadataFormattingException( - f"Message field is too long (> 64 characters): {line}\nConsider splitting into an array of shorter strings." + f"Message field is too long (> 64 characters): {line}\nConsider splitting into an array of shorter " + f"strings. " ) if not isinstance(line, str): raise MetadataFormattingException( @@ -1384,7 +1414,8 @@ def check_metadata(to_check: Union[dict, list, str], top_level: bool = False): if len(str(key)) > 64: raise MetadataFormattingException( - f"Metadata key is too long (> 64 characters): {key}\nConsider splitting into an array of shorter strings." + f"Metadata key is too long (> 64 characters): {key}\nConsider splitting into an array of shorter " + f"strings. " ) if isinstance(value, dict) or isinstance(value, list): @@ -1392,7 +1423,8 @@ def check_metadata(to_check: Union[dict, list, str], top_level: bool = False): elif len(str(value)) > 64: raise MetadataFormattingException( - f"Metadata field is too long (> 64 characters): {key}: {value}\nConsider splitting into an array of shorter strings." + f"Metadata field is too long (> 64 characters): {key}: {value}\nConsider splitting into an array " + f"of shorter strings. " ) elif isinstance(to_check, list): @@ -1400,13 +1432,15 @@ def check_metadata(to_check: Union[dict, list, str], top_level: bool = False): for item in to_check: if len(str(item)) > 64: raise MetadataFormattingException( - f"Metadata field is too long (> 64 characters): {item}\nConsider splitting into an array of shorter strings." + f"Metadata field is too long (> 64 characters): {item}\nConsider splitting into an array of " + f"shorter strings. " ) elif isinstance(to_check, str): if len(to_check) > 64: raise MetadataFormattingException( - f"Metadata field is too long (> 64 characters): {item}\nConsider splitting into an array of shorter strings." + f"Metadata field is too long (> 64 characters): {to_check}\nConsider splitting into an array of " + f"shorter strings. " ) if top_level: @@ -1417,7 +1451,6 @@ def check_metadata(to_check: Union[dict, list, str], top_level: bool = False): def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")): - if isinstance(wallet_path, str): wallet_path = Path(wallet_path) @@ -1427,7 +1460,6 @@ def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")): def get_all_policies(policy_path: Union[str, Path] = Path("./priv/policies")): - if isinstance(policy_path, str): policy_path = Path(policy_path) @@ -1437,7 +1469,6 @@ def get_all_policies(policy_path: Union[str, Path] = Path("./priv/policies")): def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext): - if isinstance(context, BlockFrostChainContext): try: @@ -1456,9 +1487,8 @@ def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext): def wait_for_confirmation( - tx_id: Union[str, TransactionId], context: ChainContext, delay: Optional[int] = 10 + tx_id: Union[str, TransactionId], context: ChainContext, delay: Optional[int] = 10 ): - if not isinstance(context, BlockFrostChainContext): logger.warn( "Confirming transactions is is only possible with Blockfrost Chain Context." From 9a77f33c28d959f442ecc6a5076716e03662346a Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 22 Jul 2022 01:21:09 +0200 Subject: [PATCH 033/130] Add stake registration, delegation, and withdrawal --- pycardano/easy.py | 124 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index af9be02e..9d82f4d1 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -565,7 +565,7 @@ class Wallet: - that can do all of the above at once - custom metadata fields - multi-output transactions - TODO: + - register wallet - stake wallet - withdraw rewards - multi-sig transactions @@ -691,6 +691,31 @@ def tokens(self): def tokens_dict(self): return self._token_dict + @property + def stake_info(self): + account_info = get_stake_info(self.stake_address, self.context) + if not account_info: + logger.warn("Stake address is not registered yet.") + return account_info + + @property + def pool_id(self): + account_info = get_stake_info(self.stake_address, self.context) + if account_info.get("pool_id"): + return account_info.get("pool_id") + else: + logger.warn("Stake address is not registered yet.") + return None + + @property + def withdrawable_amount(self): + account_info = get_stake_info(self.stake_address, self.context) + if account_info.get("withdrawable_amount"): + return Lovelace(int(account_info.get("withdrawable_amount"))) + else: + logger.warn("Stake address is not registered yet.") + return Lovelace(0) + def _load_or_create_key_pair(self, stake=True): if not self.keys_dir.exists(): @@ -987,6 +1012,83 @@ def empty_wallet( context=context, ) + def delegate( + self, + pool_hash: Union[PoolKeyHash, str], + register: Optional[bool] = True, + amount: Optional[Union[Ada, Lovelace]] = Lovelace(2000000), + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, + ): + # streamline inputs + if not self.stake_address: + raise ValueError("This wallet does not have staking keys.") + + context = self._find_context(context) + + if isinstance(pool_hash, str): + pool_hash = PoolKeyHash(bytes.fromhex(pool_hash)) + + # check registration + if register: + register = not self.stake_info.get("active") + + stake_credential = StakeCredential(self.stake_verification_key.hash()) + stake_registration = StakeRegistration(stake_credential) + stake_delegation = StakeDelegation(stake_credential, pool_keyhash=pool_hash) + + # draft the transaction + builder = TransactionBuilder(context) + builder.add_input_address(self.address) + builder.add_output(TransactionOutput(self.address, amount.lovelace)) + + if register: + builder.certificates = [stake_registration, stake_delegation] + else: + builder.certificates = [stake_delegation] + + signed_tx = builder.build_and_sign([self.signing_key, self.stake_signing_key], self.address) + + context.submit_tx(signed_tx.to_cbor()) + + if await_confirmation: + confirmed = wait_for_confirmation(str(signed_tx.id), self.context) + self.query_utxos() + + return str(signed_tx.id) + + def withdraw( + self, + withdrawal_amount: Optional[Union[Ada, Lovelace]] = None, + output_amount: Optional[Union[Ada, Lovelace]] = Lovelace(1000000), + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, + ): + # streamline inputs + if not self.stake_address: + raise ValueError("This wallet does not have staking keys.") + + context = self._find_context(context) + + if not withdrawal_amount: + # automatically detect rewards: + withdrawal_amount = self.withdrawable_amount + + builder = TransactionBuilder(context) + builder.add_input_address(self.address) + builder.add_output(TransactionOutput(self.address, output_amount.lovelace)) + builder.withdrawals = Withdrawals({self.stake_address.to_primitive(): withdrawal_amount.lovelace}) + + signed_tx = builder.build_and_sign([self.signing_key, self.stake_signing_key], self.address) + + context.submit_tx(signed_tx.to_cbor()) + + if await_confirmation: + confirmed = wait_for_confirmation(str(signed_tx.id), self.context) + self.query_utxos() + + return str(signed_tx.id) + def mint_tokens( self, to: Union[str, Address], @@ -1375,6 +1477,26 @@ def get_utxo_block_time(utxo: UTxO, context: ChainContext): ) +def get_stake_info(stake_address: Union[str, Address], context: ChainContext): + if isinstance(context, BlockFrostChainContext): + + if isinstance(stake_address, str): + stake_address = Address.from_primitive(stake_address) + + if not stake_address.staking_part: + raise TypeError(f"Address {stake_address} has no staking part.") + + try: + return context.api.accounts(str(stake_address)).to_dict() + except ApiError: + return {} + + else: + logger.warn( + "Retrieving stake address information is only possible with Blockfrost Chain Context." + ) + + def get_stake_address(address: Union[str, Address]): if isinstance(address, str): address = Address.from_primitive(address) From 0a8df7b16e723a05152e7853d80c4182be17eeb5 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 22 Jul 2022 01:21:27 +0200 Subject: [PATCH 034/130] Add stake features to manual mode --- pycardano/easy.py | 123 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 115 insertions(+), 8 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 9d82f4d1..2159a44b 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -13,9 +13,10 @@ from pycardano.address import Address from pycardano.backend.base import ChainContext from pycardano.backend.blockfrost import BlockFrostChainContext +from pycardano.certificate import StakeCredential, StakeDelegation, StakeRegistration from pycardano.cip.cip8 import sign from pycardano.exception import PyCardanoException -from pycardano.hash import ScriptHash, TransactionId +from pycardano.hash import PoolKeyHash, ScriptHash, TransactionId from pycardano.key import ( PaymentKeyPair, PaymentSigningKey, @@ -39,7 +40,7 @@ MultiAsset, TransactionOutput, UTxO, - Value, + Value, Withdrawals, ) from pycardano.txbuilder import TransactionBuilder from pycardano.utils import min_lovelace @@ -568,6 +569,8 @@ class Wallet: - register wallet - stake wallet - withdraw rewards + - register, stake, and withdraw in manual transactions + TODO: - multi-sig transactions - interaction with native scripts - interaction with plutus scripts @@ -1257,7 +1260,13 @@ def manual( ], outputs: Union[Output, List[Output]], mints: Optional[Union[Token, List[Token]]] = None, - signers: Optional[Union["Wallet", List["Wallet"]]] = None, + signers: Optional[Union["Wallet", List["Wallet"], SigningKey, List[SigningKey]]] = None, + # TODO: Add signing keys as types to signers + stake_registration: Optional[ + Union[bool, "Wallet", Address, str, List[Address], List["Wallet"], List[str]] + ] = None, + delegations: Optional[Union[str, dict, PoolKeyHash]] = None, + withdrawals: Optional[Union[bool, dict]] = None, change_address: Optional[Union["Wallet", Address, str]] = None, merge_change: Optional[bool] = True, message: Optional[Union[str, List[str]]] = None, @@ -1276,12 +1285,17 @@ def manual( if not isinstance(outputs, list): outputs = [outputs] - if not isinstance(mints, list): + if mints and not isinstance(mints, list): mints = [mints] elif not mints: mints = [] - if not isinstance(signers, list): + if stake_registration and not isinstance(stake_registration, list) and not isinstance(stake_registration, bool): + stake_registration = [stake_registration] + elif not stake_registration: + stake_registration = [] + + if signers and not isinstance(signers, list): signers = [signers] elif not signers: signers = [] @@ -1366,6 +1380,84 @@ def manual( else: auxiliary_data = AuxiliaryData(Metadata()) + # create stake registrations, delegations + certificates = [] + if stake_registration: # add registrations + if isinstance(stake_registration, bool): + # register current wallet + stake_credential = StakeCredential(self.stake_verification_key.hash()) + certificates.append(StakeRegistration(stake_credential)) + if self.stake_signing_key not in signers: + signers.append(self.stake_signing_key) + else: + for stake in stake_registration: + if isinstance(stake, str): + stake_hash = Address.from_primitive(stake).staking_part + elif isinstance(stake, self.__class__): + stake_hash = self.stake_verification_key.hash() + else: + stake_hash = stake.staking_part + stake_credential = StakeCredential(stake_hash) + certificates.append(StakeRegistration(stake_credential)) + if delegations: # add delegations + if isinstance(delegations, str): # register current wallet + pool_hash = PoolKeyHash(bytes.fromhex(delegations)) + stake_credential = StakeCredential(self.stake_verification_key.hash()) + certificates.append(StakeDelegation(stake_credential, pool_keyhash=pool_hash)) + if self.stake_signing_key not in signers: + signers.append(self.stake_signing_key) + elif isinstance(delegations, PoolKeyHash): # register current wallet + stake_credential = StakeCredential(self.stake_verification_key.hash()) + certificates.append(StakeDelegation(stake_credential, pool_keyhash=delegations)) + else: + for key, value in delegations: + # get stake hash from key + if isinstance(key, str): + stake_hash = Address.from_primitive(key).staking_part + elif isinstance(key, self.__class__): + stake_hash = self.stake_verification_key.hash() + else: + stake_hash = key.staking_part + + # get pool hash from value: + if isinstance(value, str): + pool_hash = PoolKeyHash(bytes.fromhex(value)) + else: + pool_hash = value + + stake_credential = StakeCredential(stake_hash) + certificates.append(StakeDelegation(stake_credential, pool_keyhash=pool_hash)) + + # withdrawals + withdraw = {} + if isinstance(withdrawals, bool): # withdraw current wallet + withdraw[self.stake_address.to_primitive()] = self.withdrawable_amount.lovelace + if self.stake_signing_key not in signers: + signers.append(self.stake_signing_key) + elif isinstance(withdrawals, dict): + for key, value in withdrawals.items(): + if isinstance(key, Address): + stake_address = key + elif isinstance(key, self.__class__): + stake_address = key.stake_address + else: + stake_address = Address.from_primitive(key) + + if isinstance(value, Amount): + withdrawal_amount = value + elif isinstance(value, bool) or value == "all": # withdraw all + account_info = get_stake_info(stake_address, self.context) + if account_info.get("withdrawable_amount"): + withdrawal_amount = Lovelace(int(account_info.get("withdrawable_amount"))) + else: + logger.warn(f"Stake address {stake_address} is not registered yet.") + withdrawal_amount = Lovelace(0) + else: + withdrawal_amount = Lovelace(0) + + withdraw[stake_address.staking_part.to_primitive()] = withdrawal_amount.as_lovelace().amount + + # build the transaction builder = TransactionBuilder(context) @@ -1425,10 +1517,25 @@ def manual( ) ) + # add registration + delegation certificates + if certificates: + builder.certificates = certificates + + # add withdrawals + if withdraw: + builder.withdrawals = Withdrawals(withdraw) + if signers: - signing_keys = [wallet.signing_key for wallet in signers] + [ - self.signing_key - ] + signing_keys = [] + for signer in signers: + if isinstance(signer, self.__class__): + signing_keys.append(signer.signing_key) + else: + signing_keys.append(signer) + + if self.signing_key not in signing_keys: + signing_keys.insert(0, self.signing_key) + else: signing_keys = [self.signing_key] From bd5e7f10c418627109dd0330a4ef9f4e9ceff638 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 22 Jul 2022 01:23:07 +0200 Subject: [PATCH 035/130] rename manual to transact --- pycardano/easy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 2159a44b..4ac69bf2 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -1246,7 +1246,7 @@ def mint_tokens( return str(signed_tx.id) - def manual( + def transact( self, inputs: Union[ "Wallet", @@ -1261,7 +1261,6 @@ def manual( outputs: Union[Output, List[Output]], mints: Optional[Union[Token, List[Token]]] = None, signers: Optional[Union["Wallet", List["Wallet"], SigningKey, List[SigningKey]]] = None, - # TODO: Add signing keys as types to signers stake_registration: Optional[ Union[bool, "Wallet", Address, str, List[Address], List["Wallet"], List[str]] ] = None, From 4cabb3f4957121f9a8319f445294ad8e61cdfa95 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 24 Aug 2022 23:39:26 +0200 Subject: [PATCH 036/130] Fix some typos --- pycardano/easy.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/pycardano/easy.py b/pycardano/easy.py index 4ac69bf2..060c7bc1 100644 --- a/pycardano/easy.py +++ b/pycardano/easy.py @@ -475,11 +475,7 @@ def _check_metadata( elif isinstance(to_check, list): for item in to_check: - if len(str(item)) > 64: - raise MetadataFormattingException( - f"Metadata field is too long (> 64 characters): {item}\nConsider splitting into an array of " - f"shorter strings. " - ) + self._check_metadata(to_check=item) elif isinstance(to_check, str): if len(to_check) > 64: @@ -623,8 +619,8 @@ def __post_init__(self): getenv("BLOCKFROST_ID_TESTNET"), network=Network.TESTNET ) - if self.context: - self.query_utxos() + #if self.context: + # self.query_utxos() logger.info(self.__repr__()) @@ -817,11 +813,13 @@ def _get_tokens(self): # Convert asset dictionary into Tokens # find all policies in which the wallet is a signer - my_policies = { - policy.id: policy - for policy in get_all_policies(self.keys_dir / "policies") - if self.verification_key.hash() in policy.required_signatures - } + my_policies = {} + if self.verification_key: + my_policies = { + policy.id: policy + for policy in get_all_policies(self.keys_dir / "policies") + if self.verification_key.hash() in policy.required_signatures + } token_list = [] for policy_id, assets in tokens.items(): @@ -1236,7 +1234,7 @@ def mint_tokens( else: signing_keys = [self.signing_key] - signed_tx = builder.build_and_sign(signing_keys, change_address=self.address) + signed_tx = builder.build_and_sign(signing_keys, change_address=change_address) context.submit_tx(signed_tx.to_cbor()) @@ -1614,7 +1612,7 @@ def get_stake_address(address: Union[str, Address]): def format_message(message: Union[str, List[str]]): if isinstance(message, str): - message = [message] + message = [message[i:i+64] for i in range(0, len(message), 64)] for line in message: if len(line) > 64: From 65991082e928ed4d4e05820d58bf1143848a84dc Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Thu, 25 Aug 2022 10:42:24 +0200 Subject: [PATCH 037/130] Start adding docstrings --- pycardano/{easy.py => wallet.py} | 45 ++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) rename pycardano/{easy.py => wallet.py} (96%) diff --git a/pycardano/easy.py b/pycardano/wallet.py similarity index 96% rename from pycardano/easy.py rename to pycardano/wallet.py index 060c7bc1..e78b56b7 100644 --- a/pycardano/easy.py +++ b/pycardano/wallet.py @@ -68,6 +68,7 @@ def __init__(self, amount: Union[float, int] = 0, amount_type="lovelace"): @property def amount(self): + """Encodes the contained amount.""" if self._amount_type == "lovelace": return self.lovelace @@ -232,35 +233,50 @@ def __round__(self): class Lovelace(Amount): - def __init__(self, amount=0): + """Stores an amount of Lovelace and automatically handles most currency math.""" + + def __init__(self, amount: int = 0): super().__init__(amount, "lovelace") def __repr__(self): return f"Lovelace({self.lovelace})" def as_lovelace(self): + """Returns a copy.""" return Lovelace(self.lovelace) def as_ada(self): + """Converts the Lovelace to an Ada class.""" return Ada(self.ada) class Ada(Amount): - def __init__(self, amount=0): + """Stores an amount of Ada and automatically handles most currency math.""" + + def __init__(self, amount: Union[int, float] = 0): super().__init__(amount, "ada") def __repr__(self): return f"Ada({self.ada})" def as_lovelace(self): + """Converts the Ada to a Lovelace class.""" return Lovelace(self.lovelace) def ad_ada(self): + """Returns a copy.""" return Ada(self.ada) @dataclass(unsafe_hash=True) class TokenPolicy: + """Class for the creation and management of fungible and non-fungible token policies. + + Args: + name (str): The name of the token policy, used for saving and loading keys. + policy (Optional[Union[NativeScript, dict, str]]): Direct provide a policy to use. + policy_dir (Optional[Union[str, Path]]): The directory to save and load the policy from. + """ name: str policy: Optional[Union[NativeScript, dict, str]] = field(repr=False, default=None) policy_dir: Optional[Union[str, Path]] = field( @@ -360,6 +376,15 @@ def generate_minting_policy( expiration: Optional[Union[datetime.datetime, int]] = None, context: Optional[ChainContext] = None, ): + """Generate a minting policy for the given signers with an optional expiration. + Policy is generated with CIP-25 in mind. + + Args: + signers (Union[Wallet, Address, List[Wallet], List[Address]]): The signer(s) of the policy. + expiration (Optional[Union[datetime.datetime, int]]): The expiration of the policy. + If given as a datetime, it will be roughly converted to a slot. + context (Optional[ChainContext]): A context is needed to estimate the expiration slot from a datetime. + """ script_filepath = Path(self.policy_dir / f"{self.name}.script") @@ -422,6 +447,16 @@ def _get_pub_key_hash(signer: Union["Wallet", Address]): @dataclass(unsafe_hash=True) class Token: + """Class that represents a token with an attached policy and amount. + + Attributes: + policy (Union[str, NativeScript]): The policy of the token. + The policy need not necessarily be owned by the user. + amount (int): The amount of tokens. + name (str): The name of the token. Either the name or the hex name should be provided. + hex_name (str): The name of the token as a hex string. + metadata (dict): The metadata attached to the token + """ policy: Union[NativeScript, TokenPolicy] amount: int name: Optional[str] = field(default="") @@ -499,6 +534,12 @@ def policy_id(self): return self.policy.policy_id def get_on_chain_metadata(self, context: ChainContext): + """Get the on-chain metadata of the token. + + Args: + context (ChainContext): A chain context is necessary to fetch the on-chain metadata. + Only BlockFrost chain context is supported at the moment. + """ if not isinstance(context, BlockFrostChainContext): logger.warn( From 23053c78f10dfaed6188b86f82e05d8cc74599ae Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 29 Aug 2022 21:56:25 +0200 Subject: [PATCH 038/130] Start commenting in the code --- pycardano/wallet.py | 448 +++++++++++++++++++++++++++++--------------- 1 file changed, 301 insertions(+), 147 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index e78b56b7..81225084 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -40,7 +40,8 @@ MultiAsset, TransactionOutput, UTxO, - Value, Withdrawals, + Value, + Withdrawals, ) from pycardano.txbuilder import TransactionBuilder from pycardano.utils import min_lovelace @@ -271,12 +272,13 @@ def ad_ada(self): @dataclass(unsafe_hash=True) class TokenPolicy: """Class for the creation and management of fungible and non-fungible token policies. - + Args: name (str): The name of the token policy, used for saving and loading keys. policy (Optional[Union[NativeScript, dict, str]]): Direct provide a policy to use. policy_dir (Optional[Union[str, Path]]): The directory to save and load the policy from. """ + name: str policy: Optional[Union[NativeScript, dict, str]] = field(repr=False, default=None) policy_dir: Optional[Union[str, Path]] = field( @@ -295,7 +297,7 @@ def __post_init__(self): # look for the policy if Path(self.policy_dir / f"{self.name}.script").exists(): with open( - Path(self.policy_dir / f"{self.name}.script"), "r" + Path(self.policy_dir / f"{self.name}.script"), "r" ) as policy_file: self.policy = NativeScript.from_dict(json.load(policy_file)) @@ -371,17 +373,17 @@ def is_expired(self, context: ChainContext): return seconds_diff < 0 def generate_minting_policy( - self, - signers: Union["Wallet", Address, List["Wallet"], List[Address]], - expiration: Optional[Union[datetime.datetime, int]] = None, - context: Optional[ChainContext] = None, + self, + signers: Union["Wallet", Address, List["Wallet"], List[Address]], + expiration: Optional[Union[datetime.datetime, int]] = None, + context: Optional[ChainContext] = None, ): """Generate a minting policy for the given signers with an optional expiration. Policy is generated with CIP-25 in mind. Args: signers (Union[Wallet, Address, List[Wallet], List[Address]]): The signer(s) of the policy. - expiration (Optional[Union[datetime.datetime, int]]): The expiration of the policy. + expiration (Optional[Union[datetime.datetime, int]]): The expiration of the policy. If given as a datetime, it will be roughly converted to a slot. context (Optional[ChainContext]): A context is needed to estimate the expiration slot from a datetime. """ @@ -450,13 +452,14 @@ class Token: """Class that represents a token with an attached policy and amount. Attributes: - policy (Union[str, NativeScript]): The policy of the token. + policy (Union[str, NativeScript]): The policy of the token. The policy need not necessarily be owned by the user. amount (int): The amount of tokens. name (str): The name of the token. Either the name or the hex name should be provided. hex_name (str): The name of the token as a hex string. metadata (dict): The metadata attached to the token """ + policy: Union[NativeScript, TokenPolicy] amount: int name: Optional[str] = field(default="") @@ -481,7 +484,7 @@ def __str__(self): return self.name def _check_metadata( - self, to_check: Union[dict, list, str], top_level: bool = False + self, to_check: Union[dict, list, str], top_level: bool = False ): """Screen the input metadata for potential issues. Used recursively to check inside all dicts and lists of the metadata. @@ -535,7 +538,7 @@ def policy_id(self): def get_on_chain_metadata(self, context: ChainContext): """Get the on-chain metadata of the token. - + Args: context (ChainContext): A chain context is necessary to fetch the on-chain metadata. Only BlockFrost chain context is supported at the moment. @@ -563,6 +566,16 @@ def get_on_chain_metadata(self, context: ChainContext): @dataclass(unsafe_hash=True) class Output: + """Represents the output of a transaction. + + Attributes: + address (Union[Wallet, Address, str]): The address of receiver of this output. + amount (Union[Lovelace, Ada, int]): The amount of Lovelace (or Ada) being sent to the address. + Should generally satisfy the minimum ADA requirement for any attached tokens. + tokens (Optional[Union[Token, List[Token]]]): Token or list of Tokens to be sent to the address. + Amount of each token to send should be defined in each Token object. + """ + address: Union["Wallet", Address, str] amount: Union[Lovelace, Ada, int] tokens: Optional[Union[Token, List[Token]]] = field(default_factory=list) @@ -585,8 +598,15 @@ def __post_init__(self): @dataclass class Wallet: - """An address for which you own the keys or will later create them. - Already does: + """Create or load a wallet for which you own the keys or will later create them. + NOTE: BlockFrost Chain Context can be automatically loaded by setting the following environment variables: + `BLOCKFROST_ID_0` for testnet + `BLOCKFROST_ID_1` for mainnet + `BLOCKFROST_ID_2` for preview + `BLOCKFROST_ID_3` for devnet + Otherwise, a custom Chain Context can be provided when necessary. + + Currently you can already: - Generate keys - Load keys - Fetch utxos @@ -597,29 +617,47 @@ class Wallet: - Get utxo block times and sort utxos - Mint / Burn tokens - Automatically load in token polices where wallet is a signer + - Automatically create BlockFrost Chain Context - Attach messages to transactions - - sign messages - - generate manual transactions - - that can do all of the above at once - - custom metadata fields - - multi-output transactions - - register wallet - - stake wallet - - withdraw rewards - - register, stake, and withdraw in manual transactions - TODO: - - multi-sig transactions - - interaction with native scripts - - interaction with plutus scripts + - Sign messages + - Add custom metadata fields + - Multi-output transactions + - Register wallet + - Stake wallet + - Withdraw rewards + - Generate fully manual transactions that can do any / all of the above + + Future additions (TODO list): + - Create and sign multi-sig transactions + - Interaction with native scripts + - Interaction with plutus scripts + - Load wallets with mnemonic phrase + + Attributes: + name (str): The name of the wallet. This is required and keys will be + automatically generated and saved with this name. If the wallet already exists in keys_dir, + it will be loaded automatically. + address (Optional[Union[Address, str]]): Optionally provide an address to use a wallet without signing capabilities. + keys_dir (Optional[Union[str, Path]]): Directory in which to save the keys. Defaults to "./priv". + use_stake (Optional[bool]): Whether to use a stake address for this wallet. Defaults to True. + network (Optional[str, Network]): The network to use for the wallet. + Can pick from "mainnet", "testnet", "preview", "preprod". Defaults to "mainnet". + BlockFrost Chain Context will be automatically loaded for this network if + the API key is set in the environment variables. + """ name: str address: Optional[Union[Address, str]] = None keys_dir: Optional[Union[str, Path]] = field(repr=False, default=Path("./priv")) use_stake: Optional[bool] = field(repr=False, default=True) - network: Optional[Literal["mainnet", "testnet", Network.MAINNET, Network.TESTNET]] = "mainnet" + network: Optional[ + Literal[ + "mainnet", "testnet", "preview", "preprod", Network.MAINNET, Network.TESTNET + ] + ] = "mainnet" - # generally added later + # generally added later upon initialization lovelace: Optional[Lovelace] = field(repr=False, default=Lovelace(0)) ada: Optional[Ada] = field(repr=True, default=Ada(0)) signing_key: Optional[SigningKey] = field(repr=False, default=None) @@ -660,44 +698,11 @@ def __post_init__(self): getenv("BLOCKFROST_ID_TESTNET"), network=Network.TESTNET ) - #if self.context: + # if self.context: # self.query_utxos() logger.info(self.__repr__()) - def query_utxos(self, context: Optional[ChainContext] = None): - - context = self._find_context(context) - - try: - self.utxos = context.utxos(str(self.address)) - except Exception as e: - logger.warning( - f"Error getting UTxOs. Address has likely not transacted yet. Details: {e}" - ) - self.utxos = [] - - # calculate total ada - if self.utxos: - - self.lovelace = Lovelace( - sum([utxo.output.amount.coin for utxo in self.utxos]) - ) - self.ada = self.lovelace.as_ada() - - # add up all the tokens - self._get_tokens() - - logger.info( - f"Wallet {self.name} has {len(self.utxos)} UTxOs containing a total of {self.ada} ₳." - ) - - else: - logger.info(f"Wallet {self.name} has no UTxOs.") - - self.lovelace = Lovelace(0) - self.ada = Ada(0) - @property def payment_address(self): @@ -757,6 +762,7 @@ def withdrawable_amount(self): return Lovelace(0) def _load_or_create_key_pair(self, stake=True): + """Look for a key pair in the keys directory. If not found, create a new key pair.""" if not self.keys_dir.exists(): self.keys_dir.mkdir(parents=True, exist_ok=True) @@ -829,6 +835,7 @@ def _find_context(self, context: Optional[ChainContext] = None): return self.context def _get_tokens(self): + """Gather up all tokens across all UTxOs.""" # loop through the utxos and sum up all tokens tokens = {} @@ -881,7 +888,50 @@ def _get_tokens(self): self._token_dict = tokens self._token_list = token_list + def query_utxos(self, context: Optional[ChainContext] = None): + """Query the blockchain for all UTxOs associated with this wallet. + + Args: + context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + """ + + context = self._find_context(context) + + try: + self.utxos = context.utxos(str(self.address)) + except Exception as e: + logger.warning( + f"Error getting UTxOs. Address has likely not transacted yet. Details: {e}" + ) + self.utxos = [] + + # calculate total ada + if self.utxos: + + self.lovelace = Lovelace( + sum([utxo.output.amount.coin for utxo in self.utxos]) + ) + self.ada = self.lovelace.as_ada() + + # add up all the tokens + self._get_tokens() + + logger.info( + f"Wallet {self.name} has {len(self.utxos)} UTxOs containing a total of {self.ada} ₳." + ) + + else: + logger.info(f"Wallet {self.name} has no UTxOs.") + + self.lovelace = Lovelace(0) + self.ada = Ada(0) + def get_utxo_creators(self, context: Optional[ChainContext] = None): + """Get a list of all addresses that created each of the UTxOs in the wallet. + + Args: + context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + """ context = self._find_context(context) @@ -889,6 +939,11 @@ def get_utxo_creators(self, context: Optional[ChainContext] = None): utxo.creator = get_utxo_creator(utxo, context) def get_utxo_block_times(self, context: Optional[ChainContext] = None): + """Get a list of the creation block number for each UTxO in the wallet. + + Args: + context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + """ context = self._find_context(context) @@ -898,6 +953,11 @@ def get_utxo_block_times(self, context: Optional[ChainContext] = None): self.sort_utxos() def sort_utxos(self, by="block_time"): + """Sort the UTxOs in the wallet by the specified field. + + Args: + by (str): The field by which to sort UTxOs. Defaults to "block_time". + """ if self.utxos: if hasattr(self.utxos[0], by): @@ -906,6 +966,11 @@ def sort_utxos(self, by="block_time"): logger.warn(f"Not all utxos have the attribute `{by}`.") def get_token_metadata(self, context: Optional[ChainContext] = None): + """Fetch the on-chain metadata for each token in the wallet. + + Args: + context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + """ context = self._find_context(context) @@ -913,11 +978,20 @@ def get_token_metadata(self, context: Optional[ChainContext] = None): token.get_on_chain_metadata(context) def sign_data( - self, - message: str, - mode: Literal["payment", "stake"] = "payment", - attach_cose_key=False, + self, + message: str, + mode: Literal["payment", "stake"] = "payment", + attach_cose_key=False, ): + """Sign a message with the wallet's private payment or stake keys following CIP-8. + NOTE: CIP-8 is not yet finalized so this could be changed in the future. + + Args: + message (str): The message to sign. + mode (Optional[Literal["payment", "stake"]]): The keys to use for signing. Defaults to "payment". + attach_cose_key (bool): Whether to attach the COSE key to the signature. Defaults to False. + At the moment, Eternl currently does not attach the COSE key, while Nami does. + """ if mode == "payment": signing_key = self.signing_key @@ -927,7 +1001,9 @@ def sign_data( else: raise TypeError(f"Wallet {self.name} does not have stake credentials.") else: - raise TypeError(f"Data signing mode must be `payment` or `stake`, not {mode}.") + raise TypeError( + f"Data signing mode must be `payment` or `stake`, not {mode}." + ) return sign( message, @@ -937,14 +1013,26 @@ def sign_data( ) def send_ada( - self, - to: Union[str, Address], - amount: Union[Ada, Lovelace], - utxos: Optional[Union[UTxO, List[UTxO]]] = None, - message: Optional[Union[str, List[str]]] = None, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + self, + to: Union[str, Address], + amount: Union[Ada, Lovelace], + utxos: Optional[Union[UTxO, List[UTxO]]] = None, + message: Optional[Union[str, List[str]]] = None, + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, ): + """Create a simple transaction in which Ada is sent to a single recipient. + + + Args: + to (Union[str, Address]): The address to which to send the Ada. + amount (Union[Ada, Lovelace]): The amount of Ada to send. + utxos (Optional[Union[UTxO, List[UTxO]]]): The UTxOs to use for the transaction. + By default all wallet UTxOs are considered. + message (Optional[Union[str, List[str]]]): Optional message to include in the transaction (CIP-20). + await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. + context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + """ context = self._find_context(context) @@ -994,13 +1082,22 @@ def send_ada( return str(signed_tx.id) def send_utxo( - self, - to: Union[str, Address], - utxos: Union[UTxO, List[UTxO]], - message: Optional[Union[str, List[str]]] = None, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + self, + to: Union[str, Address], + utxos: Union[UTxO, List[UTxO]], + message: Optional[Union[str, List[str]]] = None, + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, ): + """Send all of the contents (ADA and tokens) of specified UTxO(s) to a single recipient. + + Args: + to (Union[str, Address]): The address to which to send the UTxO contents. + utxos (Optional[Union[UTxO, List[UTxO]]]): The UTxO(s) to use for the transaction. + message (Optional[Union[str, List[str]]]): Optional message to include in the transaction (CIP-20). + await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. + context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + """ # streamline inputs context = self._find_context(context) @@ -1039,13 +1136,23 @@ def send_utxo( return str(signed_tx.id) def empty_wallet( - self, - to: Union[str, Address], - message: Optional[Union[str, List[str]]] = None, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + self, + to: Union[str, Address], + message: Optional[Union[str, List[str]]] = None, + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, ): + """Send all of the contents (ADA and tokens) of the wallet to a single recipient. + The current wallet will be left completely empty + + Args: + to (Union[str, Address]): The address to which to send the entire contents of the wallet. + message (Optional[Union[str, List[str]]]): Optional message to include in the transaction (CIP-20). + await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. + context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + """ + return self.send_utxo( to=to, utxos=self.utxos, @@ -1055,13 +1162,25 @@ def empty_wallet( ) def delegate( - self, - pool_hash: Union[PoolKeyHash, str], - register: Optional[bool] = True, - amount: Optional[Union[Ada, Lovelace]] = Lovelace(2000000), - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + self, + pool_hash: Union[PoolKeyHash, str], + register: Optional[bool] = True, + amount: Optional[Union[Ada, Lovelace]] = Lovelace(2000000), + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, ): + """Delegate the current wallet to a pool. + + Args: + pool_hash (Union[PoolKeyHash, str]): The hash of the pool to which to delegate. + register (Optional[bool]): Whether to register the pool with the network. Defaults to True. + If True, this will skip registration is the wallet is already registered. + amount (Optional[Union[Ada, Lovelace]]): The amount of Ada to attach to the delegation transaction. + Defaults to 2 Ada. + await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. + context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + """ + # streamline inputs if not self.stake_address: raise ValueError("This wallet does not have staking keys.") @@ -1089,7 +1208,9 @@ def delegate( else: builder.certificates = [stake_delegation] - signed_tx = builder.build_and_sign([self.signing_key, self.stake_signing_key], self.address) + signed_tx = builder.build_and_sign( + [self.signing_key, self.stake_signing_key], self.address + ) context.submit_tx(signed_tx.to_cbor()) @@ -1100,12 +1221,20 @@ def delegate( return str(signed_tx.id) def withdraw( - self, - withdrawal_amount: Optional[Union[Ada, Lovelace]] = None, - output_amount: Optional[Union[Ada, Lovelace]] = Lovelace(1000000), - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + self, + withdrawal_amount: Optional[Union[Ada, Lovelace]] = None, + output_amount: Optional[Union[Ada, Lovelace]] = Lovelace(1000000), + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, ): + """Withdraw staking rewards. + + Args: + withdrawal_amount (Optional[Union[Ada, Lovelace]]): The amount of Ada to withdraw. + Defaults to the entire rewards balance. + output_amount + """ + # streamline inputs if not self.stake_address: raise ValueError("This wallet does not have staking keys.") @@ -1119,9 +1248,13 @@ def withdraw( builder = TransactionBuilder(context) builder.add_input_address(self.address) builder.add_output(TransactionOutput(self.address, output_amount.lovelace)) - builder.withdrawals = Withdrawals({self.stake_address.to_primitive(): withdrawal_amount.lovelace}) + builder.withdrawals = Withdrawals( + {self.stake_address.to_primitive(): withdrawal_amount.lovelace} + ) - signed_tx = builder.build_and_sign([self.signing_key, self.stake_signing_key], self.address) + signed_tx = builder.build_and_sign( + [self.signing_key, self.stake_signing_key], self.address + ) context.submit_tx(signed_tx.to_cbor()) @@ -1132,16 +1265,16 @@ def withdraw( return str(signed_tx.id) def mint_tokens( - self, - to: Union[str, Address], - mints: Union[Token, List[Token]], - amount: Optional[Union[Ada, Lovelace]] = None, - utxos: Optional[Union[UTxO, List[UTxO]]] = None, - other_signers: Optional[Union["Wallet", List["Wallet"]]] = None, - change_address: Optional[Union["Wallet", Address, str]] = None, - message: Optional[Union[str, List[str]]] = None, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + self, + to: Union[str, Address], + mints: Union[Token, List[Token]], + amount: Optional[Union[Ada, Lovelace]] = None, + utxos: Optional[Union[UTxO, List[UTxO]]] = None, + other_signers: Optional[Union["Wallet", List["Wallet"]]] = None, + change_address: Optional[Union["Wallet", Address, str]] = None, + message: Optional[Union[str, List[str]]] = None, + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, ): """Under construction.""" @@ -1286,32 +1419,36 @@ def mint_tokens( return str(signed_tx.id) def transact( - self, - inputs: Union[ - "Wallet", - Address, - UTxO, - str, - List["Wallet"], - List[Address], - List[UTxO], - List[str], - ], - outputs: Union[Output, List[Output]], - mints: Optional[Union[Token, List[Token]]] = None, - signers: Optional[Union["Wallet", List["Wallet"], SigningKey, List[SigningKey]]] = None, - stake_registration: Optional[ - Union[bool, "Wallet", Address, str, List[Address], List["Wallet"], List[str]] - ] = None, - delegations: Optional[Union[str, dict, PoolKeyHash]] = None, - withdrawals: Optional[Union[bool, dict]] = None, - change_address: Optional[Union["Wallet", Address, str]] = None, - merge_change: Optional[bool] = True, - message: Optional[Union[str, List[str]]] = None, - other_metadata=None, - submit: Optional[bool] = True, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + self, + inputs: Union[ + "Wallet", + Address, + UTxO, + str, + List["Wallet"], + List[Address], + List[UTxO], + List[str], + ], + outputs: Union[Output, List[Output]], + mints: Optional[Union[Token, List[Token]]] = None, + signers: Optional[ + Union["Wallet", List["Wallet"], SigningKey, List[SigningKey]] + ] = None, + stake_registration: Optional[ + Union[ + bool, "Wallet", Address, str, List[Address], List["Wallet"], List[str] + ] + ] = None, + delegations: Optional[Union[str, dict, PoolKeyHash]] = None, + withdrawals: Optional[Union[bool, dict]] = None, + change_address: Optional[Union["Wallet", Address, str]] = None, + merge_change: Optional[bool] = True, + message: Optional[Union[str, List[str]]] = None, + other_metadata=None, + submit: Optional[bool] = True, + await_confirmation: Optional[bool] = False, + context: Optional[ChainContext] = None, ): # streamline inputs @@ -1328,7 +1465,11 @@ def transact( elif not mints: mints = [] - if stake_registration and not isinstance(stake_registration, list) and not isinstance(stake_registration, bool): + if ( + stake_registration + and not isinstance(stake_registration, list) + and not isinstance(stake_registration, bool) + ): stake_registration = [stake_registration] elif not stake_registration: stake_registration = [] @@ -1441,12 +1582,16 @@ def transact( if isinstance(delegations, str): # register current wallet pool_hash = PoolKeyHash(bytes.fromhex(delegations)) stake_credential = StakeCredential(self.stake_verification_key.hash()) - certificates.append(StakeDelegation(stake_credential, pool_keyhash=pool_hash)) + certificates.append( + StakeDelegation(stake_credential, pool_keyhash=pool_hash) + ) if self.stake_signing_key not in signers: signers.append(self.stake_signing_key) - elif isinstance(delegations, PoolKeyHash): # register current wallet + elif isinstance(delegations, PoolKeyHash): # register current wallet stake_credential = StakeCredential(self.stake_verification_key.hash()) - certificates.append(StakeDelegation(stake_credential, pool_keyhash=delegations)) + certificates.append( + StakeDelegation(stake_credential, pool_keyhash=delegations) + ) else: for key, value in delegations: # get stake hash from key @@ -1464,12 +1609,16 @@ def transact( pool_hash = value stake_credential = StakeCredential(stake_hash) - certificates.append(StakeDelegation(stake_credential, pool_keyhash=pool_hash)) + certificates.append( + StakeDelegation(stake_credential, pool_keyhash=pool_hash) + ) # withdrawals withdraw = {} if isinstance(withdrawals, bool): # withdraw current wallet - withdraw[self.stake_address.to_primitive()] = self.withdrawable_amount.lovelace + withdraw[ + self.stake_address.to_primitive() + ] = self.withdrawable_amount.lovelace if self.stake_signing_key not in signers: signers.append(self.stake_signing_key) elif isinstance(withdrawals, dict): @@ -1486,15 +1635,20 @@ def transact( elif isinstance(value, bool) or value == "all": # withdraw all account_info = get_stake_info(stake_address, self.context) if account_info.get("withdrawable_amount"): - withdrawal_amount = Lovelace(int(account_info.get("withdrawable_amount"))) + withdrawal_amount = Lovelace( + int(account_info.get("withdrawable_amount")) + ) else: - logger.warn(f"Stake address {stake_address} is not registered yet.") + logger.warn( + f"Stake address {stake_address} is not registered yet." + ) withdrawal_amount = Lovelace(0) else: withdrawal_amount = Lovelace(0) - withdraw[stake_address.staking_part.to_primitive()] = withdrawal_amount.as_lovelace().amount - + withdraw[ + stake_address.staking_part.to_primitive() + ] = withdrawal_amount.as_lovelace().amount # build the transaction builder = TransactionBuilder(context) @@ -1653,7 +1807,7 @@ def get_stake_address(address: Union[str, Address]): def format_message(message: Union[str, List[str]]): if isinstance(message, str): - message = [message[i:i+64] for i in range(0, len(message), 64)] + message = [message[i : i + 64] for i in range(0, len(message), 64)] for line in message: if len(line) > 64: @@ -1754,7 +1908,7 @@ def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext): def wait_for_confirmation( - tx_id: Union[str, TransactionId], context: ChainContext, delay: Optional[int] = 10 + tx_id: Union[str, TransactionId], context: ChainContext, delay: Optional[int] = 10 ): if not isinstance(context, BlockFrostChainContext): logger.warn( From 997ed0f7b7dd79562ffa5a7b3b8907e356d77c8d Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Tue, 30 Aug 2022 20:02:46 +0200 Subject: [PATCH 039/130] Add all docstrings --- pycardano/wallet.py | 212 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 187 insertions(+), 25 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 81225084..df2bcbcc 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -536,12 +536,16 @@ def bytes_name(self): def policy_id(self): return self.policy.policy_id - def get_on_chain_metadata(self, context: ChainContext): + def get_on_chain_metadata(self, context: ChainContext) -> dict: """Get the on-chain metadata of the token. Args: context (ChainContext): A chain context is necessary to fetch the on-chain metadata. Only BlockFrost chain context is supported at the moment. + + Returns: + dict: The on-chain metadata of the token. + """ if not isinstance(context, BlockFrostChainContext): @@ -574,6 +578,7 @@ class Output: Should generally satisfy the minimum ADA requirement for any attached tokens. tokens (Optional[Union[Token, List[Token]]]): Token or list of Tokens to be sent to the address. Amount of each token to send should be defined in each Token object. + """ address: Union["Wallet", Address, str] @@ -893,6 +898,7 @@ def query_utxos(self, context: Optional[ChainContext] = None): Args: context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + """ context = self._find_context(context) @@ -982,7 +988,7 @@ def sign_data( message: str, mode: Literal["payment", "stake"] = "payment", attach_cose_key=False, - ): + ) -> Union[str, dict]: """Sign a message with the wallet's private payment or stake keys following CIP-8. NOTE: CIP-8 is not yet finalized so this could be changed in the future. @@ -991,6 +997,9 @@ def sign_data( mode (Optional[Literal["payment", "stake"]]): The keys to use for signing. Defaults to "payment". attach_cose_key (bool): Whether to attach the COSE key to the signature. Defaults to False. At the moment, Eternl currently does not attach the COSE key, while Nami does. + + Returns: + Union[str, dict]: The signature. If attach_cose_key is True, the signature is a dictionary with the signature and the COSE key. """ if mode == "payment": @@ -1020,7 +1029,7 @@ def send_ada( message: Optional[Union[str, List[str]]] = None, await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, - ): + ) -> str: """Create a simple transaction in which Ada is sent to a single recipient. @@ -1032,6 +1041,9 @@ def send_ada( message (Optional[Union[str, List[str]]]): Optional message to include in the transaction (CIP-20). await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + + Returns: + str: The transaction ID. """ context = self._find_context(context) @@ -1088,7 +1100,7 @@ def send_utxo( message: Optional[Union[str, List[str]]] = None, await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, - ): + ) -> str: """Send all of the contents (ADA and tokens) of specified UTxO(s) to a single recipient. Args: @@ -1097,6 +1109,9 @@ def send_utxo( message (Optional[Union[str, List[str]]]): Optional message to include in the transaction (CIP-20). await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + + Returns: + str: The transaction ID. """ # streamline inputs @@ -1141,7 +1156,7 @@ def empty_wallet( message: Optional[Union[str, List[str]]] = None, await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, - ): + ) -> str: """Send all of the contents (ADA and tokens) of the wallet to a single recipient. The current wallet will be left completely empty @@ -1151,6 +1166,9 @@ def empty_wallet( message (Optional[Union[str, List[str]]]): Optional message to include in the transaction (CIP-20). await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + + Returns: + str: The transaction ID. """ return self.send_utxo( @@ -1168,7 +1186,7 @@ def delegate( amount: Optional[Union[Ada, Lovelace]] = Lovelace(2000000), await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, - ): + ) -> str: """Delegate the current wallet to a pool. Args: @@ -1179,6 +1197,9 @@ def delegate( Defaults to 2 Ada. await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + + Returns: + str: The transaction ID. """ # streamline inputs @@ -1226,13 +1247,19 @@ def withdraw( output_amount: Optional[Union[Ada, Lovelace]] = Lovelace(1000000), await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, - ): + ) -> str: """Withdraw staking rewards. Args: withdrawal_amount (Optional[Union[Ada, Lovelace]]): The amount of Ada to withdraw. - Defaults to the entire rewards balance. - output_amount + If not set, defaults to the entire rewards balance. + output_amount (Optional[Union[Ada, Lovelace]]): Amount of ADA to attach to the withdrawal transaction. + Defaults to 1 ADA which is generally a safe minimum amount to use. + await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. + context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + + Returns: + str: The transaction ID. """ # streamline inputs @@ -1276,7 +1303,27 @@ def mint_tokens( await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, ): - """Under construction.""" + """Mints (or burns) tokens of a policy owned by a user wallet. To attach metadata, set it in Token class directly. + Burn tokens by setting Token class amount to a negative value. + + Args: + to (Union[str, Address]): The address to which to send the newly minted tokens. + mints (Union[Token, List[Token]]): The token(s) to mint/burn. Set metadata and quantity directly in the Token class. + amount (Optional[Union[Ada, Lovelace]]): The amount of Ada to attach to the transaction. + If not set, the minimum amount will be calculated automatically. + utxos (Optional[Union[UTxO, List[UTxO]]]): The UTxO(s) to use as inputs. + If not set, the wallet will be queried for the latest UTxO set. + other_signers (Optional[Union["Wallet", List["Wallet"]]]): The other signers to use for the transaction. + e.g. a separate wallet which can sign the token policy + change_address (Optional[Union["Wallet", Address, str]]): The address to send any change to. + If not set, defaults to the wallet's own address + message (Optional[Union[str, List[str]]]): The message to attach to the transaction. + await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. + context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. + + Returns: + str: The transaction ID. + """ # streamline inputs context = self._find_context(context) @@ -1449,7 +1496,35 @@ def transact( submit: Optional[bool] = True, await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, - ): + ) -> str: + """ + Construct fully manual transactions. + + Args: + inputs (Union[Wallet, Address, UTxO, str, List[Wallet], List[Address], List[UTxO], List[str]]): Inputs to the transaction. + If wallets or addresses are provided, they will be queried for UTxOs. + outputs (Union[Output, List[Output]]): Outputs of the transaction using the Output class. + Can specify any number of recipients, ADA amounts, and native tokens. + mints (Union[Token, List[Token]]): The token(s) to mint/burn. Set metadata and quantity here. + signers (Union[Wallet, List[Wallet], SigningKey, List[SigningKey]]): The wallets or keys with which to sign the transaction. + stake_registration (Union[bool, Wallet, Address, str, List[Address], List["Wallet"], List[str]]): Wallets or addresses to register. + delegations (Union[str, dict, PoolKeyHash]): The hash of the pool to delegate to. + To delegate to multiple wallets, provide a dict of the form {wallet/address: PoolHash}. + withdrawals (Union[bool, dict]): Set the rewards to withdraw. Set to True to withdraw all rewards from the current wallet. + To withdraw a specific amount from one or more wallets, provide a dict of {wallet/address: Amount}, where Amount is an amount of + Lovelace(), Ada(). Use True or "all" to withdraw all available rewards from each specified wallet. + change_address (Union["Wallet", Address, str]): The wallet or address to send change to. + merge_change (bool): Whether to merge change into any Output whose address matches the change_address. True by default. + message (Union[str, List[str]]): A message to include in the transaction. + other_metadata (dict): Any other metadata to include in the transaction. + submit (bool): Whether to submit the transaction to the network. Defaults to True. + If False, return the signed transaction CBOR. + await_confirmation (bool): Whether to wait for the transaction to be confirmed. Defaults to False. + context (ChainContext): The context to use for the transaction. Defaults to the default context. + + Returns: + str: The transaction ID if submit is True, otherwise the signed transaction CBOR. + """ # streamline inputs context = self._find_context(context) @@ -1615,7 +1690,7 @@ def transact( # withdrawals withdraw = {} - if isinstance(withdrawals, bool): # withdraw current wallet + if withdrawals and isinstance(withdrawals, bool): # withdraw current wallet withdraw[ self.stake_address.to_primitive() ] = self.withdrawable_amount.lovelace @@ -1747,8 +1822,18 @@ def transact( return str(signed_tx.id) -# helpers -def get_utxo_creator(utxo: UTxO, context: ChainContext): +# Utility and Helper functions +def get_utxo_creator(utxo: UTxO, context: ChainContext) -> Address: + """Fetch the creator of a UTxO. + If there are multiple input UTxOs, the creator is the first one. + + Args: + utxo (UTxO): The UTxO to get the creator of. + context (ChainContext): The context to use for the query. For now must be BlockFrost. + + Returns: + Address: The creator of the UTxO. + """ if isinstance(context, BlockFrostChainContext): utxo_creator = ( context.api.transaction_utxos(str(utxo.input.transaction_id)) @@ -1764,7 +1849,17 @@ def get_utxo_creator(utxo: UTxO, context: ChainContext): ) -def get_utxo_block_time(utxo: UTxO, context: ChainContext): +def get_utxo_block_time(utxo: UTxO, context: ChainContext) -> int: + """Get the block time at which a UTxO was created. + + Args: + utxo (UTxO): The UTxO to get the block time of. + context (ChainContext): The context to use for the query. For now must be BlockFrost. + + Returns: + int: The block time at which the UTxO was created. + + """ if isinstance(context, BlockFrostChainContext): block_time = context.api.transaction(str(utxo.input.transaction_id)).block_time @@ -1776,7 +1871,17 @@ def get_utxo_block_time(utxo: UTxO, context: ChainContext): ) -def get_stake_info(stake_address: Union[str, Address], context: ChainContext): +def get_stake_info(stake_address: Union[str, Address], context: ChainContext) -> dict: + """Get the stake info of a stake address from Blockfrost. + For more info see: https://docs.blockfrost.io/#tag/Cardano-Accounts/paths/~1accounts~1{stake_address}/get + + Args: + stake_address (Union[str, Address]): The stake address to get the stake info of. + context (ChainContext): The context to use for the query. For now must be BlockFrost. + + Returns: + dict: Info regarding the given stake address. + """ if isinstance(context, BlockFrostChainContext): if isinstance(stake_address, str): @@ -1796,7 +1901,15 @@ def get_stake_info(stake_address: Union[str, Address], context: ChainContext): ) -def get_stake_address(address: Union[str, Address]): +def get_stake_address(address: Union[str, Address]) -> Address: + """Get the stake address of any given address. + + Args: + address (Union[str, Address]): The address to get the stake address of. + + Returns: + Address: The stake address of the given address. + """ if isinstance(address, str): address = Address.from_primitive(address) @@ -1805,7 +1918,16 @@ def get_stake_address(address: Union[str, Address]): ) -def format_message(message: Union[str, List[str]]): +def format_message(message: Union[str, List[str]]) -> dict: + """Format a metadata message according to CIP-20 + + Args: + message (Union[str, List[str]]): The message to format. + + Returns: + dict: The message formatted properly to attach to a transaction. + """ + if isinstance(message, str): message = [message[i : i + 64] for i in range(0, len(message), 64)] @@ -1824,10 +1946,15 @@ def format_message(message: Union[str, List[str]]): def check_metadata(to_check: Union[dict, list, str], top_level: bool = False): - """Screen the input metadata for potential issues. + """Screen any given input metadata for potential issues. + + Used recursively to check inside all dicts and lists of the metadata. - Use top_level=True only for the full metadata dictionary in order to check that - it is JSON serializable. + + Args: + to_check (Union[dict, list, str]): The metadata to check. + top_level (bool): Whether this is the top level of the metadata. + Set to True only for the full metadata dictionary in order to check that it is JSON serializable. """ if isinstance(to_check, dict): @@ -1871,7 +1998,15 @@ def check_metadata(to_check: Union[dict, list, str], top_level: bool = False): raise MetadataFormattingException(f"Cannot format metadata: {e}") -def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")): +def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")) -> List[str]: + """List all PyCardano wallets in the directory specified by wallet_path. + + Args: + wallet_path (Union[str, Path]): The path to the directory to list the wallets in. + + Returns: + List[str]: A list of all names of wallets in the directory. + """ if isinstance(wallet_path, str): wallet_path = Path(wallet_path) @@ -1880,7 +2015,15 @@ def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")): return wallets -def get_all_policies(policy_path: Union[str, Path] = Path("./priv/policies")): +def get_all_policies(policy_path: Union[str, Path] = Path("./priv/policies")) -> List[TokenPolicy]: + """List all policies in the directory specified by policy_path. + + Args: + policy_path (Union[str, Path]): The path to the directory to list the policies in. + + Returns: + List[TokenPolicy]: A list of all policies in the directory. + """ if isinstance(policy_path, str): policy_path = Path(policy_path) @@ -1889,7 +2032,16 @@ def get_all_policies(policy_path: Union[str, Path] = Path("./priv/policies")): return policies -def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext): +def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext) -> bool: + """Confirm that a transaction has been included in a block. + + Args: + tx_id (Union[str, TransactionId]): The transaction id to check. + context (ChainContext): The context to use for the query. For now must be BlockFrost. + + Returns: + bool: Whether the transaction has been included in a block. + """ if isinstance(context, BlockFrostChainContext): try: @@ -1909,7 +2061,17 @@ def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext): def wait_for_confirmation( tx_id: Union[str, TransactionId], context: ChainContext, delay: Optional[int] = 10 -): +) -> bool: + """Wait for a transaction to be confirmed, checking every `delay` seconds. + + Args: + tx_id (Union[str, TransactionId]): The transaction id to check. + context (ChainContext): The context to use for the query. For now must be BlockFrost. + delay (Optional[int]): The number of seconds to wait between checking the context. Defaults to 10. + + Returns: + bool: Whether the transaction has been confirmed. + """ if not isinstance(context, BlockFrostChainContext): logger.warn( "Confirming transactions is is only possible with Blockfrost Chain Context." From 8bc04523767169d48489e8f13c3c0cd9f94e690d Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Tue, 30 Aug 2022 20:09:15 +0200 Subject: [PATCH 040/130] Update todo list --- pycardano/wallet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index df2bcbcc..0534b018 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -633,6 +633,8 @@ class Wallet: - Generate fully manual transactions that can do any / all of the above Future additions (TODO list): + - Add new preview and devnet functionalities + - Add tests and examples - Create and sign multi-sig transactions - Interaction with native scripts - Interaction with plutus scripts From 15f341db9d2e7c9cbc57023ace0cde92dbccdfd7 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 25 Sep 2022 00:23:57 +0200 Subject: [PATCH 041/130] Fix typo in Token docs --- pycardano/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 0534b018..6f97fcee 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -452,7 +452,7 @@ class Token: """Class that represents a token with an attached policy and amount. Attributes: - policy (Union[str, NativeScript]): The policy of the token. + policy (Union[NativeScript, TokenPolicy]): The policy of the token. The policy need not necessarily be owned by the user. amount (int): The amount of tokens. name (str): The name of the token. Either the name or the hex name should be provided. From a219f4917afdc7570dd5f003b387e4360c754ae0 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 25 Sep 2022 18:40:15 +0200 Subject: [PATCH 042/130] Fix typo is Ada.as_ada() --- pycardano/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 6f97fcee..1ce01797 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -264,7 +264,7 @@ def as_lovelace(self): """Converts the Ada to a Lovelace class.""" return Lovelace(self.lovelace) - def ad_ada(self): + def as_ada(self): """Returns a copy.""" return Ada(self.ada) From d85d34d52be994f601b1e935818a42437f105624 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 25 Sep 2022 19:15:25 +0200 Subject: [PATCH 043/130] Add first tests for Wallet class --- test/pycardano/test_wallet.py | 93 +++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 test/pycardano/test_wallet.py diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py new file mode 100644 index 00000000..a01765c8 --- /dev/null +++ b/test/pycardano/test_wallet.py @@ -0,0 +1,93 @@ +import pathlib + +from pycardano.wallet import Ada, Lovelace, Wallet, TokenPolicy + +import pytest + + +def test_amount(): + """Check that the Ada / Lovelace math works as expected.""" + + assert Ada(1).as_lovelace() == Lovelace(1000000) + assert Lovelace(1).as_ada() == Ada(0.000001) + assert Ada(1).as_ada() == Ada(1) + assert Lovelace(1).as_lovelace() == Lovelace(1) + assert Ada(1) + Ada(1) == Ada(2) + assert Ada(1) - Ada(1) == Ada(0) + assert Lovelace(1) + Lovelace(1) == Lovelace(2) + assert Lovelace(1) - Lovelace(1) == Lovelace(0) + assert Lovelace(1) + Ada(1) == Lovelace(1000001) + assert Lovelace(1000001) - Ada(1) == Lovelace(1) + assert Ada(1) + Lovelace(1) == Ada(1.000001) + assert Ada(1) - Lovelace(1) == Ada(0.999999) + assert Ada(1) == Ada(1) + assert Lovelace(1) == Lovelace(1) + assert Ada(1) != Ada(2) + assert Lovelace(1) != Lovelace(2) + assert Ada(1) < Ada(2) + assert Lovelace(1) < Lovelace(2) + assert Ada(2) > Ada(1) + assert Lovelace(2) > Lovelace(1) + assert Ada(1) <= Ada(1) + assert Lovelace(1) <= Lovelace(1) + assert Ada(1) >= Ada(1) + assert Lovelace(1) >= Lovelace(1) + assert Ada(1) <= Ada(2) + assert Lovelace(1) <= Lovelace(2) + assert Ada(2) >= Ada(1) + assert Lovelace(2) >= Lovelace(1) + assert Ada(1) * 2 == Ada(2) + assert Lovelace(1) * 2 == Lovelace(2) + assert Ada(1) / 2 == Ada(0.5) + assert Ada(1) / 2 == Lovelace(500000) + assert str(Ada(1)) == "1" + assert str(Lovelace(1)) == "1" + assert bool(Ada(1)) == True + assert bool(Lovelace(1)) == True + assert bool(Ada(0)) == False + assert bool(Lovelace(0)) == False + assert sum([Ada(3), Ada(5), Ada(7)]) == Ada(15) + assert sum([Lovelace(500000), Ada(5)]) == Lovelace(5500000) + assert abs(Ada(-1)) == Ada(1) + assert abs(Lovelace(-1)) == Lovelace(1) + + +def test_lovelace_integer(): + """Check that the Lovelace class only accepts integer values.""" + + with pytest.raises(TypeError): + Lovelace(5.5) + + +def test_wallet(): + + wallet = Wallet( + name="payment", + keys_dir=str(pathlib.Path(__file__).parent / "../resources/keys"), + ) + + assert ( + str(wallet.address) + == "addr1q8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3uk2g9z3d4kaf0j5l6rxunxt43x28pssehhqds2x05mwld45s399sr7" + ) + + assert ( + wallet.sign_data("pycardano rocks", mode="payment") + == ("84584da301276761646472657373581d61cc30497f4ff962f4c1dca54cceefe39f86f1" + "d7179668009f8eb71e590458205797dc2cc919dfec0bb849551ebdf30d96e5cbe0f33f" + "734a87fe826db30f7ef9a166686173686564f44f707963617264616e6f20726f636b73" + "58402beecd6dba2f7f73d0d72abd5cc43829173a069afa2a29eff72d65049b092bc80c" + "571569e8a7c26354cd1d38b5fcdc3d7a3b6955d2211106824ba02c33ba220f" + ) + ) + + +def test_token_policy(): + + wallet = Wallet(name="payment") + + + +def split_string_into_n_chunks(string, n): + """Split a string into n chunks.""" + return [string[i:i + n] for i in range(0, len(string), n)] From 78cce6ee58b9a1dcdd625e762de287bcd4f198aa Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 25 Sep 2022 19:15:43 +0200 Subject: [PATCH 044/130] Catch non-integer lovelace values --- pycardano/wallet.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 1ce01797..d7ae3897 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -237,6 +237,10 @@ class Lovelace(Amount): """Stores an amount of Lovelace and automatically handles most currency math.""" def __init__(self, amount: int = 0): + + if not isinstance(amount, int): + raise TypeError("Lovelace must be an integer.") + super().__init__(amount, "lovelace") def __repr__(self): From 421462591b1588a96b1edf7890d8c896522cc2a7 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Tue, 27 Sep 2022 23:09:06 +0200 Subject: [PATCH 045/130] Add preview and preprod net functionality --- pycardano/wallet.py | 87 ++++++++++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index d7ae3897..4e08aa30 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -237,10 +237,10 @@ class Lovelace(Amount): """Stores an amount of Lovelace and automatically handles most currency math.""" def __init__(self, amount: int = 0): - + if not isinstance(amount, int): raise TypeError("Lovelace must be an integer.") - + super().__init__(amount, "lovelace") def __repr__(self): @@ -546,7 +546,7 @@ def get_on_chain_metadata(self, context: ChainContext) -> dict: Args: context (ChainContext): A chain context is necessary to fetch the on-chain metadata. Only BlockFrost chain context is supported at the moment. - + Returns: dict: The on-chain metadata of the token. @@ -582,7 +582,7 @@ class Output: Should generally satisfy the minimum ADA requirement for any attached tokens. tokens (Optional[Union[Token, List[Token]]]): Token or list of Tokens to be sent to the address. Amount of each token to send should be defined in each Token object. - + """ address: Union["Wallet", Address, str] @@ -591,6 +591,15 @@ class Output: def __post_init__(self): + if ( + not isinstance(self.amount, Ada) + and not isinstance(self.amount, Lovelace) + and not isinstance(self.amount, int) + ): + raise TypeError( + "Please provide amount as either `Ada(amount)` or `Lovelace(amount)`. Otherwise provide lovelace as an integer." + ) + if isinstance(self.amount, int): self.amount = Lovelace(self.amount) @@ -609,10 +618,9 @@ def __post_init__(self): class Wallet: """Create or load a wallet for which you own the keys or will later create them. NOTE: BlockFrost Chain Context can be automatically loaded by setting the following environment variables: - `BLOCKFROST_ID_0` for testnet - `BLOCKFROST_ID_1` for mainnet - `BLOCKFROST_ID_2` for preview - `BLOCKFROST_ID_3` for devnet + `BLOCKFROST_ID_MAINNET` for mainnet + `BLOCKFROST_ID_PREPROD` for preprod + `BLOCKFROST_ID_PREVIEW` for preview Otherwise, a custom Chain Context can be provided when necessary. Currently you can already: @@ -652,7 +660,7 @@ class Wallet: keys_dir (Optional[Union[str, Path]]): Directory in which to save the keys. Defaults to "./priv". use_stake (Optional[bool]): Whether to use a stake address for this wallet. Defaults to True. network (Optional[str, Network]): The network to use for the wallet. - Can pick from "mainnet", "testnet", "preview", "preprod". Defaults to "mainnet". + Can pick from "mainnet", "preview", "preprod". Defaults to "mainnet". BlockFrost Chain Context will be automatically loaded for this network if the API key is set in the environment variables. @@ -662,11 +670,7 @@ class Wallet: address: Optional[Union[Address, str]] = None keys_dir: Optional[Union[str, Path]] = field(repr=False, default=Path("./priv")) use_stake: Optional[bool] = field(repr=False, default=True) - network: Optional[ - Literal[ - "mainnet", "testnet", "preview", "preprod", Network.MAINNET, Network.TESTNET - ] - ] = "mainnet" + network: Optional[Literal["mainnet", "preview", "preprod"]] = "mainnet" # generally added later upon initialization lovelace: Optional[Lovelace] = field(repr=False, default=Lovelace(0)) @@ -685,13 +689,26 @@ def __post_init__(self): if isinstance(self.keys_dir, str): self.keys_dir = Path(self.keys_dir) + if self.network == "preprod": + self._network = Network.TESTNET + elif self.network == "preview": + self._network = Network.TESTNET + else: + self._network = Network.MAINNET + # if not address was provided, get keys if not self.address: self._load_or_create_key_pair(stake=self.use_stake) # otherwise derive the network from the address provided else: - # noinspection PyTypeChecker - self.network = self.address.network.name.lower() + # check that the given address matches the desired network + if self._network: + if self.address.network != self._network: + raise ValueError( + f"{self._network} does not match the network of the provided address." + ) + + self._network = self.address.network self.signing_key = None self.verification_key = None self.stake_signing_key = None @@ -699,18 +716,22 @@ def __post_init__(self): # try to automatically create blockfrost context if not self.context: - if self.network.lower() == "mainnet": - if getenv("BLOCKFROST_ID"): - self.context = BlockFrostChainContext( - getenv("BLOCKFROST_ID"), network=Network.MAINNET - ) - elif getenv("BLOCKFROST_ID_TESTNET"): + if self.network.lower() == "mainnet" and getenv("BLOCKFROST_ID_MAINNET"): self.context = BlockFrostChainContext( - getenv("BLOCKFROST_ID_TESTNET"), network=Network.TESTNET + getenv("BLOCKFROST_ID_MAINNET"), network=Network.MAINNET + ) + elif self.network.lower() == "preprod" and getenv("BLOCKFROST_ID_PREPROD"): + self.context = BlockFrostChainContext( + getenv("BLOCKFROST_ID_PREPROD"), + network=Network.TESTNET, + base_url="https://cardano-preprod.blockfrost.io/api", + ) + elif self.network.lower() == "preview" and getenv("BLOCKFROST_ID_PREVIEW"): + self.context = BlockFrostChainContext( + getenv("BLOCKFROST_ID_PREVIEW"), + network=Network.TESTNET, + base_url="https://cardano-preview.blockfrost.io/api", ) - - # if self.context: - # self.query_utxos() logger.info(self.__repr__()) @@ -718,7 +739,7 @@ def __post_init__(self): def payment_address(self): return Address( - payment_part=self.address.payment_part, network=self.address.network + payment_part=self.address.payment_part, network=self._network ) @property @@ -726,7 +747,7 @@ def stake_address(self): if self.stake_signing_key or self.address.staking_part: return Address( - staking_part=self.address.staking_part, network=self.address.network + staking_part=self.address.staking_part, network=self._network ) else: return None @@ -828,10 +849,10 @@ def _load_or_create_key_pair(self, stake=True): if stake: self.address = Address( - vkey.hash(), stake_vkey.hash(), network=Network[self.network.upper()] + vkey.hash(), stake_vkey.hash(), network=self._network ) else: - self.address = Address(vkey.hash(), network=Network[self.network.upper()]) + self.address = Address(vkey.hash(), network=self._network) def _find_context(self, context: Optional[ChainContext] = None): """Helper function to ensure that a context is always provided when needed. @@ -904,7 +925,7 @@ def query_utxos(self, context: Optional[ChainContext] = None): Args: context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. - + """ context = self._find_context(context) @@ -1003,7 +1024,7 @@ def sign_data( mode (Optional[Literal["payment", "stake"]]): The keys to use for signing. Defaults to "payment". attach_cose_key (bool): Whether to attach the COSE key to the signature. Defaults to False. At the moment, Eternl currently does not attach the COSE key, while Nami does. - + Returns: Union[str, dict]: The signature. If attach_cose_key is True, the signature is a dictionary with the signature and the COSE key. """ @@ -1024,7 +1045,7 @@ def sign_data( message, signing_key, attach_cose_key=attach_cose_key, - network=self.address.network, + network=self._network, ) def send_ada( From 37e0a80fba99de0d244c896f5eb8a84c334cb888 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 00:54:48 +0200 Subject: [PATCH 046/130] Update first methods to use transact directly --- pycardano/wallet.py | 189 ++++++++++---------------------------------- 1 file changed, 43 insertions(+), 146 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 4e08aa30..1949a0d9 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1053,9 +1053,7 @@ def send_ada( to: Union[str, Address], amount: Union[Ada, Lovelace], utxos: Optional[Union[UTxO, List[UTxO]]] = None, - message: Optional[Union[str, List[str]]] = None, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + **kwargs, ) -> str: """Create a simple transaction in which Ada is sent to a single recipient. @@ -1068,121 +1066,39 @@ def send_ada( message (Optional[Union[str, List[str]]]): Optional message to include in the transaction (CIP-20). await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. - + Returns: str: The transaction ID. """ - context = self._find_context(context) - - # streamline inputs - if isinstance(to, str): - to = Address.from_primitive(to) - - if not isinstance(amount, Ada) and not isinstance(amount, Lovelace): - raise TypeError( - "Please provide amount as either `Ada(amount)` or `Lovelace(amount)`." - ) - + # streamline inputs, use either specific utxos or all wallet utxos if utxos: if isinstance(utxos, UTxO): - utxos = [utxos] + inputs = [utxos] else: - utxos = [] + inputs = [self] - builder = TransactionBuilder(context) - - builder.add_input_address(self.address) - - if utxos: - for utxo in utxos: - builder.add_input(utxo) - - builder.add_output( - TransactionOutput(to, Value.from_primitive([amount.as_lovelace().amount])) - ) - - if message: - metadata = {674: format_message(message)} - builder.auxiliary_data = AuxiliaryData( - AlonzoMetadata(metadata=Metadata(metadata)) - ) - - signed_tx = builder.build_and_sign( - [self.signing_key], change_address=self.address - ) - - context.submit_tx(signed_tx.to_cbor()) - - if await_confirmation: - confirmed = wait_for_confirmation(str(signed_tx.id), self.context) - self.query_utxos() - - return str(signed_tx.id) + return self.transact(inputs=inputs, outputs=[Output(to, amount)], **kwargs) def send_utxo( - self, - to: Union[str, Address], - utxos: Union[UTxO, List[UTxO]], - message: Optional[Union[str, List[str]]] = None, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + self, to: Union[str, Address], utxos: Union[UTxO, List[UTxO]], **kwargs ) -> str: """Send all of the contents (ADA and tokens) of specified UTxO(s) to a single recipient. Args: to (Union[str, Address]): The address to which to send the UTxO contents. utxos (Optional[Union[UTxO, List[UTxO]]]): The UTxO(s) to use for the transaction. - message (Optional[Union[str, List[str]]]): Optional message to include in the transaction (CIP-20). - await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. - context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. - + Returns: str: The transaction ID. """ - # streamline inputs - context = self._find_context(context) - - if isinstance(to, str): - to = Address.from_primitive(to) - - if isinstance(utxos, UTxO): - utxos = [utxos] - - builder = TransactionBuilder(context) - - builder.add_input_address(self.address) - - for utxo in utxos: - builder.add_input(utxo) - - if message: - metadata = {674: format_message(message)} - builder.auxiliary_data = AuxiliaryData( - AlonzoMetadata(metadata=Metadata(metadata)) - ) - - signed_tx = builder.build_and_sign( - [self.signing_key], - change_address=to, - merge_change=True, - ) - - context.submit_tx(signed_tx.to_cbor()) - - if await_confirmation: - confirmed = wait_for_confirmation(str(signed_tx.id), self.context) - self.query_utxos() - - return str(signed_tx.id) + return self.transact(inputs=utxos, change_address=to, **kwargs) def empty_wallet( self, to: Union[str, Address], - message: Optional[Union[str, List[str]]] = None, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + **kwargs, ) -> str: """Send all of the contents (ADA and tokens) of the wallet to a single recipient. @@ -1190,41 +1106,32 @@ def empty_wallet( Args: to (Union[str, Address]): The address to which to send the entire contents of the wallet. - message (Optional[Union[str, List[str]]]): Optional message to include in the transaction (CIP-20). - await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. - context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. - + Returns: str: The transaction ID. """ - return self.send_utxo( - to=to, - utxos=self.utxos, - message=message, - await_confirmation=await_confirmation, - context=context, - ) + return self.transact(inputs=self.utxos, change_address=to, **kwargs) def delegate( self, pool_hash: Union[PoolKeyHash, str], register: Optional[bool] = True, amount: Optional[Union[Ada, Lovelace]] = Lovelace(2000000), - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + utxos: Optional[Union[UTxO, List[UTxO]]] = None, + **kwargs ) -> str: """Delegate the current wallet to a pool. Args: pool_hash (Union[PoolKeyHash, str]): The hash of the pool to which to delegate. register (Optional[bool]): Whether to register the pool with the network. Defaults to True. - If True, this will skip registration is the wallet is already registered. + If True, this will skip registration if the wallet is already registered. amount (Optional[Union[Ada, Lovelace]]): The amount of Ada to attach to the delegation transaction. Defaults to 2 Ada. await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. - + Returns: str: The transaction ID. """ @@ -1232,41 +1139,29 @@ def delegate( # streamline inputs if not self.stake_address: raise ValueError("This wallet does not have staking keys.") - - context = self._find_context(context) - - if isinstance(pool_hash, str): - pool_hash = PoolKeyHash(bytes.fromhex(pool_hash)) - - # check registration - if register: - register = not self.stake_info.get("active") - - stake_credential = StakeCredential(self.stake_verification_key.hash()) - stake_registration = StakeRegistration(stake_credential) - stake_delegation = StakeDelegation(stake_credential, pool_keyhash=pool_hash) - - # draft the transaction - builder = TransactionBuilder(context) - builder.add_input_address(self.address) - builder.add_output(TransactionOutput(self.address, amount.lovelace)) - - if register: - builder.certificates = [stake_registration, stake_delegation] + + # streamline inputs, use either specific utxos or all wallet utxos + if utxos: + if isinstance(utxos, UTxO): + inputs = [utxos] else: - builder.certificates = [stake_delegation] - - signed_tx = builder.build_and_sign( - [self.signing_key, self.stake_signing_key], self.address + inputs = [self] + + # check registration, do not register if already registered + active = self.stake_info.get("active") + if register: + register = not active + elif not active: + raise ValueError("Cannot delegate to a pool. This wallet is not yet registered. Try again with register=True.") + + return self.transact( + inputs=inputs, + outputs=[Output(self, amount)], + stake_registration=register, + delegations=pool_hash, + **kwargs, ) - - context.submit_tx(signed_tx.to_cbor()) - - if await_confirmation: - confirmed = wait_for_confirmation(str(signed_tx.id), self.context) - self.query_utxos() - - return str(signed_tx.id) + def withdraw( self, @@ -1284,7 +1179,7 @@ def withdraw( Defaults to 1 ADA which is generally a safe minimum amount to use. await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. - + Returns: str: The transaction ID. """ @@ -1332,7 +1227,7 @@ def mint_tokens( ): """Mints (or burns) tokens of a policy owned by a user wallet. To attach metadata, set it in Token class directly. Burn tokens by setting Token class amount to a negative value. - + Args: to (Union[str, Address]): The address to which to send the newly minted tokens. mints (Union[Token, List[Token]]): The token(s) to mint/burn. Set metadata and quantity directly in the Token class. @@ -1347,7 +1242,7 @@ def mint_tokens( message (Optional[Union[str, List[str]]]): The message to attach to the transaction. await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. - + Returns: str: The transaction ID. """ @@ -1371,7 +1266,9 @@ def mint_tokens( elif not utxos: utxos = [] - if not isinstance(other_signers, list): + if other_signers is None: + other_signers = [] + elif not isinstance(other_signers, list): other_signers = [other_signers] elif not other_signers: other_signers = [] From 0fc796d62ecf958cd45f62a637c18a290743ba26 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 00:55:20 +0200 Subject: [PATCH 047/130] Make outputs and optional param for transact --- pycardano/wallet.py | 113 +++++++++++++++++++++++--------------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 1949a0d9..6e149357 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1401,7 +1401,7 @@ def transact( List[UTxO], List[str], ], - outputs: Union[Output, List[Output]], + outputs: Optional[Union[Output, List[Output]]] = None, mints: Optional[Union[Token, List[Token]]] = None, signers: Optional[ Union["Wallet", List["Wallet"], SigningKey, List[SigningKey]] @@ -1423,7 +1423,7 @@ def transact( ) -> str: """ Construct fully manual transactions. - + Args: inputs (Union[Wallet, Address, UTxO, str, List[Wallet], List[Address], List[UTxO], List[str]]): Inputs to the transaction. If wallets or addresses are provided, they will be queried for UTxOs. @@ -1445,7 +1445,7 @@ def transact( If False, return the signed transaction CBOR. await_confirmation (bool): Whether to wait for the transaction to be confirmed. Defaults to False. context (ChainContext): The context to use for the transaction. Defaults to the default context. - + Returns: str: The transaction ID if submit is True, otherwise the signed transaction CBOR. """ @@ -1456,7 +1456,7 @@ def transact( if not isinstance(inputs, list): inputs = [inputs] - if not isinstance(outputs, list): + if outputs and not isinstance(outputs, list): outputs = [outputs] if mints and not isinstance(mints, list): @@ -1674,39 +1674,40 @@ def transact( builder.auxiliary_data = auxiliary_data # format tokens and lovelace of outputs - for output in outputs: - multi_asset = {} - if output.tokens: - multi_asset = MultiAsset() - output_policies = {} - for token in output.tokens: - if not output_policies.get(token.policy_id): - output_policies[token.policy_id] = {} - - if output_policies[token.policy_id].get(token.name): - output_policies[token.policy_id][token.name] += token.amount - else: - output_policies[token.policy_id][token.name] = token.amount + if outputs: + for output in outputs: + multi_asset = {} + if output.tokens: + multi_asset = MultiAsset() + output_policies = {} + for token in output.tokens: + if not output_policies.get(token.policy_id): + output_policies[token.policy_id] = {} - for policy, token_info in output_policies.items(): + if output_policies[token.policy_id].get(token.name): + output_policies[token.policy_id][token.name] += token.amount + else: + output_policies[token.policy_id][token.name] = token.amount - asset = Asset() + for policy, token_info in output_policies.items(): - for token_name, token_amount in token_info.items(): - asset[AssetName(str.encode(token_name))] = token_amount + asset = Asset() - multi_asset[ScriptHash.from_primitive(policy)] = asset + for token_name, token_amount in token_info.items(): + asset[AssetName(str.encode(token_name))] = token_amount - if not output.amount.lovelace: # Calculate min lovelace if necessary - output.amount = Lovelace( - min_lovelace(Value(0, mint_multiasset), context) - ) + multi_asset[ScriptHash.from_primitive(policy)] = asset - builder.add_output( - TransactionOutput( - output.address, Value(output.amount.lovelace, multi_asset) + if not output.amount.lovelace: # Calculate min lovelace if necessary + output.amount = Lovelace( + min_lovelace(Value(0, mint_multiasset), context) + ) + + builder.add_output( + TransactionOutput( + output.address, Value(output.amount.lovelace, multi_asset) + ) ) - ) # add registration + delegation certificates if certificates: @@ -1750,11 +1751,11 @@ def transact( def get_utxo_creator(utxo: UTxO, context: ChainContext) -> Address: """Fetch the creator of a UTxO. If there are multiple input UTxOs, the creator is the first one. - - Args: + + Args: utxo (UTxO): The UTxO to get the creator of. context (ChainContext): The context to use for the query. For now must be BlockFrost. - + Returns: Address: The creator of the UTxO. """ @@ -1775,14 +1776,14 @@ def get_utxo_creator(utxo: UTxO, context: ChainContext) -> Address: def get_utxo_block_time(utxo: UTxO, context: ChainContext) -> int: """Get the block time at which a UTxO was created. - + Args: utxo (UTxO): The UTxO to get the block time of. context (ChainContext): The context to use for the query. For now must be BlockFrost. - + Returns: int: The block time at which the UTxO was created. - + """ if isinstance(context, BlockFrostChainContext): block_time = context.api.transaction(str(utxo.input.transaction_id)).block_time @@ -1798,11 +1799,11 @@ def get_utxo_block_time(utxo: UTxO, context: ChainContext) -> int: def get_stake_info(stake_address: Union[str, Address], context: ChainContext) -> dict: """Get the stake info of a stake address from Blockfrost. For more info see: https://docs.blockfrost.io/#tag/Cardano-Accounts/paths/~1accounts~1{stake_address}/get - - Args: + + Args: stake_address (Union[str, Address]): The stake address to get the stake info of. context (ChainContext): The context to use for the query. For now must be BlockFrost. - + Returns: dict: Info regarding the given stake address. """ @@ -1827,10 +1828,10 @@ def get_stake_info(stake_address: Union[str, Address], context: ChainContext) -> def get_stake_address(address: Union[str, Address]) -> Address: """Get the stake address of any given address. - + Args: address (Union[str, Address]): The address to get the stake address of. - + Returns: Address: The stake address of the given address. """ @@ -1844,10 +1845,10 @@ def get_stake_address(address: Union[str, Address]) -> Address: def format_message(message: Union[str, List[str]]) -> dict: """Format a metadata message according to CIP-20 - + Args: message (Union[str, List[str]]): The message to format. - + Returns: dict: The message formatted properly to attach to a transaction. """ @@ -1871,10 +1872,10 @@ def format_message(message: Union[str, List[str]]) -> dict: def check_metadata(to_check: Union[dict, list, str], top_level: bool = False): """Screen any given input metadata for potential issues. - - + + Used recursively to check inside all dicts and lists of the metadata. - + Args: to_check (Union[dict, list, str]): The metadata to check. top_level (bool): Whether this is the top level of the metadata. @@ -1924,10 +1925,10 @@ def check_metadata(to_check: Union[dict, list, str], top_level: bool = False): def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")) -> List[str]: """List all PyCardano wallets in the directory specified by wallet_path. - + Args: wallet_path (Union[str, Path]): The path to the directory to list the wallets in. - + Returns: List[str]: A list of all names of wallets in the directory. """ @@ -1939,12 +1940,14 @@ def list_all_wallets(wallet_path: Union[str, Path] = Path("./priv")) -> List[str return wallets -def get_all_policies(policy_path: Union[str, Path] = Path("./priv/policies")) -> List[TokenPolicy]: +def get_all_policies( + policy_path: Union[str, Path] = Path("./priv/policies") +) -> List[TokenPolicy]: """List all policies in the directory specified by policy_path. - + Args: policy_path (Union[str, Path]): The path to the directory to list the policies in. - + Returns: List[TokenPolicy]: A list of all policies in the directory. """ @@ -1958,11 +1961,11 @@ def get_all_policies(policy_path: Union[str, Path] = Path("./priv/policies")) -> def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext) -> bool: """Confirm that a transaction has been included in a block. - + Args: tx_id (Union[str, TransactionId]): The transaction id to check. context (ChainContext): The context to use for the query. For now must be BlockFrost. - + Returns: bool: Whether the transaction has been included in a block. """ @@ -1987,12 +1990,12 @@ def wait_for_confirmation( tx_id: Union[str, TransactionId], context: ChainContext, delay: Optional[int] = 10 ) -> bool: """Wait for a transaction to be confirmed, checking every `delay` seconds. - + Args: tx_id (Union[str, TransactionId]): The transaction id to check. context (ChainContext): The context to use for the query. For now must be BlockFrost. delay (Optional[int]): The number of seconds to wait between checking the context. Defaults to 10. - + Returns: bool: Whether the transaction has been confirmed. """ From eacc6ee364db1c3618c63f26c6abb756593e4730 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 01:02:54 +0200 Subject: [PATCH 048/130] Update withdraw method to use transact --- pycardano/wallet.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 6e149357..4f080c2b 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1167,8 +1167,7 @@ def withdraw( self, withdrawal_amount: Optional[Union[Ada, Lovelace]] = None, output_amount: Optional[Union[Ada, Lovelace]] = Lovelace(1000000), - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + **kwargs, ) -> str: """Withdraw staking rewards. @@ -1188,31 +1187,20 @@ def withdraw( if not self.stake_address: raise ValueError("This wallet does not have staking keys.") - context = self._find_context(context) - if not withdrawal_amount: # automatically detect rewards: withdrawal_amount = self.withdrawable_amount + + if not withdrawal_amount: + raise ValueError("No rewards to withdraw.") - builder = TransactionBuilder(context) - builder.add_input_address(self.address) - builder.add_output(TransactionOutput(self.address, output_amount.lovelace)) - builder.withdrawals = Withdrawals( - {self.stake_address.to_primitive(): withdrawal_amount.lovelace} - ) - - signed_tx = builder.build_and_sign( - [self.signing_key, self.stake_signing_key], self.address + return self.transact( + inputs=[self], + outputs=[Output(self, output_amount)], + withdrawals={self: withdrawal_amount}, + **kwargs, ) - context.submit_tx(signed_tx.to_cbor()) - - if await_confirmation: - confirmed = wait_for_confirmation(str(signed_tx.id), self.context) - self.query_utxos() - - return str(signed_tx.id) - def mint_tokens( self, to: Union[str, Address], From ca916aa6cf62fbd128b8d3623d5d0435ff7ab5f2 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 01:27:58 +0200 Subject: [PATCH 049/130] Rewrite wallet minting method to use transact --- pycardano/wallet.py | 181 ++++++-------------------------------------- 1 file changed, 23 insertions(+), 158 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 4f080c2b..88ee58ea 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -738,9 +738,7 @@ def __post_init__(self): @property def payment_address(self): - return Address( - payment_part=self.address.payment_part, network=self._network - ) + return Address(payment_part=self.address.payment_part, network=self._network) @property def stake_address(self): @@ -1119,7 +1117,7 @@ def delegate( register: Optional[bool] = True, amount: Optional[Union[Ada, Lovelace]] = Lovelace(2000000), utxos: Optional[Union[UTxO, List[UTxO]]] = None, - **kwargs + **kwargs, ) -> str: """Delegate the current wallet to a pool. @@ -1139,29 +1137,30 @@ def delegate( # streamline inputs if not self.stake_address: raise ValueError("This wallet does not have staking keys.") - + # streamline inputs, use either specific utxos or all wallet utxos if utxos: if isinstance(utxos, UTxO): inputs = [utxos] else: inputs = [self] - + # check registration, do not register if already registered active = self.stake_info.get("active") if register: register = not active elif not active: - raise ValueError("Cannot delegate to a pool. This wallet is not yet registered. Try again with register=True.") - + raise ValueError( + "Cannot delegate to a pool. This wallet is not yet registered. Try again with register=True." + ) + return self.transact( inputs=inputs, - outputs=[Output(self, amount)], + outputs=Output(self, amount), stake_registration=register, delegations=pool_hash, **kwargs, ) - def withdraw( self, @@ -1190,13 +1189,13 @@ def withdraw( if not withdrawal_amount: # automatically detect rewards: withdrawal_amount = self.withdrawable_amount - + if not withdrawal_amount: raise ValueError("No rewards to withdraw.") return self.transact( inputs=[self], - outputs=[Output(self, output_amount)], + outputs=Output(self, output_amount), withdrawals={self: withdrawal_amount}, **kwargs, ) @@ -1207,11 +1206,7 @@ def mint_tokens( mints: Union[Token, List[Token]], amount: Optional[Union[Ada, Lovelace]] = None, utxos: Optional[Union[UTxO, List[UTxO]]] = None, - other_signers: Optional[Union["Wallet", List["Wallet"]]] = None, - change_address: Optional[Union["Wallet", Address, str]] = None, - message: Optional[Union[str, List[str]]] = None, - await_confirmation: Optional[bool] = False, - context: Optional[ChainContext] = None, + **kwargs, ): """Mints (or burns) tokens of a policy owned by a user wallet. To attach metadata, set it in Token class directly. Burn tokens by setting Token class amount to a negative value. @@ -1223,159 +1218,29 @@ def mint_tokens( If not set, the minimum amount will be calculated automatically. utxos (Optional[Union[UTxO, List[UTxO]]]): The UTxO(s) to use as inputs. If not set, the wallet will be queried for the latest UTxO set. - other_signers (Optional[Union["Wallet", List["Wallet"]]]): The other signers to use for the transaction. - e.g. a separate wallet which can sign the token policy - change_address (Optional[Union["Wallet", Address, str]]): The address to send any change to. - If not set, defaults to the wallet's own address - message (Optional[Union[str, List[str]]]): The message to attach to the transaction. - await_confirmation (Optional[bool]): Whether to wait for the transaction to be confirmed. Defaults to False. - context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. Returns: str: The transaction ID. """ - # streamline inputs - context = self._find_context(context) - - if isinstance(to, str): - to = Address.from_primitive(to) - - if amount and not isinstance(amount, Ada) and not isinstance(amount, Lovelace): + if amount and not issubclass(amount.__class__, Amount): raise TypeError( "Please provide amount as either `Ada(amount)` or `Lovelace(amount)`." ) - if not isinstance(mints, list): - mints = [mints] - - if isinstance(utxos, UTxO): - utxos = [utxos] - elif not utxos: - utxos = [] - - if other_signers is None: - other_signers = [] - elif not isinstance(other_signers, list): - other_signers = [other_signers] - elif not other_signers: - other_signers = [] - - if not change_address: - change_address = self.address - else: - if isinstance(change_address, str): - change_address = Address.from_primitive(change_address) - elif not isinstance(change_address, Address): - change_address = change_address.address - - # sort assets by policy_id - all_metadata = {} - mints_dict = {} - mint_metadata = {} - native_scripts = [] - for token in mints: - if isinstance(token.policy, NativeScript): - policy_hash = token.policy.hash() - elif isinstance(token.policy, TokenPolicy): - policy_hash = ScriptHash.from_primitive(token.policy_id) - else: - policy_hash = None - - policy_id = str(policy_hash) - - if not mints_dict.get(policy_hash): - mints_dict[policy_hash] = {} - - if isinstance(token.policy, NativeScript): - native_scripts.append(token.policy) - else: - native_scripts.append(token.policy.policy) - - mints_dict[policy_hash][token.name] = token - if token.metadata and token.amount > 0: - if not mint_metadata.get(policy_id): - mint_metadata[policy_id] = {} - mint_metadata[policy_id][token.name] = token.metadata - - mint_multiasset = MultiAsset() - all_assets = MultiAsset() - - for policy_hash, tokens in mints_dict.items(): - - mint_assets = Asset() - assets = Asset() - for token in tokens.values(): - assets[AssetName(token.bytes_name)] = int(token.amount) - - if token.amount > 0: - mint_assets[AssetName(token.bytes_name)] = int(token.amount) - - if mint_assets: - mint_multiasset[policy_hash] = mint_assets - all_assets[policy_hash] = assets - - # create mint metadata - if mint_metadata: - all_metadata[721] = mint_metadata - - # add message - if message: - all_metadata[674] = format_message(message) - - # Place metadata in AuxiliaryData, the format acceptable by a transaction. - if all_metadata: - auxiliary_data = AuxiliaryData( - AlonzoMetadata(metadata=Metadata(all_metadata)) - ) - else: - auxiliary_data = AuxiliaryData(Metadata()) - - # build the transaction - builder = TransactionBuilder(context) - - # add transaction inputs + # streamline inputs, use either specific utxos or all wallet utxos if utxos: - for utxo in utxos: - builder.add_input(utxo) - - builder.add_input_address(self.address) - - # set builder ttl to the min of the included policies - builder.ttl = min( - [TokenPolicy("", policy).expiration_slot for policy in native_scripts] - ) - - builder.mint = all_assets - builder.native_scripts = native_scripts - if all_metadata: - builder.auxiliary_data = auxiliary_data - - if not amount: # sent min amount if none specified - amount = Lovelace(min_lovelace(Value(0, mint_multiasset), context)) - logger.debug("Min value =", amount) - - if mint_multiasset: - builder.add_output( - TransactionOutput(to, Value(amount.lovelace, mint_multiasset)) - ) - - if other_signers: - signing_keys = [wallet.signing_key for wallet in other_signers] + [ - self.signing_key - ] + if isinstance(utxos, UTxO): + inputs = [utxos] else: - signing_keys = [self.signing_key] - - signed_tx = builder.build_and_sign(signing_keys, change_address=change_address) - - context.submit_tx(signed_tx.to_cbor()) - - if await_confirmation: - confirmed = wait_for_confirmation(str(signed_tx.id), self.context) - self.query_utxos() + inputs = [self] - return str(signed_tx.id) + return self.transact( + inputs=inputs, + outputs=Output(to, amount, tokens=mints), + mints=mints, + **kwargs, + ) def transact( self, From 9ca4edc0015d2efde616e709c0e5db54eba57ea4 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 01:28:33 +0200 Subject: [PATCH 050/130] Remove unfinished tests --- test/pycardano/test_wallet.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index a01765c8..d87a563d 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -86,8 +86,3 @@ def test_token_policy(): wallet = Wallet(name="payment") - - -def split_string_into_n_chunks(string, n): - """Split a string into n chunks.""" - return [string[i:i + n] for i in range(0, len(string), n)] From 5d84148591a31b13603400e29210d84a83c13f47 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 01:28:57 +0200 Subject: [PATCH 051/130] Remove unused test functions --- test/pycardano/test_wallet.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index d87a563d..aadd6013 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -81,8 +81,3 @@ def test_wallet(): ) ) - -def test_token_policy(): - - wallet = Wallet(name="payment") - From bb2cc74c1f72329fb74dc57acff5504b6d07c5a2 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 21:01:28 +0200 Subject: [PATCH 052/130] Add tests for wallet policy and token --- test/pycardano/test_wallet.py | 75 ++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index aadd6013..c7f4ba08 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -1,10 +1,28 @@ import pathlib +from pycardano.address import Address +from pycardano.nativescript import ScriptAll, ScriptPubkey -from pycardano.wallet import Ada, Lovelace, Wallet, TokenPolicy +from pycardano.wallet import Ada, Lovelace, Token, Wallet, TokenPolicy import pytest +def test_load_wallet(): + + w = Wallet( + name="payment", + keys_dir=str(pathlib.Path(__file__).parent / "../resources/keys"), + ) + + assert w.address == Address.from_primitive("addr1q8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3uk2g9z3d4kaf0j5l6rxunxt43x28pssehhqds2x05mwld45s399sr7") + assert w.payment_address == Address.from_primitive("addr1v8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3ukgqdsn8w") + assert w.stake_address == Address.from_primitive("stake1u9yz3gk6mw5he20apnwfn96cn9rscgvmmsxc9r86dh0k66ghyrkpw") + +WALLET = Wallet( + name="payment", + keys_dir=str(pathlib.Path(__file__).parent / "../resources/keys"), + ) + def test_amount(): """Check that the Ada / Lovelace math works as expected.""" @@ -59,20 +77,15 @@ def test_lovelace_integer(): Lovelace(5.5) -def test_wallet(): - - wallet = Wallet( - name="payment", - keys_dir=str(pathlib.Path(__file__).parent / "../resources/keys"), - ) +def test_wallet_sign_data(): assert ( - str(wallet.address) + str(WALLET.address) == "addr1q8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3uk2g9z3d4kaf0j5l6rxunxt43x28pssehhqds2x05mwld45s399sr7" ) assert ( - wallet.sign_data("pycardano rocks", mode="payment") + WALLET.sign_data("pycardano rocks", mode="payment") == ("84584da301276761646472657373581d61cc30497f4ff962f4c1dca54cceefe39f86f1" "d7179668009f8eb71e590458205797dc2cc919dfec0bb849551ebdf30d96e5cbe0f33f" "734a87fe826db30f7ef9a166686173686564f44f707963617264616e6f20726f636b73" @@ -81,3 +94,47 @@ def test_wallet(): ) ) + +def test_policy(): + + policy_dir = pathlib.Path(__file__).parent / "../resources/policy" + + script_filepath = policy_dir / f"testToken.script" + + # remove policy file if it exists + if script_filepath.exists(): + script_filepath.unlink() + + policy = TokenPolicy(name="testToken", policy_dir=str(policy_dir)) + + policy.generate_minting_policy(signers=WALLET) + + script = ScriptAll([ + ScriptPubkey(WALLET.verification_key.hash()) + ]) + + assert policy.policy_id == "6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e0831" + assert policy.policy == script + assert policy.required_signatures == [WALLET.verification_key.hash()] + + # cleanup + if script_filepath.exists(): + script_filepath.unlink() + + +def test_token(): + + script = ScriptAll([ + ScriptPubkey(WALLET.verification_key.hash()) + ]) + + policy = TokenPolicy(name="testToken", policy=script) + + token = Token(policy=policy, name="testToken", amount=1) + token_hex = Token(policy=policy, hex_name="74657374546f6b656e", amount=1) + + assert token == token_hex + assert token.hex_name == "74657374546f6b656e" + assert token.bytes_name == b"testToken" + assert token.policy_id == "6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e0831" + From a4c010a9fb9bbdbe093ae7ca74fc19d087c146fa Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 21:02:41 +0200 Subject: [PATCH 053/130] Fix order in which token policy is loaded --- pycardano/wallet.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 88ee58ea..fd2d7c40 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -294,19 +294,23 @@ def __post_init__(self): # streamline inputs if isinstance(self.policy_dir, str): self.policy_dir = Path(self.policy_dir) + + # if native script is directly provide, stop there + if self.policy: + if isinstance(self.policy, dict): + self.policy = NativeScript.from_dict(self.policy) + else: + if not self.policy_dir.exists(): + self.policy_dir.mkdir(parents=True, exist_ok=True) - if not self.policy_dir.exists(): - self.policy_dir.mkdir(parents=True, exist_ok=True) + # look for the policy + if Path(self.policy_dir / f"{self.name}.script").exists(): + with open( + Path(self.policy_dir / f"{self.name}.script"), "r" + ) as policy_file: + self.policy = NativeScript.from_dict(json.load(policy_file)) - # look for the policy - if Path(self.policy_dir / f"{self.name}.script").exists(): - with open( - Path(self.policy_dir / f"{self.name}.script"), "r" - ) as policy_file: - self.policy = NativeScript.from_dict(json.load(policy_file)) - elif isinstance(self.policy, dict): - self.policy = NativeScript.from_dict(self.policy) @property def policy_id(self): From b3b87c25fc906fbfa7538b88c54e1bfea56c1663 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 21:03:00 +0200 Subject: [PATCH 054/130] Set default amount to 0 --- pycardano/wallet.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index fd2d7c40..28d6977d 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1231,6 +1231,9 @@ def mint_tokens( raise TypeError( "Please provide amount as either `Ada(amount)` or `Lovelace(amount)`." ) + + if not amount: + amount = Lovelace(0) # streamline inputs, use either specific utxos or all wallet utxos if utxos: From d92e2dc941aa5e52b10e4cd48b9c5cdea836f469 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 21:03:12 +0200 Subject: [PATCH 055/130] Add method for auto burning tokens --- pycardano/wallet.py | 53 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 28d6977d..4e7c93f8 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1249,6 +1249,59 @@ def mint_tokens( **kwargs, ) + def burn_tokens( + self, + tokens: Union[Token, List[Token]], + amount: Optional[Union[Ada, Lovelace]] = None, + utxos: Optional[Union[UTxO, List[UTxO]]] = None, + **kwargs, + ): + """Burns tokens of a policy owned by a user wallet. + Same as mint_tokens but automatically sets Token class amount to a negative value. + + Args: + to (Union[str, Address]): The address to which to send the newly minted tokens. + mints (Union[Token, List[Token]]): The token(s) to mint/burn. Set metadata and quantity directly in the Token class. + amount (Optional[Union[Ada, Lovelace]]): The amount of Ada to attach to the transaction. + If not provided, the minimum amount will be calculated automatically. + utxos (Optional[Union[UTxO, List[UTxO]]]): The UTxO(s) to use as inputs. + If not set, the wallet will be queried for the latest UTxO set. + + Returns: + str: The transaction ID. + """ + + if amount and not issubclass(amount.__class__, Amount): + raise TypeError( + "Please provide amount as either `Ada(amount)` or `Lovelace(amount)`." + ) + + if not amount: + # attach 1 ADA to burn transactions + amount = Ada(1) + + # streamline inputs, use either specific utxos or all wallet utxos + if utxos: + if isinstance(utxos, UTxO): + inputs = [utxos] + else: + inputs = [self] + + + if not isinstance(tokens, list): + tokens = [tokens] + + # set token values to negative + for token in tokens: + token.amount = -abs(token.amount) + + return self.transact( + inputs=inputs, + outputs=Output(self, amount, tokens=tokens), + mints=tokens, + **kwargs, + ) + def transact( self, inputs: Union[ From a4019ed84653fa397aa82558f116c49582c545f9 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 21:03:23 +0200 Subject: [PATCH 056/130] Update min lovelace calculations --- pycardano/wallet.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 4e7c93f8..5f92594e 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1613,7 +1613,12 @@ def transact( if not output.amount.lovelace: # Calculate min lovelace if necessary output.amount = Lovelace( - min_lovelace(Value(0, mint_multiasset), context) + min_lovelace( + context=context, + output=TransactionOutput( + output.address, Value(1000000, mint_multiasset) + ) + ) ) builder.add_output( From 3ccf3954dbed1913f61a245df66e8e4126fd5362 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 21:03:36 +0200 Subject: [PATCH 057/130] Update todo list --- pycardano/wallet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 5f92594e..c29094a1 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -638,7 +638,7 @@ class Wallet: - Get utxo block times and sort utxos - Mint / Burn tokens - Automatically load in token polices where wallet is a signer - - Automatically create BlockFrost Chain Context + - Automatically create BlockFrost Chain Context (mainnet, preprod, and preview) - Attach messages to transactions - Sign messages - Add custom metadata fields @@ -649,8 +649,8 @@ class Wallet: - Generate fully manual transactions that can do any / all of the above Future additions (TODO list): - - Add new preview and devnet functionalities - - Add tests and examples + - Add tests + - Add examples - Create and sign multi-sig transactions - Interaction with native scripts - Interaction with plutus scripts From 457157960be42b16142dc97cd66db796195f7793 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 21:06:06 +0200 Subject: [PATCH 058/130] Format with black --- pycardano/wallet.py | 19 ++++----- test/pycardano/test_wallet.py | 79 ++++++++++++++++++----------------- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index c29094a1..980f0a77 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -294,7 +294,7 @@ def __post_init__(self): # streamline inputs if isinstance(self.policy_dir, str): self.policy_dir = Path(self.policy_dir) - + # if native script is directly provide, stop there if self.policy: if isinstance(self.policy, dict): @@ -310,8 +310,6 @@ def __post_init__(self): ) as policy_file: self.policy = NativeScript.from_dict(json.load(policy_file)) - - @property def policy_id(self): @@ -1231,7 +1229,7 @@ def mint_tokens( raise TypeError( "Please provide amount as either `Ada(amount)` or `Lovelace(amount)`." ) - + if not amount: amount = Lovelace(0) @@ -1275,23 +1273,22 @@ def burn_tokens( raise TypeError( "Please provide amount as either `Ada(amount)` or `Lovelace(amount)`." ) - + if not amount: # attach 1 ADA to burn transactions amount = Ada(1) - + # streamline inputs, use either specific utxos or all wallet utxos if utxos: if isinstance(utxos, UTxO): inputs = [utxos] else: inputs = [self] - - + if not isinstance(tokens, list): tokens = [tokens] - - # set token values to negative + + # set token values to negative for token in tokens: token.amount = -abs(token.amount) @@ -1617,7 +1614,7 @@ def transact( context=context, output=TransactionOutput( output.address, Value(1000000, mint_multiasset) - ) + ), ) ) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index c7f4ba08..0d8ca4e6 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -8,21 +8,29 @@ def test_load_wallet(): - - w = Wallet( - name="payment", - keys_dir=str(pathlib.Path(__file__).parent / "../resources/keys"), - ) - - assert w.address == Address.from_primitive("addr1q8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3uk2g9z3d4kaf0j5l6rxunxt43x28pssehhqds2x05mwld45s399sr7") - assert w.payment_address == Address.from_primitive("addr1v8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3ukgqdsn8w") - assert w.stake_address == Address.from_primitive("stake1u9yz3gk6mw5he20apnwfn96cn9rscgvmmsxc9r86dh0k66ghyrkpw") -WALLET = Wallet( + w = Wallet( name="payment", keys_dir=str(pathlib.Path(__file__).parent / "../resources/keys"), ) + assert w.address == Address.from_primitive( + "addr1q8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3uk2g9z3d4kaf0j5l6rxunxt43x28pssehhqds2x05mwld45s399sr7" + ) + assert w.payment_address == Address.from_primitive( + "addr1v8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3ukgqdsn8w" + ) + assert w.stake_address == Address.from_primitive( + "stake1u9yz3gk6mw5he20apnwfn96cn9rscgvmmsxc9r86dh0k66ghyrkpw" + ) + + +WALLET = Wallet( + name="payment", + keys_dir=str(pathlib.Path(__file__).parent / "../resources/keys"), +) + + def test_amount(): """Check that the Ada / Lovelace math works as expected.""" @@ -84,57 +92,52 @@ def test_wallet_sign_data(): == "addr1q8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3uk2g9z3d4kaf0j5l6rxunxt43x28pssehhqds2x05mwld45s399sr7" ) - assert ( - WALLET.sign_data("pycardano rocks", mode="payment") - == ("84584da301276761646472657373581d61cc30497f4ff962f4c1dca54cceefe39f86f1" - "d7179668009f8eb71e590458205797dc2cc919dfec0bb849551ebdf30d96e5cbe0f33f" - "734a87fe826db30f7ef9a166686173686564f44f707963617264616e6f20726f636b73" - "58402beecd6dba2f7f73d0d72abd5cc43829173a069afa2a29eff72d65049b092bc80c" - "571569e8a7c26354cd1d38b5fcdc3d7a3b6955d2211106824ba02c33ba220f" - ) + assert WALLET.sign_data("pycardano rocks", mode="payment") == ( + "84584da301276761646472657373581d61cc30497f4ff962f4c1dca54cceefe39f86f1" + "d7179668009f8eb71e590458205797dc2cc919dfec0bb849551ebdf30d96e5cbe0f33f" + "734a87fe826db30f7ef9a166686173686564f44f707963617264616e6f20726f636b73" + "58402beecd6dba2f7f73d0d72abd5cc43829173a069afa2a29eff72d65049b092bc80c" + "571569e8a7c26354cd1d38b5fcdc3d7a3b6955d2211106824ba02c33ba220f" ) def test_policy(): - + policy_dir = pathlib.Path(__file__).parent / "../resources/policy" - + script_filepath = policy_dir / f"testToken.script" - + # remove policy file if it exists if script_filepath.exists(): script_filepath.unlink() - + policy = TokenPolicy(name="testToken", policy_dir=str(policy_dir)) - + policy.generate_minting_policy(signers=WALLET) - - script = ScriptAll([ - ScriptPubkey(WALLET.verification_key.hash()) - ]) - - assert policy.policy_id == "6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e0831" + + script = ScriptAll([ScriptPubkey(WALLET.verification_key.hash())]) + + assert ( + policy.policy_id == "6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e0831" + ) assert policy.policy == script assert policy.required_signatures == [WALLET.verification_key.hash()] - + # cleanup if script_filepath.exists(): script_filepath.unlink() def test_token(): - - script = ScriptAll([ - ScriptPubkey(WALLET.verification_key.hash()) - ]) - + + script = ScriptAll([ScriptPubkey(WALLET.verification_key.hash())]) + policy = TokenPolicy(name="testToken", policy=script) - + token = Token(policy=policy, name="testToken", amount=1) token_hex = Token(policy=policy, hex_name="74657374546f6b656e", amount=1) - + assert token == token_hex assert token.hex_name == "74657374546f6b656e" assert token.bytes_name == b"testToken" assert token.policy_id == "6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e0831" - From 0c8879646cb9f42455d5856b3db31d8894a07736 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 21:09:23 +0200 Subject: [PATCH 059/130] Remove example notebook --- examples/using_wallet_class.ipynb | 962 ------------------------------ 1 file changed, 962 deletions(-) delete mode 100644 examples/using_wallet_class.ipynb diff --git a/examples/using_wallet_class.ipynb b/examples/using_wallet_class.ipynb deleted file mode 100644 index 3515d088..00000000 --- a/examples/using_wallet_class.ipynb +++ /dev/null @@ -1,962 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using the `Wallet`" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Imports" - ] - }, - { - "cell_type": "code", - "execution_count": 275, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "import pycardano as pyc\n", - "from pycardano.cip.cip8 import sign, verify\n", - "from pycardano.easy import Ada, Lovelace, Wallet, TokenPolicy, Token, Output\n", - "from pycardano.logging import logger\n", - "\n", - "import datetime as dt\n", - "\n", - "from pprint import pprint" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Create a wallet" - ] - }, - { - "cell_type": "code", - "execution_count": 89, - "metadata": {}, - "outputs": [], - "source": [ - "wallet_name = \"test\"" - ] - }, - { - "cell_type": "code", - "execution_count": 260, - "metadata": {}, - "outputs": [], - "source": [ - "w = Wallet(wallet_name, network=\"testnet\")" - ] - }, - { - "cell_type": "code", - "execution_count": 131, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "addr_test1qqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l280yjxzjg0gspza08tumhczpwmy040fmfsgxrkcdk2r4hsqrc2azd" - ] - }, - "execution_count": 131, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "w.address" - ] - }, - { - "cell_type": "code", - "execution_count": 188, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "addr_test1vqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l2qmchv8f" - ] - }, - "execution_count": 188, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# address of the payment part\n", - "w.payment_address" - ] - }, - { - "cell_type": "code", - "execution_count": 189, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "stake_test1urhjfrpfy85gq3whn47dmupqhdj86h5a5cyrpmvxm9p6mcqtyhx4g" - ] - }, - "execution_count": 189, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# address of the staking part\n", - "w.stake_address" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Send ADA to wallet and query utxos" - ] - }, - { - "cell_type": "code", - "execution_count": 134, - "metadata": {}, - "outputs": [], - "source": [ - "w.query_utxos()" - ] - }, - { - "cell_type": "code", - "execution_count": 135, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "15.965142\n", - "15965142\n" - ] - } - ], - "source": [ - "print(w.ada)\n", - "print(w.lovelace)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Get the creator (sender) of each utxo" - ] - }, - { - "cell_type": "code", - "execution_count": 150, - "metadata": {}, - "outputs": [], - "source": [ - "w.get_utxo_creators()" - ] - }, - { - "cell_type": "code", - "execution_count": 152, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'addr_test1vqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l2qmchv8f'" - ] - }, - "execution_count": 152, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "w.utxos[0].creator" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Get the block height of each utxo and sort " - ] - }, - { - "cell_type": "code", - "execution_count": 154, - "metadata": {}, - "outputs": [], - "source": [ - "w.get_utxo_block_times()" - ] - }, - { - "cell_type": "code", - "execution_count": 156, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "1658076307" - ] - }, - "execution_count": 156, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "w.utxos[0].block_time" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## View Tokens and get their metadata" - ] - }, - { - "cell_type": "code", - "execution_count": 128, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[Token(policy=TokenPolicy(name='6a4dfeee'), amount=1, name='PastaFun004', hex_name='506173746146756e303034', metadata={}),\n", - " Token(policy=TokenPolicy(name='6a4dfeee'), amount=2, name='PastaFun006', hex_name='506173746146756e303036', metadata={}),\n", - " Token(policy=TokenPolicy(name='6a4dfeee'), amount=4, name='PastaFun007', hex_name='506173746146756e303037', metadata={}),\n", - " Token(policy=TokenPolicy(name='6a4dfeee'), amount=2, name='PastaFun008', hex_name='506173746146756e303038', metadata={}),\n", - " Token(policy=TokenPolicy(name='83f8b74f'), amount=1, name='temptoken1', hex_name='74656d70746f6b656e31', metadata={})]" - ] - }, - "execution_count": 128, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "w.tokens" - ] - }, - { - "cell_type": "code", - "execution_count": 137, - "metadata": {}, - "outputs": [], - "source": [ - "w.get_token_metadata()" - ] - }, - { - "cell_type": "code", - "execution_count": 147, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Token(policy=TokenPolicy(name='6a4dfeee'), amount=1, name='PastaFun004', hex_name='506173746146756e303034', metadata={'name': 'Pasta Fun 004', 'image': 'ipfs://QmZ5WqN65x4dKueofyfXwiRhAoLSvue8gHp4VamKYyVKLJ', 'description': 'This is pasta!'})" - ] - }, - "execution_count": 147, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "w.tokens[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 144, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'6a4dfeee6fb7c02148c26575e594acb61dc2825ffadd0d09909c6b74'" - ] - }, - "execution_count": 144, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "w.tokens[0].policy_id" - ] - }, - { - "cell_type": "code", - "execution_count": 143, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'name': 'Pasta Fun 004',\n", - " 'image': 'ipfs://QmZ5WqN65x4dKueofyfXwiRhAoLSvue8gHp4VamKYyVKLJ',\n", - " 'description': 'This is pasta!'}" - ] - }, - "execution_count": 143, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "w.tokens[0].metadata" - ] - }, - { - "cell_type": "code", - "execution_count": 149, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'6a4dfeee6fb7c02148c26575e594acb61dc2825ffadd0d09909c6b74': {'PastaFun004': 1,\n", - " 'PastaFun006': 2,\n", - " 'PastaFun007': 4,\n", - " 'PastaFun008': 2},\n", - " '83f8b74f0b49680c9944303afaf4333dac682135e7a05ad71bc66229': {'temptoken1': 1}}\n" - ] - } - ], - "source": [ - "pprint(w.tokens_dict)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Sign a Message\n", - "- with either payment key or stake key" - ] - }, - { - "cell_type": "code", - "execution_count": 180, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "84584da301276761646472657373581d6010880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa804582073cd3b0641209194bae64953311072bab0897feef55e5df71e9e5d2f0f5aaea8a166686173686564f44b48656c6c6f20576f726c645840d53c57e6d4fb7cfaedb8225aa41b9f4f7742a85cb8f3e0638dec53ec0ce86553498d1d94fb9401d05f77d9c7bb9ca3d593ae490da69f248cb58e1dba8a47ea04\n" - ] - } - ], - "source": [ - "signed_message = w.sign_message(\"Hello World\")\n", - "print(signed_message)" - ] - }, - { - "cell_type": "code", - "execution_count": 158, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'84584da301276761646472657373581d60ef248c2921e88045d79d7cddf020bb647d5e9da60830ed86d943ade0045820a1128f3e7f44f3a3cffedf3497686ad014fc824f609daa0ccd25370e1da9b8f8a166686173686564f44b48656c6c6f20576f726c645840dcb087b86866eb465e6b0a8c4723f9482e93a14c76936f7240d8ac73335494f88cefae3d18647ebcda19648bfee556eafb506f4d0555571f8343f04bd2551905'" - ] - }, - "execution_count": 158, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "w.sign_message(\"Hello World\", mode=\"stake\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Verify Signed message" - ] - }, - { - "cell_type": "code", - "execution_count": 182, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'message': 'Hello World',\n", - " 'signing_address': addr_test1vqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l2qmchv8f,\n", - " 'verified': True}\n" - ] - } - ], - "source": [ - "verification = verify(signed_message)\n", - "pprint(verification)" - ] - }, - { - "cell_type": "code", - "execution_count": 186, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "addr_test1vqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l2qmchv8f\n", - "addr_test1vqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l2qmchv8f\n" - ] - } - ], - "source": [ - "print(w.payment_address)\n", - "print(verification[\"signing_address\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Send ADA\n", - "- for easy simple ADA-only transactions\n", - "- returns the transaction ID" - ] - }, - { - "cell_type": "code", - "execution_count": 164, - "metadata": {}, - "outputs": [], - "source": [ - "receiver = \"addr_test1qqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l280yjxzjg0gspza08tumhczpwmy040fmfsgxrkcdk2r4hsqrc2azd\"" - ] - }, - { - "cell_type": "code", - "execution_count": 165, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'12b115c85877452ac4acd41c4c03227466b8b6a4113a2a718e53f41ffa18a217'" - ] - }, - "execution_count": 165, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "w.send_ada(receiver, Ada(2.5))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Send entire UTxO(s)\n", - "- Useful for sending refunds back to sender\n", - "- With any transaction you can \"await_confirmation\". The context gets polled every N seconds and returns the transaction ID upon confirmation" - ] - }, - { - "cell_type": "code", - "execution_count": 167, - "metadata": {}, - "outputs": [], - "source": [ - "w.query_utxos()" - ] - }, - { - "cell_type": "code", - "execution_count": 171, - "metadata": {}, - "outputs": [], - "source": [ - "w.get_utxo_creators()" - ] - }, - { - "cell_type": "code", - "execution_count": 172, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'creator': 'addr_test1qqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l280yjxzjg0gspza08tumhczpwmy040fmfsgxrkcdk2r4hsqrc2azd',\n", - " 'input': {'index': 0,\n", - " 'transaction_id': TransactionId(hex='12b115c85877452ac4acd41c4c03227466b8b6a4113a2a718e53f41ffa18a217')},\n", - " 'output': {'address': addr_test1qqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l280yjxzjg0gspza08tumhczpwmy040fmfsgxrkcdk2r4hsqrc2azd,\n", - " 'amount': {'coin': 2500000, 'multi_asset': {}},\n", - " 'datum_hash': None}}\n" - ] - } - ], - "source": [ - "pprint(w.utxos[0])" - ] - }, - { - "cell_type": "code", - "execution_count": 170, - "metadata": {}, - "outputs": [], - "source": [ - "utxo_to_return = w.utxos[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 175, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0c4f49bbe6e6d9c7a252a9caf3e19d8599a00be001cb132ae5eb04eae69be269'" - ] - }, - "execution_count": 175, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "w.send_utxo(utxo_to_return.creator, utxos=utxo_to_return, await_confirmation=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Empty an entire wallet\n", - "- Useful for consolidating UTxOs or retiring the wallet\n", - "- Sends all UTxOs to one place\n", - "- Can attach a 674 metadata message to any transaction using `message`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "w.empty_wallet(receiver, message=\"Thank you!\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Minting token Example" - ] - }, - { - "cell_type": "code", - "execution_count": 193, - "metadata": {}, - "outputs": [], - "source": [ - "# Create a token policy\n", - "\n", - "policy = TokenPolicy(name=\"funTokens\")" - ] - }, - { - "cell_type": "code", - "execution_count": 197, - "metadata": {}, - "outputs": [], - "source": [ - "# Generate expiring policy script\n", - "signers = [w]\n", - "expiration = dt.datetime(2022, 12, 12, 12, 12, 12)\n", - "\n", - "policy.generate_minting_policy(signers=w, expiration=expiration, context=w.context)" - ] - }, - { - "cell_type": "code", - "execution_count": 309, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Policy ID: b05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99\n" - ] - } - ], - "source": [ - "print(\"Policy ID:\", policy.id)" - ] - }, - { - "cell_type": "code", - "execution_count": 211, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[VerificationKeyHash(hex='10880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa8')]\n", - "Expiration Slot : 76470652\n", - "Expiration Timestamp: 2022-12-12 10:11:16.131225+00:00\n", - "Is expired: False\n" - ] - } - ], - "source": [ - "# other available info\n", - "\n", - "print(policy.required_signatures)\n", - "print(\"Expiration Slot :\", policy.expiration_slot)\n", - "print(\"Expiration Timestamp: \", policy.get_expiration_timestamp(w.context))\n", - "print(\"Is expired: \", policy.is_expired(w.context))" - ] - }, - { - "cell_type": "code", - "execution_count": 308, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'scripts': [{'keyHash': '10880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa8',\n", - " 'type': 'sig'},\n", - " {'slot': 76470652, 'type': 'before'}],\n", - " 'type': 'all'}\n" - ] - } - ], - "source": [ - "pprint(policy.script.to_dict())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Mint some tokens!" - ] - }, - { - "cell_type": "code", - "execution_count": 225, - "metadata": {}, - "outputs": [], - "source": [ - "metadata = {\n", - " \"name\": \"Fun Token 001\", \n", - " \"image\": \"ipfs://QmZ5WqN65x4dKueofyfXwiRhAoLSvue8gHp4VamKYyVKLJ\", \n", - " \"description\": \"This is a fun token.\"\n", - "}\n", - "\n", - "metadata2 = {\n", - " \"name\": \"Fun Token 002\", \n", - " \"image\": \"ipfs://QmZ5WqN65x4dKueofyfXwiRhAoLSvue8gHp4VamKYyVKLJ\", \n", - " \"description\": \"This is a second fun token.\"\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 226, - "metadata": {}, - "outputs": [], - "source": [ - "funToken1 = Token(policy, amount=1, name=\"FunToken001\", metadata=metadata)\n", - "funToken2 = Token(policy, amount=2, name=\"FunToken002\", metadata=metadata2)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 227, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'c705213f6cf8f1277cdcb7bf954aa6aa2da457dd7d9f114db33038099db7e8eb'" - ] - }, - "execution_count": 227, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "w.mint_tokens(\n", - " receiver, \n", - " mints=[funToken1, funToken2], \n", - " amount=Ada(2.5), # Ada to attach to tokens\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Burn a token" - ] - }, - { - "cell_type": "code", - "execution_count": 261, - "metadata": {}, - "outputs": [], - "source": [ - "w.query_utxos()" - ] - }, - { - "cell_type": "code", - "execution_count": 263, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[Token(policy=TokenPolicy(name='funTokens'), amount=1, name='FunToken001', hex_name='46756e546f6b656e303031', metadata={}),\n", - " Token(policy=TokenPolicy(name='funTokens'), amount=2, name='FunToken002', hex_name='46756e546f6b656e303032', metadata={})]" - ] - }, - "execution_count": 263, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# the policy is recognized and loaded in automatically\n", - "\n", - "w.tokens" - ] - }, - { - "cell_type": "code", - "execution_count": 269, - "metadata": {}, - "outputs": [], - "source": [ - "to_burn = w.tokens[1]" - ] - }, - { - "cell_type": "code", - "execution_count": 270, - "metadata": {}, - "outputs": [], - "source": [ - "# set amount to a negative number to burn\n", - "\n", - "to_burn.amount = -1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "w.mint_tokens(\n", - " receiver, \n", - " mints=[to_burn], \n", - " amount=Ada(4), # Ada to attach to tokens\n", - " await_confirmation=True,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 274, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[Token(policy=TokenPolicy(name='funTokens'), amount=1, name='FunToken001', hex_name='46756e546f6b656e303031', metadata={}),\n", - " Token(policy=TokenPolicy(name='funTokens'), amount=1, name='FunToken002', hex_name='46756e546f6b656e303032', metadata={})]" - ] - }, - "execution_count": 274, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "w.tokens" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Draft a manual transaction\n", - "- Can give Wallets, addresses, string addresses, and UTxOs (or any combination thereof) as inputs.\n", - "- Must manually specify all outputs, plus a change address\n", - "- Can mint, burn, send ada, tokens, attach a message, attach any metadata\n", - "- Can return the signed transaction CBOR for sending to others in case of multisignature scenarios" - ] - }, - { - "cell_type": "code", - "execution_count": 321, - "metadata": {}, - "outputs": [], - "source": [ - "outputs = [\n", - " Output(w, Ada(2), funToken1),\n", - " Output(w, Lovelace(1700000), funToken2)\n", - "]" - ] - }, - { - "cell_type": "code", - "execution_count": 322, - "metadata": {}, - "outputs": [], - "source": [ - "tx = w.manual(\n", - " inputs=w,\n", - " outputs=outputs,\n", - " message=\"Fun times.\",\n", - " mints=[funToken1, funToken2],\n", - " other_metadata={111: \"Random stuff\"},\n", - " submit=False,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 323, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "84a60081825820c705213f6cf8f1277cdcb7bf954aa6aa2da457dd7d9f114db33038099db7e8eb0101828258390010880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa8ef248c2921e88045d79d7cddf020bb647d5e9da60830ed86d943ade0821a0084acc7a3581c6a4dfeee6fb7c02148c26575e594acb61dc2825ffadd0d09909c6b74a44b506173746146756e303034014b506173746146756e303036024b506173746146756e303037044b506173746146756e30303802581c83f8b74f0b49680c9944303afaf4333dac682135e7a05ad71bc66229a14a74656d70746f6b656e3101581cb05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99a14b46756e546f6b656e303031018258390010880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa8ef248c2921e88045d79d7cddf020bb647d5e9da60830ed86d943ade0821a0019f0a0a1581cb05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99a14b46756e546f6b656e30303202021a00030c61031a048ed97c075820ee0d472bcd8a990980bdbc8e8a968c384f43d66191e4d8f02776b57424657e8109a1581cb05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99a24b46756e546f6b656e303031014b46756e546f6b656e30303202a2008182582073cd3b0641209194bae64953311072bab0897feef55e5df71e9e5d2f0f5aaea85840d7f7e80d1dae76d3002d35684c8d5799cc27af2e5b5b8fe2486e9efb681f3c37ab8089d296036d834ae178168017d5eae0b3d45635655e7981cf2444d38ceb0a01818201828200581c10880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa882051a048ed97cf5d90103a100a3186f6c52616e646f6d2073747566661902a2a1636d7367816a46756e2074696d65732e1902d1a178386230353435396232643939343036353637623361363665333065313431356665323465323333643166653666303633633130663739643939a26b46756e546f6b656e303031a3646e616d656d46756e20546f6b656e2030303165696d6167657835697066733a2f2f516d5a3557714e36357834644b75656f6679665877695268416f4c53767565386748703456616d4b5979564b4c4a6b6465736372697074696f6e745468697320697320612066756e20746f6b656e2e6b46756e546f6b656e303032a3646e616d656d46756e20546f6b656e2030303265696d6167657835697066733a2f2f516d5a3557714e36357834644b75656f6679665877695268416f4c53767565386748703456616d4b5979564b4c4a6b6465736372697074696f6e781b546869732069732061207365636f6e642066756e20746f6b656e2e\n" - ] - } - ], - "source": [ - "print(tx)" - ] - }, - { - "cell_type": "code", - "execution_count": 324, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'auxiliary_data': AuxiliaryData(data=AlonzoMetadata(metadata={111: 'Random stuff', 674: {'msg': ['Fun times.']}, 721: {'b05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99': {'FunToken001': {'name': 'Fun Token 001', 'image': 'ipfs://QmZ5WqN65x4dKueofyfXwiRhAoLSvue8gHp4VamKYyVKLJ', 'description': 'This is a fun token.'}, 'FunToken002': {'name': 'Fun Token 002', 'image': 'ipfs://QmZ5WqN65x4dKueofyfXwiRhAoLSvue8gHp4VamKYyVKLJ', 'description': 'This is a second fun token.'}}}}, native_scripts=None, plutus_scripts=None)),\n", - " 'transaction_body': {'auxiliary_data_hash': AuxiliaryDataHash(hex='ee0d472bcd8a990980bdbc8e8a968c384f43d66191e4d8f02776b57424657e81'),\n", - " 'certificates': None,\n", - " 'collateral': None,\n", - " 'fee': 199777,\n", - " 'inputs': [{'index': 1,\n", - " 'transaction_id': TransactionId(hex='c705213f6cf8f1277cdcb7bf954aa6aa2da457dd7d9f114db33038099db7e8eb')}],\n", - " 'mint': {ScriptHash(hex='b05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99'): {AssetName(b'FunToken001'): 1, AssetName(b'FunToken002'): 2}},\n", - " 'network_id': None,\n", - " 'outputs': [{'address': addr_test1qqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l280yjxzjg0gspza08tumhczpwmy040fmfsgxrkcdk2r4hsqrc2azd,\n", - " 'amount': {'coin': 8694983,\n", - " 'multi_asset': {ScriptHash(hex='6a4dfeee6fb7c02148c26575e594acb61dc2825ffadd0d09909c6b74'): {AssetName(b'PastaFun004'): 1, AssetName(b'PastaFun006'): 2, AssetName(b'PastaFun007'): 4, AssetName(b'PastaFun008'): 2}, ScriptHash(hex='83f8b74f0b49680c9944303afaf4333dac682135e7a05ad71bc66229'): {AssetName(b'temptoken1'): 1}, ScriptHash(hex='b05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99'): {AssetName(b'FunToken001'): 1}}},\n", - " 'datum_hash': None},\n", - " {'address': addr_test1qqggspuexxdqxf8fkrtvx5usy967zp7zk4ykwrea27e2l280yjxzjg0gspza08tumhczpwmy040fmfsgxrkcdk2r4hsqrc2azd,\n", - " 'amount': {'coin': 1700000,\n", - " 'multi_asset': {ScriptHash(hex='b05459b2d99406567b3a66e30e1415fe24e233d1fe6f063c10f79d99'): {AssetName(b'FunToken002'): 2}}},\n", - " 'datum_hash': None}],\n", - " 'required_signers': None,\n", - " 'script_data_hash': None,\n", - " 'ttl': 76470652,\n", - " 'update': None,\n", - " 'validity_start': None,\n", - " 'withdraws': None},\n", - " 'transaction_witness_set': {'bootstrap_witness': None,\n", - " 'native_scripts': [ScriptAll(_TYPE=1, native_scripts=[ScriptPubkey(_TYPE=0, key_hash=VerificationKeyHash(hex='10880799319a0324e9b0d6c353902175e107c2b549670f3d57b2afa8')), InvalidHereAfter(_TYPE=5, after=76470652)])],\n", - " 'plutus_data': None,\n", - " 'plutus_script': None,\n", - " 'redeemer': None,\n", - " 'vkey_witnesses': [{'signature': b\"\\xd7\\xf7\\xe8\\r\\x1d\\xaev\\xd3\\x00-5hL\\x8dW\\x99\\xcc'\\xaf.\"\n", - " b'[[\\x8f\\xe2Hn\\x9e\\xfbh\\x1f<7\\xab\\x80\\x89\\xd2\\x96\\x03m\\x83'\n", - " b'J\\xe1x\\x16\\x80\\x17\\xd5\\xea\\xe0\\xb3\\xd4V5e^y\\x81\\xcf$D'\n", - " b'\\xd3\\x8c\\xeb\\n',\n", - " 'vkey': {\"type\": \"\", \"description\": \"\", \"cborHex\": \"582073cd3b0641209194bae64953311072bab0897feef55e5df71e9e5d2f0f5aaea8\"}}]},\n", - " 'valid': True}\n" - ] - } - ], - "source": [ - "print(pyc.Transaction.from_cbor(tx))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "pycardano-dev", - "language": "python", - "name": "pycardano-dev" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.8" - }, - "toc-autonumbering": true - }, - "nbformat": 4, - "nbformat_minor": 4 -} From 76105b47047f712d2d2bcce36b33cf12d9ac5be6 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 22:02:14 +0200 Subject: [PATCH 060/130] Add wallet example code --- examples/wallet.py | 144 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 examples/wallet.py diff --git a/examples/wallet.py b/examples/wallet.py new file mode 100644 index 00000000..579fb85a --- /dev/null +++ b/examples/wallet.py @@ -0,0 +1,144 @@ +""" +This is a walk through of the Wallet class in pycardano. +This class offers a number of methods which simplify many basic use cases, +and abstracts away some of the lower level details. +If you need more advanced functionality, you can directly use the lower level classes and methods, +sometimes in tandem with the Wallet class. + +Note: If you plan to use Blockfrost as your chain context, you can set the following environment variables: + `BLOCKFROST_ID_MAINNET` for mainnet + `BLOCKFROST_ID_PREPROD` for preprod + `BLOCKFROST_ID_PREVIEW` for preview + + +""" + +from datetime import datetime +from multiprocessing import pool +from pycardano import * +from pycardano.wallet import Output, Wallet, Ada, TokenPolicy, Token + +"""Create a new wallet""" +# Make sure to provide a name so you can easily load it later +# this will save the keys to ./keys/my_wallet.* +# payment and staking keys will be automatically generated, or loaded a wallet of the given name already exists +w = Wallet(name="my_wallet") # set the parameter `network` to mainnet, preprod, or preview + + + +w.query_utxos() # query the wallet for its balance + +w.utxos # list of wallets UTXOs +w.ada # view total amount of ADA +w.lovelace # view total amount of lovelace +w.tokens # get a list of all tokens in the wallet + + +"""Send ADA using the wallet""" + +receiver = Address("addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x") + +tx_id = w.send_ada(receiver, Ada(2)) # send 2 ADA to the receiver + + +"""Sending an entire UTxO""" +# useful if you have to send a refund, for example +tx_id = w.send_utxo(receiver, w.utxos[0]) # send the first UTXO in the wallet to the receiver + + +"""Empty an entire wallet""" +# Careful with this one! +tx_id = w.empty_wallet(receiver) + + +"""Sign data""" +# can sign a message with either the payment or stake keys +signed_message = w.sign_data("Hello world!", mode="payment") # default mode is "payment" + + +"""Mint a token""" + +# first generate a policy +my_policy = TokenPolicy(name="my_policy") # give it a descriptive name + +# generate a locking policy with expiration +# note: the actual policy locking time might be slightly off from the datetime provided +# this will save a file to ./policy/my_policy.policy +my_policy.generate_minting_policy(signers=w, expiration=datetime(2025, 5, 12, 12, 0, 0)) + +# create a token with metadata +metadata = { + "description": "This is my first NFT thanks to PyCardano", + "name": "PyCardano NFT example token 1", + "id": 1, + "image": "ipfs://QmRhTTbUrPYEw3mJGGhQqQST9k86v1DPBiTTWJGKDJsVFw", +} + +my_nft = Token(policy=my_policy, amount=2, name="MY_NFT_1", metadata=metadata) + +tx_id = w.mint_tokens( + to=receiver, + mints=my_nft, # can be a single Token or list of multiple +) + + +"""Burn a token""" +# Oops, we minted two. Let's burn one. +# There are two ways to do this: + +# Method 2 +# create a token object with the quantity you want to burn +# note you don't have to include metadata here since it's only relevant for minting, not burning +my_nft = Token(policy=my_policy, amount=1, name="MY_NFT_1") + +# this will automatically switch the amount to negative and burn them +tx_id = w.burn_tokens(my_nft) + +# Method 2 +# this method might be relevant in case you want to mint and burn multiple tokens in one transaction +# set amount to negative integer to burn +my_nft = Token(policy=my_policy, amount=-1, name="MY_NFT_1") + +# then use the mint_tokens method +tx_id = w.mint_tokens( + to=receiver, + mints=my_nft, # can be a single Token or list of multiple +) + + +"""Register a stake address and delegate to a pool""" + +pool_hash = "pool17arkxvwwh8rx0ldrzc54ysmpq4pd6gwzl66qe84a0nmh7qx7ze5" + +tx_id = w.delegate(pool_hash) + + +"""Withdraw staking rewards""" +# withdraws all rewards by default, otherwise set `withdraw_amount` to a specific quantity +tx_id = w.withdraw() + +"""Fully Manual Transaction""" +# Let's make a monster transaction with all the bells and whistles + +my_nft = Token(policy=my_policy, amount=1, name="MY_NFT_1", metadata=metadata) +your_nft = Token(policy=my_policy, amount=1, name="YOUR_NFT_1", metadata={"Name": "Your NFT"}) + + +tx_id = w.transact( + inputs=w, # use all UTXOs in the wallet, can also specify unique UTxOs or addresses + outputs=[ + Output(w, Ada(0), [my_nft]), # mint an NFT to myself, setting Ada(0) will automatically calculate the minimum amount of ADA needed + Output(receiver, Ada(10), [my_nft]), # send 10 ADA and an NFT to the receiver + ], + mints=[my_nft, your_nft], # must list all mints/burns here, even if they are sent to yourself + # signers = [w, other_w], # if you want to sign with multiple wallets or keys, specify them here + delegations=pool_hash, # delegate to a pool + withdrawals=Ada(2), # withdraw 2 ADA + # change_address=w, # specify a change address, will default to itself if not specified + message="I love PyCardano", # attach a message to the transaction metadata [CIP-0020] + other_metadata={ # attach any other metadata + "247": {"random": "metadata"} + }, + # submit=True # set to False to return the transaction body as CBOR + await_confirmation=True, # set to True to block the code and periodically check until the transaction is confirmed +) From 01a454f0cdad361a3592f12bebcd4fb9472f0f8f Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 28 Sep 2022 22:08:06 +0200 Subject: [PATCH 061/130] Add further clarification about transaction kwargs --- examples/wallet.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/wallet.py b/examples/wallet.py index 579fb85a..b5da0020 100644 --- a/examples/wallet.py +++ b/examples/wallet.py @@ -119,6 +119,9 @@ """Fully Manual Transaction""" # Let's make a monster transaction with all the bells and whistles +# Note: All the above examples are based on the following `transact` method +# so you can also pass any of the following parameters to the above methods as well as **kwargs +# e.g. `change_address`, `signers`, `message`, `await_confirmation`, etc. my_nft = Token(policy=my_policy, amount=1, name="MY_NFT_1", metadata=metadata) your_nft = Token(policy=my_policy, amount=1, name="YOUR_NFT_1", metadata={"Name": "Your NFT"}) From 78b1095de26671478659862169f56f8cf63dddae Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 7 Oct 2022 20:00:20 +0200 Subject: [PATCH 062/130] Fix input bug on mint/burn tokens --- pycardano/wallet.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 980f0a77..0cec46cc 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1237,6 +1237,8 @@ def mint_tokens( if utxos: if isinstance(utxos, UTxO): inputs = [utxos] + else: + inputs = utxos else: inputs = [self] @@ -1282,6 +1284,8 @@ def burn_tokens( if utxos: if isinstance(utxos, UTxO): inputs = [utxos] + else: + inputs = utxos else: inputs = [self] From 8ee307a668821b372cad5297a68efc6fe80ab376 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 7 Oct 2022 11:03:46 -0700 Subject: [PATCH 063/130] Fix bug in mint/burn input variable --- pycardano/wallet.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 980f0a77..0cec46cc 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1237,6 +1237,8 @@ def mint_tokens( if utxos: if isinstance(utxos, UTxO): inputs = [utxos] + else: + inputs = utxos else: inputs = [self] @@ -1282,6 +1284,8 @@ def burn_tokens( if utxos: if isinstance(utxos, UTxO): inputs = [utxos] + else: + inputs = utxos else: inputs = [self] From 221b13091d0a9206860562e44abe3e812f32254b Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 17:56:48 +0200 Subject: [PATCH 064/130] Add blockfrost context checker as function wrapper --- pycardano/wallet.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 0cec46cc..fba1882c 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -3,6 +3,7 @@ import json import logging import operator +from functools import wraps from os import getenv from pathlib import Path from time import sleep @@ -49,7 +50,20 @@ # set logging level to info logger.setLevel(logging.INFO) +# function wrappers +def blockfrost_only(func): + @wraps(func) + def wrapper(*args, **kwargs): + if isinstance(kwargs.get("context"), BlockFrostChainContext) or isinstance( + args[1], BlockFrostChainContext + ): + func(*args, **kwargs) + else: + raise TypeError( + f"Function {func.__name__} is only available for context of type BlockFrostChainContext." + ) + return wrapper class Amount: """Base class for Cardano currency amounts.""" @@ -542,6 +556,7 @@ def bytes_name(self): def policy_id(self): return self.policy.policy_id + @blockfrost_only def get_on_chain_metadata(self, context: ChainContext) -> dict: """Get the on-chain metadata of the token. @@ -1667,6 +1682,7 @@ def transact( # Utility and Helper functions +@blockfrost_only def get_utxo_creator(utxo: UTxO, context: ChainContext) -> Address: """Fetch the creator of a UTxO. If there are multiple input UTxOs, the creator is the first one. @@ -1687,12 +1703,8 @@ def get_utxo_creator(utxo: UTxO, context: ChainContext) -> Address: return utxo_creator - else: - logger.warn( - "Fetching UTxO creators (sender) is only possible with Blockfrost Chain Context." - ) - +@blockfrost_only def get_utxo_block_time(utxo: UTxO, context: ChainContext) -> int: """Get the block time at which a UTxO was created. @@ -1709,12 +1721,8 @@ def get_utxo_block_time(utxo: UTxO, context: ChainContext) -> int: return block_time - else: - logger.warn( - "Fetching UTxO block time is only possible with Blockfrost Chain Context." - ) - +@blockfrost_only def get_stake_info(stake_address: Union[str, Address], context: ChainContext) -> dict: """Get the stake info of a stake address from Blockfrost. For more info see: https://docs.blockfrost.io/#tag/Cardano-Accounts/paths/~1accounts~1{stake_address}/get @@ -1878,6 +1886,7 @@ def get_all_policies( return policies +@blockfrost_only def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext) -> bool: """Confirm that a transaction has been included in a block. @@ -1899,12 +1908,8 @@ def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext) -> bool: return confirmed - else: - logger.warn( - "Confirming transactions is is only possible with Blockfrost Chain Context." - ) - +@blockfrost_only def wait_for_confirmation( tx_id: Union[str, TransactionId], context: ChainContext, delay: Optional[int] = 10 ) -> bool: From e6d2fb96a97115ac10d5864379cf4b64dab933fa Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 17:57:19 +0200 Subject: [PATCH 065/130] Convert Amount and subclasses to frozen dataclass --- pycardano/wallet.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index fba1882c..6ed7c76b 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -64,22 +64,25 @@ def wrapper(*args, **kwargs): ) return wrapper + + +@dataclass(frozen=True) class Amount: """Base class for Cardano currency amounts.""" - def __init__(self, amount: Union[float, int] = 0, amount_type="lovelace"): + _amount: Union[float, int] + _amount_type: str = "lovelace" - self._amount = amount - self._amount_type = amount_type + def __post_init__(self): if self._amount_type == "lovelace": - self.lovelace = int(self._amount) - self.ada = self._amount / 1000000 + self._lovelace = int(self._amount) + self._ada = self._amount / 1000000 else: - self.lovelace = int(self._amount * 1000000) - self.ada = self._amount + self._lovelace = int(self._amount * 1000000) + self._ada = self._amount - self._amount_dict = {"lovelace": self.lovelace, "ada": self.ada} + self._amount_dict = {"lovelace": self._lovelace, "ada": self._ada} @property def amount(self): @@ -90,6 +93,16 @@ def amount(self): else: return self.ada + @property + def lovelace(self): + """Returns the lovelace amount""" + return self._lovelace + + @property + def ada(self): + """Returns the ada amount""" + return self._ada + def __eq__(self, other): if isinstance(other, (int, float)): return self.amount == other From b153ebdae55e0c09864378cb944b4e1a74c6804b Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 17:57:39 +0200 Subject: [PATCH 066/130] Rename query_utxos to sync --- pycardano/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 6ed7c76b..bef6751d 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -948,7 +948,7 @@ def _get_tokens(self): self._token_dict = tokens self._token_list = token_list - def query_utxos(self, context: Optional[ChainContext] = None): + def sync(self, context: Optional[ChainContext] = None): """Query the blockchain for all UTxOs associated with this wallet. Args: From 75b35a13ab334d7e5ae8454715fc73add243c205 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 17:57:58 +0200 Subject: [PATCH 067/130] Rename withdraw to withdraw_rewards --- pycardano/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index bef6751d..78560fcf 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1192,7 +1192,7 @@ def delegate( **kwargs, ) - def withdraw( + def withdraw_rewards( self, withdrawal_amount: Optional[Union[Ada, Lovelace]] = None, output_amount: Optional[Union[Ada, Lovelace]] = Lovelace(1000000), From 0f736163328829822707322f9d997e9cf93c2d76 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 17:58:27 +0200 Subject: [PATCH 068/130] Rename call of query_utxos to sync --- pycardano/wallet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 78560fcf..b12560b1 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1688,8 +1688,8 @@ def transact( context.submit_tx(signed_tx.to_cbor()) if await_confirmation: - confirmed = wait_for_confirmation(str(signed_tx.id), self.context) - self.query_utxos() + _ = wait_for_confirmation(str(signed_tx.id), self.context) + self.sync() return str(signed_tx.id) From a7463cbc8e14f4567411ef13849928c3019b5735 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 17:59:24 +0200 Subject: [PATCH 069/130] Remove all independent checks for BF chain context --- pycardano/wallet.py | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index b12560b1..829e8195 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -582,12 +582,6 @@ def get_on_chain_metadata(self, context: ChainContext) -> dict: """ - if not isinstance(context, BlockFrostChainContext): - logger.warn( - "Getting on-chain metadata is is only possible with Blockfrost Chain Context." - ) - return {} - try: metadata = context.api.asset( self.policy.id + self.hex_name @@ -1747,23 +1741,17 @@ def get_stake_info(stake_address: Union[str, Address], context: ChainContext) -> Returns: dict: Info regarding the given stake address. """ - if isinstance(context, BlockFrostChainContext): - - if isinstance(stake_address, str): - stake_address = Address.from_primitive(stake_address) - if not stake_address.staking_part: - raise TypeError(f"Address {stake_address} has no staking part.") + if isinstance(stake_address, str): + stake_address = Address.from_primitive(stake_address) - try: - return context.api.accounts(str(stake_address)).to_dict() - except ApiError: - return {} + if not stake_address.staking_part: + raise TypeError(f"Address {stake_address} has no staking part.") - else: - logger.warn( - "Retrieving stake address information is only possible with Blockfrost Chain Context." - ) + try: + return context.api.accounts(str(stake_address)).to_dict() + except ApiError: + return {} def get_stake_address(address: Union[str, Address]) -> Address: @@ -1936,11 +1924,6 @@ def wait_for_confirmation( Returns: bool: Whether the transaction has been confirmed. """ - if not isinstance(context, BlockFrostChainContext): - logger.warn( - "Confirming transactions is is only possible with Blockfrost Chain Context." - ) - return confirmed = False while not confirmed: From 24081c6076b7efbe8de887bc497c6f4c27d7c3f2 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 17:59:34 +0200 Subject: [PATCH 070/130] Remove call of logging function --- pycardano/wallet.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 829e8195..4eb7fe81 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -47,8 +47,6 @@ from pycardano.txbuilder import TransactionBuilder from pycardano.utils import min_lovelace -# set logging level to info -logger.setLevel(logging.INFO) # function wrappers def blockfrost_only(func): From 005e552df5ee2bedc4504e69201a8dd952d4f8bd Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 17:59:48 +0200 Subject: [PATCH 071/130] import dataclass needed for Amount --- pycardano/wallet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 4eb7fe81..31d3fa67 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -3,6 +3,7 @@ import json import logging import operator +from dataclasses import dataclass, field from functools import wraps from os import getenv from pathlib import Path From c2376184aba45eede38a0666649ecc042c1d5665 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 18:00:17 +0200 Subject: [PATCH 072/130] Remove another independent check for BF context --- pycardano/wallet.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 31d3fa67..5ec01140 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1700,14 +1700,11 @@ def get_utxo_creator(utxo: UTxO, context: ChainContext) -> Address: Returns: Address: The creator of the UTxO. """ - if isinstance(context, BlockFrostChainContext): - utxo_creator = ( - context.api.transaction_utxos(str(utxo.input.transaction_id)) - .inputs[0] - .address - ) + utxo_creator = ( + context.api.transaction_utxos(str(utxo.input.transaction_id)).inputs[0].address + ) - return utxo_creator + return utxo_creator @blockfrost_only From dafa0849b150de89fad3517e788865f02ed201fa Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 18:00:32 +0200 Subject: [PATCH 073/130] Remove final independent check for BF chain context --- pycardano/wallet.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 5ec01140..4f9981d3 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1719,10 +1719,9 @@ def get_utxo_block_time(utxo: UTxO, context: ChainContext) -> int: int: The block time at which the UTxO was created. """ - if isinstance(context, BlockFrostChainContext): - block_time = context.api.transaction(str(utxo.input.transaction_id)).block_time + block_time = context.api.transaction(str(utxo.input.transaction_id)).block_time - return block_time + return block_time @blockfrost_only From 1f0360d0e4ecb00f6409d1991ea847743a949c31 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 18:01:01 +0200 Subject: [PATCH 074/130] Remove unused variable --- pycardano/wallet.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 4f9981d3..42a565a6 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1893,16 +1893,14 @@ def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext) -> bool: Returns: bool: Whether the transaction has been included in a block. """ - if isinstance(context, BlockFrostChainContext): - try: - transaction_info = context.api.transaction(str(tx_id)) - confirmed = True - except ApiError: - confirmed = False - transaction_info = {} + try: + _ = context.api.transaction(str(tx_id)) + confirmed = True + except ApiError: + confirmed = False - return confirmed + return confirmed @blockfrost_only From dd3817255f32866c6534cf80c1eaed0a4a820a7e Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 18:01:57 +0200 Subject: [PATCH 075/130] Clean code and format with makefile --- examples/wallet.py | 53 ++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/examples/wallet.py b/examples/wallet.py index b5da0020..afe984c6 100644 --- a/examples/wallet.py +++ b/examples/wallet.py @@ -15,21 +15,23 @@ from datetime import datetime from multiprocessing import pool + from pycardano import * -from pycardano.wallet import Output, Wallet, Ada, TokenPolicy, Token +from pycardano.wallet import Ada, Output, Token, TokenPolicy, Wallet """Create a new wallet""" # Make sure to provide a name so you can easily load it later # this will save the keys to ./keys/my_wallet.* # payment and staking keys will be automatically generated, or loaded a wallet of the given name already exists -w = Wallet(name="my_wallet") # set the parameter `network` to mainnet, preprod, or preview - +w = Wallet( + name="my_wallet" +) # set the parameter `network` to mainnet, preprod, or preview -w.query_utxos() # query the wallet for its balance +w.sync() # query the wallet for its balance w.utxos # list of wallets UTXOs -w.ada # view total amount of ADA +w.ada # view total amount of ADA w.lovelace # view total amount of lovelace w.tokens # get a list of all tokens in the wallet @@ -43,7 +45,9 @@ """Sending an entire UTxO""" # useful if you have to send a refund, for example -tx_id = w.send_utxo(receiver, w.utxos[0]) # send the first UTXO in the wallet to the receiver +tx_id = w.send_utxo( + receiver, w.utxos[0] +) # send the first UTXO in the wallet to the receiver """Empty an entire wallet""" @@ -53,7 +57,9 @@ """Sign data""" # can sign a message with either the payment or stake keys -signed_message = w.sign_data("Hello world!", mode="payment") # default mode is "payment" +signed_message = w.sign_data( + "Hello world!", mode="payment" +) # default mode is "payment" """Mint a token""" @@ -64,7 +70,7 @@ # generate a locking policy with expiration # note: the actual policy locking time might be slightly off from the datetime provided # this will save a file to ./policy/my_policy.policy -my_policy.generate_minting_policy(signers=w, expiration=datetime(2025, 5, 12, 12, 0, 0)) +my_policy.generate_minting_policy(signers=w, expiration=datetime(2025, 5, 12, 12, 0, 0)) # create a token with metadata metadata = { @@ -97,7 +103,7 @@ # Method 2 # this method might be relevant in case you want to mint and burn multiple tokens in one transaction # set amount to negative integer to burn -my_nft = Token(policy=my_policy, amount=-1, name="MY_NFT_1") +my_nft = Token(policy=my_policy, amount=-1, name="MY_NFT_1") # then use the mint_tokens method tx_id = w.mint_tokens( @@ -115,7 +121,7 @@ """Withdraw staking rewards""" # withdraws all rewards by default, otherwise set `withdraw_amount` to a specific quantity -tx_id = w.withdraw() +tx_id = w.withdraw() """Fully Manual Transaction""" # Let's make a monster transaction with all the bells and whistles @@ -124,24 +130,29 @@ # e.g. `change_address`, `signers`, `message`, `await_confirmation`, etc. my_nft = Token(policy=my_policy, amount=1, name="MY_NFT_1", metadata=metadata) -your_nft = Token(policy=my_policy, amount=1, name="YOUR_NFT_1", metadata={"Name": "Your NFT"}) +your_nft = Token( + policy=my_policy, amount=1, name="YOUR_NFT_1", metadata={"Name": "Your NFT"} +) tx_id = w.transact( - inputs=w, # use all UTXOs in the wallet, can also specify unique UTxOs or addresses + inputs=w, # use all UTXOs in the wallet, can also specify unique UTxOs or addresses outputs=[ - Output(w, Ada(0), [my_nft]), # mint an NFT to myself, setting Ada(0) will automatically calculate the minimum amount of ADA needed + Output( + w, Ada(0), [my_nft] + ), # mint an NFT to myself, setting Ada(0) will automatically calculate the minimum amount of ADA needed Output(receiver, Ada(10), [my_nft]), # send 10 ADA and an NFT to the receiver ], - mints=[my_nft, your_nft], # must list all mints/burns here, even if they are sent to yourself + mints=[ + my_nft, + your_nft, + ], # must list all mints/burns here, even if they are sent to yourself # signers = [w, other_w], # if you want to sign with multiple wallets or keys, specify them here - delegations=pool_hash, # delegate to a pool - withdrawals=Ada(2), # withdraw 2 ADA + delegations=pool_hash, # delegate to a pool + withdrawals=Ada(2), # withdraw 2 ADA # change_address=w, # specify a change address, will default to itself if not specified - message="I love PyCardano", # attach a message to the transaction metadata [CIP-0020] - other_metadata={ # attach any other metadata - "247": {"random": "metadata"} - }, + message="I love PyCardano", # attach a message to the transaction metadata [CIP-0020] + other_metadata={"247": {"random": "metadata"}}, # attach any other metadata # submit=True # set to False to return the transaction body as CBOR - await_confirmation=True, # set to True to block the code and periodically check until the transaction is confirmed + await_confirmation=True, # set to True to block the code and periodically check until the transaction is confirmed ) From 9973e826631fa1b78ead1de111bcd4bfae11a0d8 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 18:02:50 +0200 Subject: [PATCH 076/130] Format and clean code --- pycardano/wallet.py | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 42a565a6..0feb0c50 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1,7 +1,5 @@ -from dataclasses import dataclass, field import datetime import json -import logging import operator from dataclasses import dataclass, field from functools import wraps @@ -620,7 +618,8 @@ def __post_init__(self): and not isinstance(self.amount, int) ): raise TypeError( - "Please provide amount as either `Ada(amount)` or `Lovelace(amount)`. Otherwise provide lovelace as an integer." + "Please provide amount as either `Ada(amount)` or `Lovelace(amount)`.", + "Otherwise provide lovelace as an integer.", ) if isinstance(self.amount, int): @@ -679,7 +678,8 @@ class Wallet: name (str): The name of the wallet. This is required and keys will be automatically generated and saved with this name. If the wallet already exists in keys_dir, it will be loaded automatically. - address (Optional[Union[Address, str]]): Optionally provide an address to use a wallet without signing capabilities. + address (Optional[Union[Address, str]]): Optionally provide an address to use as a wallet + without signing capabilities. keys_dir (Optional[Union[str, Path]]): Directory in which to save the keys. Defaults to "./priv". use_stake (Optional[bool]): Whether to use a stake address for this wallet. Defaults to True. network (Optional[str, Network]): The network to use for the wallet. @@ -1047,7 +1047,8 @@ def sign_data( At the moment, Eternl currently does not attach the COSE key, while Nami does. Returns: - Union[str, dict]: The signature. If attach_cose_key is True, the signature is a dictionary with the signature and the COSE key. + Union[str, dict]: The signature. If attach_cose_key is True, + the signature is a dictionary with the signature and the COSE key. """ if mode == "payment": @@ -1231,12 +1232,15 @@ def mint_tokens( utxos: Optional[Union[UTxO, List[UTxO]]] = None, **kwargs, ): - """Mints (or burns) tokens of a policy owned by a user wallet. To attach metadata, set it in Token class directly. - Burn tokens by setting Token class amount to a negative value. + """Mints (or burns) tokens of a policy owned by a user wallet. To attach metadata, + set it in Token class directly. + + Burn tokens by setting Token class amount to a negative value or using burn_tokens directly. Args: to (Union[str, Address]): The address to which to send the newly minted tokens. - mints (Union[Token, List[Token]]): The token(s) to mint/burn. Set metadata and quantity directly in the Token class. + mints (Union[Token, List[Token]]): The token(s) to mint/burn. + Set metadata and quantity directly in the Token class. amount (Optional[Union[Ada, Lovelace]]): The amount of Ada to attach to the transaction. If not set, the minimum amount will be calculated automatically. utxos (Optional[Union[UTxO, List[UTxO]]]): The UTxO(s) to use as inputs. @@ -1282,7 +1286,8 @@ def burn_tokens( Args: to (Union[str, Address]): The address to which to send the newly minted tokens. - mints (Union[Token, List[Token]]): The token(s) to mint/burn. Set metadata and quantity directly in the Token class. + mints (Union[Token, List[Token]]): The token(s) to mint/burn. + Set metadata and quantity directly in the Token class. amount (Optional[Union[Ada, Lovelace]]): The amount of Ada to attach to the transaction. If not provided, the minimum amount will be calculated automatically. utxos (Optional[Union[UTxO, List[UTxO]]]): The UTxO(s) to use as inputs. @@ -1360,20 +1365,24 @@ def transact( Construct fully manual transactions. Args: - inputs (Union[Wallet, Address, UTxO, str, List[Wallet], List[Address], List[UTxO], List[str]]): Inputs to the transaction. - If wallets or addresses are provided, they will be queried for UTxOs. + inputs (Union[Wallet, Address, UTxO, str, List[Wallet], List[Address], List[UTxO], List[str]]): + Inputs to the transaction. If wallets or addresses are provided, they will be queried for UTxOs. outputs (Union[Output, List[Output]]): Outputs of the transaction using the Output class. Can specify any number of recipients, ADA amounts, and native tokens. mints (Union[Token, List[Token]]): The token(s) to mint/burn. Set metadata and quantity here. - signers (Union[Wallet, List[Wallet], SigningKey, List[SigningKey]]): The wallets or keys with which to sign the transaction. - stake_registration (Union[bool, Wallet, Address, str, List[Address], List["Wallet"], List[str]]): Wallets or addresses to register. + signers (Union[Wallet, List[Wallet], SigningKey, List[SigningKey]]): The wallets + or keys with which to sign the transaction. + stake_registration (Union[bool, Wallet, Address, str, List[Address], List["Wallet"], List[str]]): + Wallets or addresses to register. delegations (Union[str, dict, PoolKeyHash]): The hash of the pool to delegate to. To delegate to multiple wallets, provide a dict of the form {wallet/address: PoolHash}. - withdrawals (Union[bool, dict]): Set the rewards to withdraw. Set to True to withdraw all rewards from the current wallet. - To withdraw a specific amount from one or more wallets, provide a dict of {wallet/address: Amount}, where Amount is an amount of - Lovelace(), Ada(). Use True or "all" to withdraw all available rewards from each specified wallet. + withdrawals (Union[bool, dict]): Set the rewards to withdraw. Set to True to withdraw + all rewards from the current wallet. To withdraw a specific amount from one or more wallets, + provide a dict of {wallet/address: Amount}, where Amount is an amount of Lovelace(), Ada(). + Use True or "all" to withdraw all available rewards from each specified wallet. change_address (Union["Wallet", Address, str]): The wallet or address to send change to. - merge_change (bool): Whether to merge change into any Output whose address matches the change_address. True by default. + merge_change (bool): Whether to merge change into any Output whose address matches the change_address. + True by default. message (Union[str, List[str]]): A message to include in the transaction. other_metadata (dict): Any other metadata to include in the transaction. submit (bool): Whether to submit the transaction to the network. Defaults to True. From fc54cd0e6bd36ffaa83d6925f0c5251f08e09ecd Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 18:21:05 +0200 Subject: [PATCH 077/130] Merge in utxo fix from remote --- pycardano/wallet.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 0feb0c50..57e15080 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1097,6 +1097,8 @@ def send_ada( if utxos: if isinstance(utxos, UTxO): inputs = [utxos] + else: + inputs = utxos else: inputs = [self] @@ -1166,6 +1168,8 @@ def delegate( if utxos: if isinstance(utxos, UTxO): inputs = [utxos] + else: + inputs = utxos else: inputs = [self] From c30fba131e85a0eab96ffcadf7617142bbe5a6af Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 9 Oct 2022 18:31:19 +0200 Subject: [PATCH 078/130] Add example payment staking keys under new names --- test/resources/keys/payment.stake.skey | 5 +++++ test/resources/keys/payment.stake.vkey | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 test/resources/keys/payment.stake.skey create mode 100644 test/resources/keys/payment.stake.vkey diff --git a/test/resources/keys/payment.stake.skey b/test/resources/keys/payment.stake.skey new file mode 100644 index 00000000..fa6f7096 --- /dev/null +++ b/test/resources/keys/payment.stake.skey @@ -0,0 +1,5 @@ +{ + "type": "StakeSigningKeyShelley_ed25519", + "description": "Stake Signing Key", + "cborHex": "5820ff3a330df8859e4e5f42a97fcaee73f6a00d0cf864f4bca902bd106d423f02c0" +} diff --git a/test/resources/keys/payment.stake.vkey b/test/resources/keys/payment.stake.vkey new file mode 100644 index 00000000..83733cf4 --- /dev/null +++ b/test/resources/keys/payment.stake.vkey @@ -0,0 +1,5 @@ +{ + "type": "StakeVerificationKeyShelley_ed25519", + "description": "Stake Verification Key", + "cborHex": "58205edaa384c658c2bd8945ae389edac0a5bd452d0cfd5d1245e3ecd540030d1e3c" +} From c2c8be8e6bb8484928bfe829fda040e0c0c58217 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 23 Oct 2022 02:41:58 +0200 Subject: [PATCH 079/130] Add last_block_slot to FixedChainContext --- test/pycardano/util.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/pycardano/util.py b/test/pycardano/util.py index 36c65892..edc7b414 100644 --- a/test/pycardano/util.py +++ b/test/pycardano/util.py @@ -92,6 +92,11 @@ def epoch(self) -> int: def slot(self) -> int: """Current slot number""" return 2000 + + @property + def last_block_slot(self) -> int: + """Slot number of last block""" + return 2000 def utxos(self, address: str) -> List[UTxO]: """Get all UTxOs associated with an address. From 4d002b2bbde1c700f2154f380c52ef267ea2d971 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 23 Oct 2022 02:42:32 +0200 Subject: [PATCH 080/130] Remove unnecessary math operators --- pycardano/wallet.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 57e15080..7ed195a3 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -175,8 +175,6 @@ def __add__(self, other): def __radd__(self, other): if isinstance(other, (int, float)): return self.__class__(self.amount + other) - elif isinstance(other, Amount): - return self.__class__(self.amount + other[self._amount_type]) else: raise TypeError("Must compute with a number or another Cardano amount") @@ -190,9 +188,7 @@ def __sub__(self, other): def __rsub__(self, other): if isinstance(other, (int, float)): - return self.__class__(self.amount - other) - elif isinstance(other, Amount): - return self.__class__(self.amount - other[self._amount_type]) + return self.__class__(other - self.amount) else: raise TypeError("Must compute with a number or another Cardano amount") @@ -207,8 +203,6 @@ def __mul__(self, other): def __rmul__(self, other): if isinstance(other, (int, float)): return self.__class__(self.amount * other) - elif isinstance(other, Amount): - return self.__class__(self.amount * other[self._amount_type]) else: raise TypeError("Must compute with a number or another Cardano amount") @@ -230,26 +224,19 @@ def __floordiv__(self, other): def __rtruediv__(self, other): if isinstance(other, (int, float)): - return self.__class__(self.amount / other) - elif isinstance(other, Amount): - return self.__class__(self.amount / other[self._amount_type]) + return self.__class__(other / self.amount) else: raise TypeError("Must compute with a number or another Cardano amount") def __rfloordiv__(self, other): if isinstance(other, (int, float)): - return self.__class__(self.amount // other) - elif isinstance(other, Amount): - return self.__class__(self.amount // other[self._amount_type]) + return self.__class__(other // self.amount) else: raise TypeError("Must compute with a number or another Cardano amount") def __neg__(self): return self.__class__(-self.amount) - def __pos__(self): - return self.__class__(+self.amount) - def __abs__(self): return self.__class__(abs(self.amount)) From 719a5f44d4a4728deb6462364392cbb1d81364f9 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 23 Oct 2022 02:42:56 +0200 Subject: [PATCH 081/130] Create easily testable get_now() method --- pycardano/wallet.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 7ed195a3..31a417ef 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -375,7 +375,7 @@ def get_expiration_timestamp(self, context: ChainContext): if self.expiration_slot: seconds_diff = self.expiration_slot - context.last_block_slot - return datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta( + return get_now(datetime.timezone.utc) + datetime.timedelta( seconds=seconds_diff ) @@ -427,11 +427,11 @@ def generate_minting_policy( must_before_slot = InvalidHereAfter(expiration) elif isinstance(expiration, datetime.datetime): if expiration.tzinfo: - time_until_expiration = expiration - datetime.datetime.now( - datetime.timezone.utc + time_until_expiration = expiration - get_now( + expiration.tzinfo ) else: - time_until_expiration = expiration - datetime.datetime.now() + time_until_expiration = expiration - get_now() last_block_slot = context.last_block_slot @@ -439,7 +439,7 @@ def generate_minting_policy( last_block_slot + int(time_until_expiration.total_seconds()) ) else: - must_before_slot = None + raise TypeError("Expiration must be a datetime or int") # noinspection PyTypeChecker policy = ScriptAll(pub_keys + [must_before_slot]) @@ -1927,6 +1927,17 @@ def wait_for_confirmation( return confirmed +def get_now(tz_info: Optional[datetime.tzinfo] = None) -> datetime.datetime: + """Get the current time. + + Args: + tz_info (Optional[datetime.tzinfo]): The timezone to get the time in. Defaults to None. + + Returns: + datetime.datetime: The current time. + """ + return datetime.datetime.now(tz_info) + # Exceptions class MetadataFormattingException(PyCardanoException): pass From abda6e1bc477c094dd0e6d932b48e7fc84e2b2a2 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 23 Oct 2022 02:43:29 +0200 Subject: [PATCH 082/130] Add coverage for Amount and TokenPolicy --- test/pycardano/test_wallet.py | 279 +++++++++++++++++++++++++++++++--- 1 file changed, 262 insertions(+), 17 deletions(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index 0d8ca4e6..65e6cc6e 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -1,17 +1,21 @@ +import datetime import pathlib -from pycardano.address import Address -from pycardano.nativescript import ScriptAll, ScriptPubkey - -from pycardano.wallet import Ada, Lovelace, Token, Wallet, TokenPolicy +from unittest.mock import patch import pytest +from pycardano.address import Address +from pycardano.nativescript import ScriptAll, ScriptPubkey +from pycardano.wallet import Ada, Lovelace, Token, TokenPolicy, Wallet +from test.pycardano.util import chain_context + def test_load_wallet(): w = Wallet( name="payment", keys_dir=str(pathlib.Path(__file__).parent / "../resources/keys"), + context="null", ) assert w.address == Address.from_primitive( @@ -28,6 +32,7 @@ def test_load_wallet(): WALLET = Wallet( name="payment", keys_dir=str(pathlib.Path(__file__).parent / "../resources/keys"), + context="null", ) @@ -38,16 +43,9 @@ def test_amount(): assert Lovelace(1).as_ada() == Ada(0.000001) assert Ada(1).as_ada() == Ada(1) assert Lovelace(1).as_lovelace() == Lovelace(1) - assert Ada(1) + Ada(1) == Ada(2) - assert Ada(1) - Ada(1) == Ada(0) - assert Lovelace(1) + Lovelace(1) == Lovelace(2) - assert Lovelace(1) - Lovelace(1) == Lovelace(0) - assert Lovelace(1) + Ada(1) == Lovelace(1000001) - assert Lovelace(1000001) - Ada(1) == Lovelace(1) - assert Ada(1) + Lovelace(1) == Ada(1.000001) - assert Ada(1) - Lovelace(1) == Ada(0.999999) assert Ada(1) == Ada(1) assert Lovelace(1) == Lovelace(1) + assert Lovelace(1) == 1 assert Ada(1) != Ada(2) assert Lovelace(1) != Lovelace(2) assert Ada(1) < Ada(2) @@ -62,10 +60,6 @@ def test_amount(): assert Lovelace(1) <= Lovelace(2) assert Ada(2) >= Ada(1) assert Lovelace(2) >= Lovelace(1) - assert Ada(1) * 2 == Ada(2) - assert Lovelace(1) * 2 == Lovelace(2) - assert Ada(1) / 2 == Ada(0.5) - assert Ada(1) / 2 == Lovelace(500000) assert str(Ada(1)) == "1" assert str(Lovelace(1)) == "1" assert bool(Ada(1)) == True @@ -76,6 +70,38 @@ def test_amount(): assert sum([Lovelace(500000), Ada(5)]) == Lovelace(5500000) assert abs(Ada(-1)) == Ada(1) assert abs(Lovelace(-1)) == Lovelace(1) + assert Lovelace(1) != Lovelace(2) + assert Lovelace(1) != 2 + assert Lovelace(2) > 1 + assert Lovelace(1) < 2 + assert Lovelace(1) >= 1 + assert Lovelace(2) <= 2 + assert -Lovelace(5) == Lovelace(-5) + assert -Ada(5) == Ada(-5) + assert round(Ada(5.66)) == Ada(6) + + with pytest.raises(TypeError): + Lovelace(500) == "500" + + with pytest.raises(TypeError): + Lovelace(1) != "1" + + with pytest.raises(TypeError): + Lovelace(1) < "2" + + with pytest.raises(TypeError): + Lovelace(1) > "2" + + with pytest.raises(TypeError): + Lovelace(1) <= "2" + + with pytest.raises(TypeError): + Lovelace(1) >= "2" + + assert int(Lovelace(100)) == 100 + assert int(Ada(100)) == 100 + assert hash(Lovelace(100)) == hash((100, "lovelace")) + assert hash(Ada(100)) == hash((100, "ada")) def test_lovelace_integer(): @@ -85,6 +111,73 @@ def test_lovelace_integer(): Lovelace(5.5) +def test_amount_math(): + """Check that the mathematical properties of Ada and Lovelace are consistent""" + + assert Ada(1) + Ada(1) == Ada(2) + assert Ada(1) - Ada(1) == Ada(0) + assert Ada(1) + 1 == Ada(2) + assert Ada(2) - 1 == Ada(1) + assert 1 + Ada(1) == Ada(2) + assert 2 - Ada(1) == Ada(1) + assert Lovelace(1) + Lovelace(1) == Lovelace(2) + assert Lovelace(1) - Lovelace(1) == Lovelace(0) + assert Lovelace(1) + 1 == Lovelace(2) + assert Lovelace(2) - 1 == Lovelace(1) + assert Lovelace(1) + Ada(1) == Lovelace(1000001) + assert Lovelace(1000001) - Ada(1) == Lovelace(1) + assert Ada(1) + Lovelace(1) == Ada(1.000001) + assert Ada(1) - Lovelace(1) == Ada(0.999999) + assert Ada(5) * Ada(2) == Ada(10) + assert Ada(1) * 2 == Ada(2) + assert 2 * Ada(1) == Ada(2) + assert Lovelace(1) * 2 == Lovelace(2) + assert Ada(6) / Ada(3) == Ada(2) + assert Ada(1) / 2 == Ada(0.5) + assert 1 / Ada(2) == Ada(0.5) + assert Ada(1) / 2 == Lovelace(500000) + assert Ada(5) // Ada(2) == Ada(2) + assert Ada(5) // 2 == Ada(2) + assert 5 // Ada(2) == Ada(2) + + assert sum([Ada(1), Ada(2)]) == Ada(3) + assert sum([Ada(1), 2]) == Ada(3) + assert sum([Ada(2), Lovelace(500000)]) == Ada(2.5) + + with pytest.raises(TypeError): + Ada(1) + "1" + + with pytest.raises(TypeError): + "1" + Ada(1) + + with pytest.raises(TypeError): + Ada(2) - "1" + + with pytest.raises(TypeError): + "1" - Ada(2) + + with pytest.raises(TypeError): + Ada(2) * "2" + + with pytest.raises(TypeError): + Ada(2) / "4" + + with pytest.raises(TypeError): + Ada(2) // "4" + + with pytest.raises(TypeError): + "2" * Ada(2) + + with pytest.raises(TypeError): + "4" / Ada(2) + + with pytest.raises(TypeError): + "4" // Ada(2) + + with pytest.raises(TypeError): + sum([Ada(1), "2"]) + + def test_wallet_sign_data(): assert ( @@ -101,11 +194,13 @@ def test_wallet_sign_data(): ) -def test_policy(): +def test_policy(chain_context): policy_dir = pathlib.Path(__file__).parent / "../resources/policy" script_filepath = policy_dir / f"testToken.script" + if script_filepath.exists(): + script_filepath.unlink() # remove policy file if it exists if script_filepath.exists(): @@ -121,12 +216,162 @@ def test_policy(): policy.policy_id == "6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e0831" ) assert policy.policy == script + assert policy.script == script assert policy.required_signatures == [WALLET.verification_key.hash()] + # load from dictionary + policy_dict = { + "type": "all", + "scripts": [ + { + "type": "sig", + "keyHash": "cc30497f4ff962f4c1dca54cceefe39f86f1d7179668009f8eb71e59", + } + ], + } + + # also test new policy directory + second_policy_dir = pathlib.Path(__file__).parent / "../resources/policy_two" + second_script_filepath = second_policy_dir / f"testToken.script" + if second_script_filepath.exists(): + second_script_filepath.unlink() + + from_dict = TokenPolicy( + name="testTokenDict", policy=policy_dict, policy_dir=str(second_policy_dir) + ) + + assert policy.policy == from_dict.policy + + # test a policy for a token for which we don't have the private key + third_policy_dir = pathlib.Path(__file__).parent / "../resources/policy_three" + their_policy = TokenPolicy( + name="notOurs", + policy="6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e0831", + policy_dir = third_policy_dir + ) + assert their_policy.policy_id == policy.policy_id + assert their_policy.id == policy.id + + # try loading an already existing policy + reloaded_policy = TokenPolicy(name="testToken", policy_dir=str(policy_dir)) + assert reloaded_policy.policy == policy.policy + + # try to generate a policy with a name that already exists + with pytest.raises(FileExistsError): + reloaded_policy.generate_minting_policy(signers=WALLET) + + with pytest.raises(AttributeError): + temp_policy = TokenPolicy( + name="noContext", + policy_dir=str(policy_dir), + ) + temp_policy.generate_minting_policy( + signers=WALLET, expiration=datetime.datetime.now() + ) + + # test policy with expiration + exp_filepath = policy_dir / f"expiring.script" + if exp_filepath.exists(): + exp_filepath.unlink() + + exp_policy = TokenPolicy(name="expiring", policy_dir=str(policy_dir)) + exp_policy.generate_minting_policy(signers=WALLET, expiration=2600) + assert exp_policy.expiration_slot == 2600 + with patch( + "pycardano.wallet.get_now", return_value=datetime.datetime(2022, 1, 1, 0, 0, 0) + ): + assert exp_policy.get_expiration_timestamp( + context=chain_context + ) == datetime.datetime(2022, 1, 1, 0, 10, 0) + assert exp_policy.is_expired(context=chain_context) == False + + # reinitialize the policy with a datetime + if exp_filepath.exists(): + exp_filepath.unlink() + + # with timezone + exp_policy = TokenPolicy(name="expiring", policy_dir=str(policy_dir)) + with patch( + "pycardano.wallet.get_now", + return_value=datetime.datetime( + 2022, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc + ), + ): + exp_policy.generate_minting_policy( + signers=WALLET, + expiration=datetime.datetime( + 2022, 1, 1, 0, 10, 0, tzinfo=datetime.timezone.utc + ), + context=chain_context, + ) + assert exp_policy.get_expiration_timestamp( + context=chain_context + ) == datetime.datetime(2022, 1, 1, 0, 10, 0, tzinfo=datetime.timezone.utc) + + # without timezone (UTC) + if exp_filepath.exists(): + exp_filepath.unlink() + + exp_policy = TokenPolicy(name="expiring", policy_dir=str(policy_dir)) + with patch( + "pycardano.wallet.get_now", + return_value=datetime.datetime( + 2022, 1, 1, 0, 0, 0 + ), + ): + exp_policy.generate_minting_policy( + signers=WALLET, + expiration=datetime.datetime( + 2022, 1, 1, 0, 10, 0 + ), + context=chain_context, + ) + assert exp_policy.get_expiration_timestamp( + context=chain_context + ) == datetime.datetime(2022, 1, 1, 0, 10, 0) + + # test address as signer + if exp_filepath.exists(): + exp_filepath.unlink() + + address_signer = TokenPolicy(name="expiring", policy_dir=str(policy_dir)) + address_signer.generate_minting_policy(signers=WALLET.address) + + # with bad expiration date + if exp_filepath.exists(): + exp_filepath.unlink() + + exp_policy = TokenPolicy(name="expiring", policy_dir=str(policy_dir)) + with pytest.raises(TypeError): + exp_policy.generate_minting_policy(signers=WALLET, expiration=2000.5) + + # test bad signer + if exp_filepath.exists(): + exp_filepath.unlink() + + bad_signer = TokenPolicy(name="expiring", policy_dir=str(policy_dir)) + with pytest.raises(TypeError): + bad_signer.generate_minting_policy(signers="addr1q") + # cleanup if script_filepath.exists(): script_filepath.unlink() + if second_script_filepath.exists(): + second_script_filepath.unlink() + + if exp_filepath.exists(): + exp_filepath.unlink() + + if policy_dir.exists(): + policy_dir.rmdir() + + if second_policy_dir.exists(): + second_policy_dir.rmdir() + + if third_policy_dir.exists(): + second_policy_dir.rmdir() + def test_token(): From e8cf33d16f592b98b67fbf61f8d00664d4cea7b6 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 23 Oct 2022 22:29:52 +0200 Subject: [PATCH 083/130] Make signing a tx optional --- pycardano/wallet.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 31a417ef..9e537f3a 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -427,9 +427,7 @@ def generate_minting_policy( must_before_slot = InvalidHereAfter(expiration) elif isinstance(expiration, datetime.datetime): if expiration.tzinfo: - time_until_expiration = expiration - get_now( - expiration.tzinfo - ) + time_until_expiration = expiration - get_now(expiration.tzinfo) else: time_until_expiration = expiration - get_now() @@ -1348,6 +1346,7 @@ def transact( merge_change: Optional[bool] = True, message: Optional[Union[str, List[str]]] = None, other_metadata=None, + sign: Optional[bool] = False, submit: Optional[bool] = True, await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, @@ -1657,6 +1656,11 @@ def transact( if withdraw: builder.withdrawals = Withdrawals(withdraw) + if not sign: + return builder.build( + change_address=change_address, merge_change=merge_change + ) + if signers: signing_keys = [] for signer in signers: @@ -1938,6 +1942,7 @@ def get_now(tz_info: Optional[datetime.tzinfo] = None) -> datetime.datetime: """ return datetime.datetime.now(tz_info) + # Exceptions class MetadataFormattingException(PyCardanoException): pass From 34d67fe3d41f988710ca08e2f1633c4f474db19a Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 23 Oct 2022 22:30:05 +0200 Subject: [PATCH 084/130] Add tests for token metadata --- test/pycardano/test_wallet.py | 80 +++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index 65e6cc6e..bcc32578 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -1,13 +1,24 @@ import datetime import pathlib +from test.pycardano.util import ( + FixedBlockFrostChainContext, + blockfrost_context, + chain_context, +) from unittest.mock import patch import pytest from pycardano.address import Address from pycardano.nativescript import ScriptAll, ScriptPubkey -from pycardano.wallet import Ada, Lovelace, Token, TokenPolicy, Wallet -from test.pycardano.util import chain_context +from pycardano.wallet import ( + Ada, + Lovelace, + MetadataFormattingException, + Token, + TokenPolicy, + Wallet, +) def test_load_wallet(): @@ -247,7 +258,7 @@ def test_policy(chain_context): their_policy = TokenPolicy( name="notOurs", policy="6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e0831", - policy_dir = third_policy_dir + policy_dir=third_policy_dir, ) assert their_policy.policy_id == policy.policy_id assert their_policy.id == policy.id @@ -307,23 +318,19 @@ def test_policy(chain_context): assert exp_policy.get_expiration_timestamp( context=chain_context ) == datetime.datetime(2022, 1, 1, 0, 10, 0, tzinfo=datetime.timezone.utc) - + # without timezone (UTC) if exp_filepath.exists(): exp_filepath.unlink() - + exp_policy = TokenPolicy(name="expiring", policy_dir=str(policy_dir)) with patch( "pycardano.wallet.get_now", - return_value=datetime.datetime( - 2022, 1, 1, 0, 0, 0 - ), + return_value=datetime.datetime(2022, 1, 1, 0, 0, 0), ): exp_policy.generate_minting_policy( signers=WALLET, - expiration=datetime.datetime( - 2022, 1, 1, 0, 10, 0 - ), + expiration=datetime.datetime(2022, 1, 1, 0, 10, 0), context=chain_context, ) assert exp_policy.get_expiration_timestamp( @@ -333,22 +340,22 @@ def test_policy(chain_context): # test address as signer if exp_filepath.exists(): exp_filepath.unlink() - + address_signer = TokenPolicy(name="expiring", policy_dir=str(policy_dir)) address_signer.generate_minting_policy(signers=WALLET.address) - + # with bad expiration date if exp_filepath.exists(): exp_filepath.unlink() - + exp_policy = TokenPolicy(name="expiring", policy_dir=str(policy_dir)) with pytest.raises(TypeError): exp_policy.generate_minting_policy(signers=WALLET, expiration=2000.5) - + # test bad signer if exp_filepath.exists(): exp_filepath.unlink() - + bad_signer = TokenPolicy(name="expiring", policy_dir=str(policy_dir)) with pytest.raises(TypeError): bad_signer.generate_minting_policy(signers="addr1q") @@ -368,7 +375,7 @@ def test_policy(chain_context): if second_policy_dir.exists(): second_policy_dir.rmdir() - + if third_policy_dir.exists(): second_policy_dir.rmdir() @@ -386,3 +393,42 @@ def test_token(): assert token.hex_name == "74657374546f6b656e" assert token.bytes_name == b"testToken" assert token.policy_id == "6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e0831" + assert str(token) == "testToken" + + # test token errors + with pytest.raises(TypeError): + Token(policy=policy, name="badToken", amount="1") + + +def test_metadata(): + + script = ScriptAll([ScriptPubkey(WALLET.verification_key.hash())]) + + policy = TokenPolicy(name="testToken", policy=script) + + metadata = { + "key_1": "value_1", + "key_2": ["value_2_1", "value_2_2"], + "key_3": {"key_3_1": "value_3_1"}, + } + + _ = Token(policy=policy, name="testToken", amount=1, metadata=metadata) + + # test bad metadata + + long_key = {"a" * 100: "so_long"} + long_value = {"so_long": "a" * 100} + long_string = "a" * 100 + unserializable = {"unserializable": lambda x: x} + + with pytest.raises(MetadataFormattingException): + _ = Token(policy=policy, name="testToken", amount=1, metadata=long_key) + + with pytest.raises(MetadataFormattingException): + _ = Token(policy=policy, name="testToken", amount=1, metadata=long_value) + + with pytest.raises(MetadataFormattingException): + _ = Token(policy=policy, name="testToken", amount=1, metadata=long_string) + + with pytest.raises(MetadataFormattingException): + _ = Token(policy=policy, name="testToken", amount=1, metadata=unserializable) From 5cacd41f334a7c659ae58c7db30eac2fdd354bfc Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 23 Oct 2022 22:39:35 +0200 Subject: [PATCH 085/130] Remove unused imports --- test/pycardano/test_wallet.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index bcc32578..9ecb013c 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -1,10 +1,6 @@ import datetime import pathlib -from test.pycardano.util import ( - FixedBlockFrostChainContext, - blockfrost_context, - chain_context, -) +from test.pycardano.util import chain_context from unittest.mock import patch import pytest From cb8166e249c88c72a3d3a4e1fb77e37d8e6f4119 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 23 Oct 2022 22:40:01 +0200 Subject: [PATCH 086/130] Return objects of wallet key hashes instead of str --- pycardano/wallet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 9e537f3a..d4be8523 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -760,11 +760,11 @@ def stake_address(self): @property def verification_key_hash(self): - return str(self.address.payment_part) + return self.address.payment_part @property def stake_verification_key_hash(self): - return str(self.address.staking_part) + return self.address.staking_part @property def tokens(self): From 6c2b4408781627a2d39b31ad6c69461866747aac Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 30 Oct 2022 13:21:24 +0100 Subject: [PATCH 087/130] Only add token policies if not already in tx --- pycardano/wallet.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index d4be8523..185288dc 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1442,10 +1442,11 @@ def transact( if not mints_dict.get(policy_hash): mints_dict[policy_hash] = {} - if isinstance(token.policy, NativeScript): - native_scripts.append(token.policy) - else: - native_scripts.append(token.policy.policy) + if token.policy.policy not in native_scripts: + if isinstance(token.policy, NativeScript): + native_scripts.append(token.policy) + else: + native_scripts.append(token.policy.policy) mints_dict[policy_hash][token.name] = token if token.metadata and token.amount > 0: From 671adc68428905d816edacaadf20e2f3bfd746b1 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 30 Oct 2022 13:35:07 +0100 Subject: [PATCH 088/130] Fix bug in blockfrost_only function wrapper --- pycardano/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 185288dc..72b178dc 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -54,7 +54,7 @@ def wrapper(*args, **kwargs): if isinstance(kwargs.get("context"), BlockFrostChainContext) or isinstance( args[1], BlockFrostChainContext ): - func(*args, **kwargs) + return func(*args, **kwargs) else: raise TypeError( f"Function {func.__name__} is only available for context of type BlockFrostChainContext." From 5b4e026ff27c3237cfe48fb2cbfe5d4a1f89ecbe Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 30 Oct 2022 13:40:05 +0100 Subject: [PATCH 089/130] Replace 'sign' transaction parameter with 'build_only' --- pycardano/wallet.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 72b178dc..d90810c4 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1346,7 +1346,7 @@ def transact( merge_change: Optional[bool] = True, message: Optional[Union[str, List[str]]] = None, other_metadata=None, - sign: Optional[bool] = False, + build_only: Optional[bool] = False, submit: Optional[bool] = True, await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, @@ -1375,6 +1375,7 @@ def transact( True by default. message (Union[str, List[str]]): A message to include in the transaction. other_metadata (dict): Any other metadata to include in the transaction. + build_only (bool): Whether to only build the transaction and not submit it. Returns a built transaction. submit (bool): Whether to submit the transaction to the network. Defaults to True. If False, return the signed transaction CBOR. await_confirmation (bool): Whether to wait for the transaction to be confirmed. Defaults to False. @@ -1382,6 +1383,8 @@ def transact( Returns: str: The transaction ID if submit is True, otherwise the signed transaction CBOR. + OR + pycardano.transaction.TransactionBody: A built but unsigned transaction body, if build_only is True. """ # streamline inputs @@ -1657,7 +1660,7 @@ def transact( if withdraw: builder.withdrawals = Withdrawals(withdraw) - if not sign: + if build_only: return builder.build( change_address=change_address, merge_change=merge_change ) From a71bf4e5ae85bdada0a66a0189bba2cef649e7de Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 5 Nov 2022 00:08:51 +0100 Subject: [PATCH 090/130] Add change address option to token burning --- pycardano/wallet.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index d90810c4..7157d684 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1266,6 +1266,7 @@ def mint_tokens( def burn_tokens( self, tokens: Union[Token, List[Token]], + change_address: Union[Address, str] = None, amount: Optional[Union[Ada, Lovelace]] = None, utxos: Optional[Union[UTxO, List[UTxO]]] = None, **kwargs, @@ -1274,9 +1275,9 @@ def burn_tokens( Same as mint_tokens but automatically sets Token class amount to a negative value. Args: - to (Union[str, Address]): The address to which to send the newly minted tokens. - mints (Union[Token, List[Token]]): The token(s) to mint/burn. + tokens (Union[Token, List[Token]]): The token(s) to burn. Set metadata and quantity directly in the Token class. + change_address (Union[str, Address]): The address to which to send the change. amount (Optional[Union[Ada, Lovelace]]): The amount of Ada to attach to the transaction. If not provided, the minimum amount will be calculated automatically. utxos (Optional[Union[UTxO, List[UTxO]]]): The UTxO(s) to use as inputs. @@ -1313,7 +1314,7 @@ def burn_tokens( return self.transact( inputs=inputs, - outputs=Output(self, amount, tokens=tokens), + outputs=Output(change_address, amount, tokens=tokens), mints=tokens, **kwargs, ) From 8574e05bee0f6c4027bbf8a13362a646334fae66 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 5 Nov 2022 00:09:27 +0100 Subject: [PATCH 091/130] Add an example script which automatically mints a CIP-27 royalty token --- examples/automatic_royalty_token.py | 196 ++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 examples/automatic_royalty_token.py diff --git a/examples/automatic_royalty_token.py b/examples/automatic_royalty_token.py new file mode 100644 index 00000000..037b1ac0 --- /dev/null +++ b/examples/automatic_royalty_token.py @@ -0,0 +1,196 @@ +import datetime as dt +import logging +import time +from pprint import pformat + +from pycardano import wallet + +# This script mints and burns an NFT royalty token based off of CIP-0027 (https://github.com/cardano-foundation/CIPs/tree/master/CIP-0027) + +# 1. Make sure to create a wallet for the policy, with name POLICY_WALLET_NAME +# 2. Create your policy with the name POLICY_NAME and the above wallet as a signer +# 3. Fill out the details below and run the script, make sure your BlockFrost ENV variables are set. + +POLICY_NAME = "myPolicy" +POLICY_WALLET_NAME = "myPolicyWallet" + +ROYALTY_ADDRESS = "addr_test1qpxh0m34vqkzsaucxx6venpnetgay6ylacuwvrdfdv5wnmw34uylg63pcm2dmsjzx8rrndy0lhwhht2h9f0kt8kv2qrswzxgy0" +ROYALTY_PERCENT = "0.05" + +WALLET_NAME = "royaltytest" # This is a temporary wallet generated by the script to mint the token. +CODED_AMOUNT = wallet.Ada(3.555432) # pick a random amount between 3 and 4 ADA +NETWORK = "preprod" + +# Set up logging! +root = logging.getLogger() +root.setLevel(logging.DEBUG) + +logging_timestamp = dt.datetime.now(tz=dt.timezone.utc).strftime("%Y-%m-%d-%H-%M-%S") +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s", + datefmt="%m-%d %H:%M", + filename=f"./{WALLET_NAME}.log", + filemode="a", +) + +fh = logging.FileHandler(filename=f"./{WALLET_NAME}.log") +fh.name = "File Logger" +fh.setLevel(logging.DEBUG) +root.addHandler(fh) + +# define a Handler which writes INFO messages or higher to the sys.stderr +console = logging.StreamHandler() +console.name = "Console Logger" +console.setLevel(logging.INFO) +# set a format which is simpler for console use +formatter = logging.Formatter("%(name)-12s: %(levelname)-8s %(message)s") +# tell the handler to use this format +console.setFormatter(formatter) +# add the handler to the root logger +root.addHandler(console) + + +def chunkstring(string, length): + return (string[0 + i : length + i] for i in range(0, len(string), length)) + + +def create_royalty_metadata(royalty_address: str, royalty_percent: str): + """Write royalty metadata to a file""" + + metadata = {"777": {}} + metadata["777"] = {} + + # add rate + metadata["777"]["rate"] = royalty_percent + + # add address, and split it longer than 64 characters + if len(royalty_address) > 64: + metadata["777"]["addr"] = list(chunkstring(royalty_address, 64)) + else: + metadata["777"]["addr"] = royalty_address + + return metadata + + +def launch(): + + logger = logging.getLogger(__name__) + + logger.info("Welcome to the royalty NFT generation script!") + logger.info(f"Network: {NETWORK}") + + ORIGINAL_SENDER = None + DONE = False + + # generate royalty metadata + royalty_metadata = create_royalty_metadata(ROYALTY_ADDRESS, ROYALTY_PERCENT) + + # create receiving wallet + tmp_wallet = wallet.Wallet(name="tmp_royalty_wallet", network=NETWORK) + policy_wallet = wallet.Wallet(name=POLICY_WALLET_NAME, network=NETWORK) + policy = wallet.TokenPolicy(name=POLICY_NAME) + + logger.info(f"Generating a 777 royalty NFT with {ROYALTY_PERCENT}/1 ({float(ROYALTY_PERCENT)*100}%) to address {ROYALTY_ADDRESS}") + logger.info("Metadata:") + logger.info(pformat(royalty_metadata)) + time.sleep(2) + + logger.info(f"If this looks right, please send exactly {CODED_AMOUNT.ada} ADA to\n {tmp_wallet.address}") + time.sleep(2) + + while not DONE: + + loop_start_time = dt.datetime.now(dt.timezone.utc) + logger.info(f"Starting loop {loop_start_time}") + + tmp_wallet.sync() + tmp_wallet.get_utxo_creators() + + for utxo in tmp_wallet.utxos: + + # check whether or not to mint + can_mint = False + if wallet.Lovelace(utxo.output.amount.coin) == CODED_AMOUNT: + logger.info(f"Coded amount of {CODED_AMOUNT.ada} ADA recieved: can mint 777 token!") + can_mint = True + else: + logger.info(f"Please send exactly {CODED_AMOUNT.ada} ADA to\n {tmp_wallet.address}") + + if can_mint: + + ORIGINAL_SENDER = utxo.creator + logger.info(f"Original sender of {CODED_AMOUNT.ada} ADA is {ORIGINAL_SENDER}") + + token = wallet.Token( + policy=policy, + amount=1, + name="", + metadata=royalty_metadata + ) + + logger.info("Minting token, please wait for confirmation...") + + mint_tx = tmp_wallet.mint_tokens( + to=tmp_wallet, + mints=token, + utxos=utxo, + signers=[tmp_wallet, policy_wallet], + await_confirmation=True, + ) + + + logger.info(f"Mint successful: Tx ID {mint_tx}") + logger.info("DO NOT STOP SCRIPT YET! Please wait so we can burn the token.") + + continue + + # check if we can burn the token + can_burn = False + utxo_tokens = utxo.output.amount.multi_asset + + if utxo_tokens: + if len(utxo_tokens) == 1 and str(list(utxo_tokens.keys())[0]) == policy.id: + logger.info(f"No name token found: can burn 777 token!") + logger.info(f"Will send change to original sender: {ORIGINAL_SENDER}") + can_burn = True + + if can_burn: + + # get original sender + utxo_info = tmp_wallet.context.api.transaction_utxos(str(utxo.input.transaction_id)) + input_utxo = utxo_info.inputs[0].tx_hash + ORIGINAL_SENDER = tmp_wallet.context.api.transaction_utxos(str(input_utxo)).inputs[0].address + + token = wallet.Token( + policy=policy, + amount=-1, + name="", + ) + + logger.info("Burning the royalty token. Please wait for confirmation...") + burn_tx = tmp_wallet.mint_tokens( + to=ORIGINAL_SENDER, + mints=token, + signers=[tmp_wallet, policy_wallet], + utxos=utxo, + change_address=ORIGINAL_SENDER, + await_confirmation=True, + ) + + logger.info(f"Burn successful! Tx ID: {burn_tx}") + + DONE = True + + continue + + time.sleep(5) + + + logger.info("Your royalties are ready!") + logger.info(f"https://cardanoscan.io/tokenPolicy/{policy.id}") + + +if __name__ == "__main__": + + launch() From cef553548b7117985b1ac2fe39d3dbcd291252f7 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 5 Nov 2022 14:08:19 +0100 Subject: [PATCH 092/130] Increase coverage of TokenPolicy and Token classes --- pycardano/wallet.py | 101 ++++++++++++++++------------------ test/pycardano/test_wallet.py | 43 +++++++++++---- 2 files changed, 80 insertions(+), 64 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 7157d684..2a72baa8 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -290,12 +290,15 @@ class TokenPolicy: Args: name (str): The name of the token policy, used for saving and loading keys. - policy (Optional[Union[NativeScript, dict, str]]): Direct provide a policy to use. + policy_id (str): The policy ID of the token. If script is provided, + this will be ignored and automatically generated. + script (Optional[Union[NativeScript, dict]]): Direct provide a policy script to use. policy_dir (Optional[Union[str, Path]]): The directory to save and load the policy from. """ name: str - policy: Optional[Union[NativeScript, dict, str]] = field(repr=False, default=None) + policy_id: Optional[str] = (None,) + script: Optional[Union[NativeScript, dict]] = field(repr=False, default=None) policy_dir: Optional[Union[str, Path]] = field( repr=False, default=Path("./priv/policies") ) @@ -306,10 +309,12 @@ def __post_init__(self): if isinstance(self.policy_dir, str): self.policy_dir = Path(self.policy_dir) - # if native script is directly provide, stop there - if self.policy: - if isinstance(self.policy, dict): - self.policy = NativeScript.from_dict(self.policy) + # if native script is directly provided, stop there + if self.script: + if isinstance(self.script, dict): + self.script = NativeScript.from_dict(self.script) + + self.policy_id = str(self.script.hash()) else: if not self.policy_dir.exists(): self.policy_dir.mkdir(parents=True, exist_ok=True) @@ -319,23 +324,9 @@ def __post_init__(self): with open( Path(self.policy_dir / f"{self.name}.script"), "r" ) as policy_file: - self.policy = NativeScript.from_dict(json.load(policy_file)) - - @property - def policy_id(self): + self.script = NativeScript.from_dict(json.load(policy_file)) - if not isinstance(self.policy, str): - return str(self.policy.hash()) - else: - return self.policy - - @property - def id(self): - return self.policy_id - - @property - def script(self): - return self.policy + self.policy_id = str(self.script.hash()) @property def expiration_slot(self): @@ -343,27 +334,35 @@ def expiration_slot(self): like one generated by generate_minting_policy """ - if not isinstance(self.policy, str): - scripts = getattr(self.policy, "native_scripts", None) + if not self.script: + raise TypeError("The script of this policy is not set.") + + scripts = getattr(self.script, "native_scripts", []) + + exp = None + for script in scripts: + if script._TYPE == 5: + exp = script.after + + if not exp: + raise TypeError("This policy does not have an expiration slot.") - if scripts: - for script in scripts: - if script._TYPE == 5: - return script.after + return exp @property def required_signatures(self): """List the public key hashes of all required signers""" + if not self.script: + raise TypeError("The script of this policy is not set.") + required_signatures = [] - if not isinstance(self.policy, str): - scripts = getattr(self.policy, "native_scripts", None) + scripts = getattr(self.script, "native_scripts", []) - if scripts: - for script in scripts: - if script._TYPE == 0: - required_signatures.append(script.key_hash) + for script in scripts: + if script._TYPE == 0: + required_signatures.append(script.key_hash) return required_signatures @@ -372,22 +371,18 @@ def get_expiration_timestamp(self, context: ChainContext): like one generated by generate_minting_policy """ - if self.expiration_slot: - seconds_diff = self.expiration_slot - context.last_block_slot + seconds_diff = self.expiration_slot - context.last_block_slot - return get_now(datetime.timezone.utc) + datetime.timedelta( - seconds=seconds_diff - ) + return get_now(datetime.timezone.utc) + datetime.timedelta(seconds=seconds_diff) def is_expired(self, context: ChainContext): """Get the expiration timestamp for a simple minting policy, like one generated by generate_minting_policy """ - if self.expiration_slot: - seconds_diff = self.expiration_slot - context.last_block_slot + seconds_diff = self.expiration_slot - context.last_block_slot - return seconds_diff < 0 + return seconds_diff < 0 def generate_minting_policy( self, @@ -407,7 +402,7 @@ def generate_minting_policy( script_filepath = Path(self.policy_dir / f"{self.name}.script") - if script_filepath.exists() or self.policy: + if script_filepath.exists() or self.script: raise FileExistsError(f"Policy named {self.name} already exists") if isinstance(expiration, datetime.datetime) and not context: @@ -449,7 +444,8 @@ def generate_minting_policy( with open(script_filepath, "w") as script_file: json.dump(policy.to_dict(), script_file, indent=4) - self.policy = policy + self.script = policy + self.policy_id = str(policy.hash()) @staticmethod def _get_pub_key_hash(signer: Union["Wallet", Address]): @@ -487,10 +483,9 @@ def __post_init__(self): raise TypeError("Expected token amount to be of type: integer.") if self.hex_name: - if isinstance(self.hex_name, str): - self.name = bytes.fromhex(self.hex_name).decode("utf-8") + self.name = bytes.fromhex(self.hex_name).decode("utf-8") - elif isinstance(self.name, str): + else: self.hex_name = bytes(self.name.encode("utf-8")).hex() self._check_metadata(to_check=self.metadata, top_level=True) @@ -530,7 +525,7 @@ def _check_metadata( for item in to_check: self._check_metadata(to_check=item) - elif isinstance(to_check, str): + else: # must be a string if len(to_check) > 64: raise MetadataFormattingException( f"Metadata field is too long (> 64 characters): {to_check}\nConsider splitting into an array of " @@ -566,7 +561,7 @@ def get_on_chain_metadata(self, context: ChainContext) -> dict: try: metadata = context.api.asset( - self.policy.id + self.hex_name + self.policy.policy_id + self.hex_name ).onchain_metadata.to_dict() except ApiError as e: logger.error( @@ -902,7 +897,7 @@ def _get_tokens(self): my_policies = {} if self.verification_key: my_policies = { - policy.id: policy + policy.policy_id: policy for policy in get_all_policies(self.keys_dir / "policies") if self.verification_key.hash() in policy.required_signatures } @@ -1384,7 +1379,7 @@ def transact( Returns: str: The transaction ID if submit is True, otherwise the signed transaction CBOR. - OR + OR pycardano.transaction.TransactionBody: A built but unsigned transaction body, if build_only is True. """ @@ -1446,11 +1441,11 @@ def transact( if not mints_dict.get(policy_hash): mints_dict[policy_hash] = {} - if token.policy.policy not in native_scripts: + if token.policy.script not in native_scripts: if isinstance(token.policy, NativeScript): native_scripts.append(token.policy) else: - native_scripts.append(token.policy.policy) + native_scripts.append(token.policy.script) mints_dict[policy_hash][token.name] = token if token.metadata and token.amount > 0: diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index 9ecb013c..3d71e07a 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -213,7 +213,9 @@ def test_policy(chain_context): if script_filepath.exists(): script_filepath.unlink() - policy = TokenPolicy(name="testToken", policy_dir=str(policy_dir)) + policy = TokenPolicy(name="testToken", policy_dir=policy_dir) + + assert policy.policy_dir.exists() policy.generate_minting_policy(signers=WALLET) @@ -222,7 +224,7 @@ def test_policy(chain_context): assert ( policy.policy_id == "6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e0831" ) - assert policy.policy == script + assert policy.script == script assert policy.required_signatures == [WALLET.verification_key.hash()] @@ -244,24 +246,24 @@ def test_policy(chain_context): second_script_filepath.unlink() from_dict = TokenPolicy( - name="testTokenDict", policy=policy_dict, policy_dir=str(second_policy_dir) + name="testTokenDict", script=policy_dict, policy_dir=str(second_policy_dir) ) - assert policy.policy == from_dict.policy + assert policy.script == from_dict.script # test a policy for a token for which we don't have the private key third_policy_dir = pathlib.Path(__file__).parent / "../resources/policy_three" their_policy = TokenPolicy( name="notOurs", - policy="6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e0831", + policy_id="6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e0831", policy_dir=third_policy_dir, ) assert their_policy.policy_id == policy.policy_id - assert their_policy.id == policy.id # try loading an already existing policy reloaded_policy = TokenPolicy(name="testToken", policy_dir=str(policy_dir)) - assert reloaded_policy.policy == policy.policy + print(reloaded_policy.policy_id, policy.policy_id) + assert reloaded_policy.script == policy.script # try to generate a policy with a name that already exists with pytest.raises(FileExistsError): @@ -282,8 +284,27 @@ def test_policy(chain_context): exp_filepath.unlink() exp_policy = TokenPolicy(name="expiring", policy_dir=str(policy_dir)) - exp_policy.generate_minting_policy(signers=WALLET, expiration=2600) + exp_policy.generate_minting_policy(signers=[WALLET], expiration=2600) assert exp_policy.expiration_slot == 2600 + assert policy.required_signatures == [WALLET.verification_key.hash()] + + # test a policy with no provided script + with pytest.raises(TypeError): + their_policy.expiration_slot + + with pytest.raises(TypeError): + their_policy.get_expiration_timestamp() + + with pytest.raises(TypeError): + their_policy.is_expired() + + with pytest.raises(TypeError): + their_policy.required_signatures + + # test a policy with no expiration slot + with pytest.raises(TypeError): + policy.expiration_slot + with patch( "pycardano.wallet.get_now", return_value=datetime.datetime(2022, 1, 1, 0, 0, 0) ): @@ -373,14 +394,14 @@ def test_policy(chain_context): second_policy_dir.rmdir() if third_policy_dir.exists(): - second_policy_dir.rmdir() + third_policy_dir.rmdir() def test_token(): script = ScriptAll([ScriptPubkey(WALLET.verification_key.hash())]) - policy = TokenPolicy(name="testToken", policy=script) + policy = TokenPolicy(name="testToken", script=script) token = Token(policy=policy, name="testToken", amount=1) token_hex = Token(policy=policy, hex_name="74657374546f6b656e", amount=1) @@ -400,7 +421,7 @@ def test_metadata(): script = ScriptAll([ScriptPubkey(WALLET.verification_key.hash())]) - policy = TokenPolicy(name="testToken", policy=script) + policy = TokenPolicy(name="testToken", script=script) metadata = { "key_1": "value_1", From 3f902ccfb4c74c2675e2d65499bd8668fa39afb1 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 5 Nov 2022 14:08:36 +0100 Subject: [PATCH 093/130] Format automatic royalty example --- examples/automatic_royalty_token.py | 108 ++++++++++++++++------------ 1 file changed, 64 insertions(+), 44 deletions(-) diff --git a/examples/automatic_royalty_token.py b/examples/automatic_royalty_token.py index 037b1ac0..6c63a6be 100644 --- a/examples/automatic_royalty_token.py +++ b/examples/automatic_royalty_token.py @@ -90,47 +90,54 @@ def launch(): tmp_wallet = wallet.Wallet(name="tmp_royalty_wallet", network=NETWORK) policy_wallet = wallet.Wallet(name=POLICY_WALLET_NAME, network=NETWORK) policy = wallet.TokenPolicy(name=POLICY_NAME) - - logger.info(f"Generating a 777 royalty NFT with {ROYALTY_PERCENT}/1 ({float(ROYALTY_PERCENT)*100}%) to address {ROYALTY_ADDRESS}") + + logger.info( + f"Generating a 777 royalty NFT with {ROYALTY_PERCENT}/1 ({float(ROYALTY_PERCENT)*100}%) to address {ROYALTY_ADDRESS}" + ) logger.info("Metadata:") logger.info(pformat(royalty_metadata)) time.sleep(2) - logger.info(f"If this looks right, please send exactly {CODED_AMOUNT.ada} ADA to\n {tmp_wallet.address}") + logger.info( + f"If this looks right, please send exactly {CODED_AMOUNT.ada} ADA to\n {tmp_wallet.address}" + ) time.sleep(2) while not DONE: - + loop_start_time = dt.datetime.now(dt.timezone.utc) logger.info(f"Starting loop {loop_start_time}") - + tmp_wallet.sync() tmp_wallet.get_utxo_creators() - + for utxo in tmp_wallet.utxos: - + # check whether or not to mint can_mint = False if wallet.Lovelace(utxo.output.amount.coin) == CODED_AMOUNT: - logger.info(f"Coded amount of {CODED_AMOUNT.ada} ADA recieved: can mint 777 token!") + logger.info( + f"Coded amount of {CODED_AMOUNT.ada} ADA recieved: can mint 777 token!" + ) can_mint = True else: - logger.info(f"Please send exactly {CODED_AMOUNT.ada} ADA to\n {tmp_wallet.address}") - + logger.info( + f"Please send exactly {CODED_AMOUNT.ada} ADA to\n {tmp_wallet.address}" + ) + if can_mint: - + ORIGINAL_SENDER = utxo.creator - logger.info(f"Original sender of {CODED_AMOUNT.ada} ADA is {ORIGINAL_SENDER}") - + logger.info( + f"Original sender of {CODED_AMOUNT.ada} ADA is {ORIGINAL_SENDER}" + ) + token = wallet.Token( - policy=policy, - amount=1, - name="", - metadata=royalty_metadata + policy=policy, amount=1, name="", metadata=royalty_metadata ) - + logger.info("Minting token, please wait for confirmation...") - + mint_tx = tmp_wallet.mint_tokens( to=tmp_wallet, mints=token, @@ -138,37 +145,51 @@ def launch(): signers=[tmp_wallet, policy_wallet], await_confirmation=True, ) - - + logger.info(f"Mint successful: Tx ID {mint_tx}") - logger.info("DO NOT STOP SCRIPT YET! Please wait so we can burn the token.") - + logger.info( + "DO NOT STOP SCRIPT YET! Please wait so we can burn the token." + ) + continue - + # check if we can burn the token can_burn = False utxo_tokens = utxo.output.amount.multi_asset - + if utxo_tokens: - if len(utxo_tokens) == 1 and str(list(utxo_tokens.keys())[0]) == policy.id: - logger.info(f"No name token found: can burn 777 token!") - logger.info(f"Will send change to original sender: {ORIGINAL_SENDER}") - can_burn = True - + if ( + len(utxo_tokens) == 1 + and str(list(utxo_tokens.keys())[0]) == policy.id + ): + logger.info(f"No name token found: can burn 777 token!") + logger.info( + f"Will send change to original sender: {ORIGINAL_SENDER}" + ) + can_burn = True + if can_burn: - + # get original sender - utxo_info = tmp_wallet.context.api.transaction_utxos(str(utxo.input.transaction_id)) + utxo_info = tmp_wallet.context.api.transaction_utxos( + str(utxo.input.transaction_id) + ) input_utxo = utxo_info.inputs[0].tx_hash - ORIGINAL_SENDER = tmp_wallet.context.api.transaction_utxos(str(input_utxo)).inputs[0].address - + ORIGINAL_SENDER = ( + tmp_wallet.context.api.transaction_utxos(str(input_utxo)) + .inputs[0] + .address + ) + token = wallet.Token( policy=policy, amount=-1, - name="", + name="", + ) + + logger.info( + "Burning the royalty token. Please wait for confirmation..." ) - - logger.info("Burning the royalty token. Please wait for confirmation...") burn_tx = tmp_wallet.mint_tokens( to=ORIGINAL_SENDER, mints=token, @@ -177,19 +198,18 @@ def launch(): change_address=ORIGINAL_SENDER, await_confirmation=True, ) - + logger.info(f"Burn successful! Tx ID: {burn_tx}") - + DONE = True - + continue - + time.sleep(5) - - + logger.info("Your royalties are ready!") logger.info(f"https://cardanoscan.io/tokenPolicy/{policy.id}") - + if __name__ == "__main__": From ba1ec7d3953226cdff992f322094efbffd1fdd47 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 5 Nov 2022 14:35:14 +0100 Subject: [PATCH 094/130] Remove manual typecheck in Output class --- pycardano/wallet.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 2a72baa8..d6891fb8 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -592,16 +592,6 @@ class Output: def __post_init__(self): - if ( - not isinstance(self.amount, Ada) - and not isinstance(self.amount, Lovelace) - and not isinstance(self.amount, int) - ): - raise TypeError( - "Please provide amount as either `Ada(amount)` or `Lovelace(amount)`.", - "Otherwise provide lovelace as an integer.", - ) - if isinstance(self.amount, int): self.amount = Lovelace(self.amount) From 5a5514ab4be775d64ccc8643c991ad8e32e02e56 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 5 Nov 2022 14:35:23 +0100 Subject: [PATCH 095/130] Add tests for Output class --- test/pycardano/test_wallet.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index 3d71e07a..6f1df2cb 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -11,6 +11,7 @@ Ada, Lovelace, MetadataFormattingException, + Output, Token, TokenPolicy, Wallet, @@ -449,3 +450,29 @@ def test_metadata(): with pytest.raises(MetadataFormattingException): _ = Token(policy=policy, name="testToken", amount=1, metadata=unserializable) + + # TODO: add tests for onchain metadata + + +def test_outputs(): + + output1 = Output(address=WALLET, amount=5000000) + output2 = Output(address=WALLET, amount=Lovelace(5000000)) + output3 = Output(address=WALLET, amount=Ada(5)) + output4 = Output(address=WALLET.address, amount=Ada(5)) + output5 = Output(address=str(WALLET.address), amount=Ada(5)) + + assert output1 == output2 == output3 == output4 == output5 + + # test outputs with tokens + script = ScriptAll([ScriptPubkey(WALLET.verification_key.hash())]) + + policy = TokenPolicy(name="testToken", script=script) + + tokens = [ + Token(policy=policy, name="testToken", amount=1), + Token(policy=policy, name="testToken2", amount=1, metadata={"key": "value"}), + ] + + output_token1 = Output(address=WALLET, amount=Ada(5), tokens=tokens[0]) + output_token2 = Output(address=WALLET, amount=Ada(0), tokens=tokens) From bbb77f795d81d262281ae2f4c178979ae625e97b Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 5 Nov 2022 14:51:06 +0100 Subject: [PATCH 096/130] Add test for wallet without stake --- test/pycardano/test_wallet.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index 6f1df2cb..4852a77d 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -35,6 +35,20 @@ def test_load_wallet(): assert w.stake_address == Address.from_primitive( "stake1u9yz3gk6mw5he20apnwfn96cn9rscgvmmsxc9r86dh0k66ghyrkpw" ) + + # check that no stake address is loaded when use_stake is False + w = Wallet( + name="payment", + keys_dir=str(pathlib.Path(__file__).parent / "../resources/keys"), + context="null", + use_stake=False, + ) + + assert w.payment_address == Address.from_primitive( + "addr1v8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3ukgqdsn8w" + ) + + assert w.stake_address is None WALLET = Wallet( From bb4bda38e530a8e44d1d13a232128f822582d2c8 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 5 Nov 2022 14:51:29 +0100 Subject: [PATCH 097/130] Remove unneeded if statement --- pycardano/wallet.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index d6891fb8..66154f6b 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -695,11 +695,10 @@ def __post_init__(self): # otherwise derive the network from the address provided else: # check that the given address matches the desired network - if self._network: - if self.address.network != self._network: - raise ValueError( - f"{self._network} does not match the network of the provided address." - ) + if self.address.network != self._network: + raise ValueError( + f"{self._network} does not match the network of the provided address." + ) self._network = self.address.network self.signing_key = None From 6e12a8ccf8f27fc77207d4dd7d0be6c137de34b9 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 5 Nov 2022 14:51:36 +0100 Subject: [PATCH 098/130] Add tests for wallet init --- test/pycardano/test_wallet.py | 38 ++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index 4852a77d..6b129b93 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -5,7 +5,7 @@ import pytest -from pycardano.address import Address +from pycardano.address import Address, VerificationKeyHash from pycardano.nativescript import ScriptAll, ScriptPubkey from pycardano.wallet import ( Ada, @@ -490,3 +490,39 @@ def test_outputs(): output_token1 = Output(address=WALLET, amount=Ada(5), tokens=tokens[0]) output_token2 = Output(address=WALLET, amount=Ada(0), tokens=tokens) + + +def test_wallet_init(): + + keys_dir = str(pathlib.Path(__file__).parent / "../resources/keys") + + wallet = Wallet( + name="payment", + keys_dir=keys_dir, + context="null", + ) + + not_my_wallet = Wallet( + name="theirs", + address="addr1q8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3uk2g9z3d4kaf0j5l6rxunxt43x28pssehhqds2x05mwld45s399sr7", + ) + + # try different networks + wallet_preprod = Wallet(name="payment", network="preprod", keys_dir=keys_dir) + wallet_preview = Wallet(name="payment", network="preview", keys_dir=keys_dir) + wallet_testnet = Wallet(name="payment", network="testnet", keys_dir=keys_dir) + + assert wallet_preprod.address == wallet_preview.address + + with pytest.raises(ValueError): + Wallet( + name="bad", + address="addr1q8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3uk2g9z3d4kaf0j5l6rxunxt43x28pssehhqds2x05mwld45s399sr7", + network="preprod", + keys_dir=keys_dir, + ) + + print(wallet.verification_key_hash) + print(wallet.stake_verification_key_hash) + assert wallet.verification_key_hash == VerificationKeyHash.from_primitive("cc30497f4ff962f4c1dca54cceefe39f86f1d7179668009f8eb71e59") + assert wallet.stake_verification_key_hash == VerificationKeyHash.from_primitive("4828a2dadba97ca9fd0cdc99975899470c219bdc0d828cfa6ddf6d69") From 6dff7fc314d3e2664e1d932d7969fbef50a77b75 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sat, 5 Nov 2022 14:51:55 +0100 Subject: [PATCH 099/130] Format code --- test/pycardano/test_wallet.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index 6b129b93..a11cfd97 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -35,7 +35,7 @@ def test_load_wallet(): assert w.stake_address == Address.from_primitive( "stake1u9yz3gk6mw5he20apnwfn96cn9rscgvmmsxc9r86dh0k66ghyrkpw" ) - + # check that no stake address is loaded when use_stake is False w = Wallet( name="payment", @@ -43,11 +43,11 @@ def test_load_wallet(): context="null", use_stake=False, ) - + assert w.payment_address == Address.from_primitive( "addr1v8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3ukgqdsn8w" ) - + assert w.stake_address is None @@ -524,5 +524,9 @@ def test_wallet_init(): print(wallet.verification_key_hash) print(wallet.stake_verification_key_hash) - assert wallet.verification_key_hash == VerificationKeyHash.from_primitive("cc30497f4ff962f4c1dca54cceefe39f86f1d7179668009f8eb71e59") - assert wallet.stake_verification_key_hash == VerificationKeyHash.from_primitive("4828a2dadba97ca9fd0cdc99975899470c219bdc0d828cfa6ddf6d69") + assert wallet.verification_key_hash == VerificationKeyHash.from_primitive( + "cc30497f4ff962f4c1dca54cceefe39f86f1d7179668009f8eb71e59" + ) + assert wallet.stake_verification_key_hash == VerificationKeyHash.from_primitive( + "4828a2dadba97ca9fd0cdc99975899470c219bdc0d828cfa6ddf6d69" + ) From 8495eb82f7eaafacd49ecbff735119c1e171c7c6 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 13 Nov 2022 13:52:02 +0100 Subject: [PATCH 100/130] Remove blockfrost_only decorator in favor of strict typechecking --- pycardano/wallet.py | 47 +++++++++++++++------------------------------ 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 66154f6b..38894ba1 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -47,22 +47,6 @@ from pycardano.utils import min_lovelace -# function wrappers -def blockfrost_only(func): - @wraps(func) - def wrapper(*args, **kwargs): - if isinstance(kwargs.get("context"), BlockFrostChainContext) or isinstance( - args[1], BlockFrostChainContext - ): - return func(*args, **kwargs) - else: - raise TypeError( - f"Function {func.__name__} is only available for context of type BlockFrostChainContext." - ) - - return wrapper - - @dataclass(frozen=True) class Amount: """Base class for Cardano currency amounts.""" @@ -546,8 +530,7 @@ def bytes_name(self): def policy_id(self): return self.policy.policy_id - @blockfrost_only - def get_on_chain_metadata(self, context: ChainContext) -> dict: + def get_on_chain_metadata(self, context: BlockFrostChainContext) -> dict: """Get the on-chain metadata of the token. Args: @@ -560,9 +543,10 @@ def get_on_chain_metadata(self, context: ChainContext) -> dict: """ try: - metadata = context.api.asset( - self.policy.policy_id + self.hex_name - ).onchain_metadata.to_dict() + token_info = context.api.asset( + self.policy_id + self.hex_name + ) + metadata = token_info.onchain_metadata.to_dict() except ApiError as e: logger.error( f"Could not get on-chain data, likely this asset has not been minted yet\n Blockfrost Error: {e}" @@ -1681,8 +1665,7 @@ def transact( # Utility and Helper functions -@blockfrost_only -def get_utxo_creator(utxo: UTxO, context: ChainContext) -> Address: +def get_utxo_creator(utxo: UTxO, context: BlockFrostChainContext) -> Address: """Fetch the creator of a UTxO. If there are multiple input UTxOs, the creator is the first one. @@ -1700,8 +1683,7 @@ def get_utxo_creator(utxo: UTxO, context: ChainContext) -> Address: return utxo_creator -@blockfrost_only -def get_utxo_block_time(utxo: UTxO, context: ChainContext) -> int: +def get_utxo_block_time(utxo: UTxO, context: BlockFrostChainContext) -> int: """Get the block time at which a UTxO was created. Args: @@ -1717,8 +1699,9 @@ def get_utxo_block_time(utxo: UTxO, context: ChainContext) -> int: return block_time -@blockfrost_only -def get_stake_info(stake_address: Union[str, Address], context: ChainContext) -> dict: +def get_stake_info( + stake_address: Union[str, Address], context: BlockFrostChainContext +) -> dict: """Get the stake info of a stake address from Blockfrost. For more info see: https://docs.blockfrost.io/#tag/Cardano-Accounts/paths/~1accounts~1{stake_address}/get @@ -1875,8 +1858,9 @@ def get_all_policies( return policies -@blockfrost_only -def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext) -> bool: +def confirm_tx( + tx_id: Union[str, TransactionId], context: BlockFrostChainContext +) -> bool: """Confirm that a transaction has been included in a block. Args: @@ -1896,9 +1880,10 @@ def confirm_tx(tx_id: Union[str, TransactionId], context: ChainContext) -> bool: return confirmed -@blockfrost_only def wait_for_confirmation( - tx_id: Union[str, TransactionId], context: ChainContext, delay: Optional[int] = 10 + tx_id: Union[str, TransactionId], + context: BlockFrostChainContext, + delay: Optional[int] = 10, ) -> bool: """Wait for a transaction to be confirmed, checking every `delay` seconds. From 908507724c9d59816dd12142d0e60eeddedd78ee Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 13 Nov 2022 13:52:12 +0100 Subject: [PATCH 101/130] Add tests for onchain token metadata --- test/pycardano/test_wallet.py | 51 +++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index a11cfd97..2a542a1e 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -1,10 +1,15 @@ import datetime import pathlib -from test.pycardano.util import chain_context -from unittest.mock import patch +from unittest.mock import patch, Mock +from blockfrost import BlockFrostApi, ApiError +from blockfrost.utils import convert_json_to_object import pytest +from requests import Response + +from test.pycardano.util import chain_context, blockfrost_patch, mock_blockfrost_api_error +from pycardano.backend.blockfrost import BlockFrostChainContext from pycardano.address import Address, VerificationKeyHash from pycardano.nativescript import ScriptAll, ScriptPubkey from pycardano.wallet import ( @@ -57,7 +62,6 @@ def test_load_wallet(): context="null", ) - def test_amount(): """Check that the Ada / Lovelace math works as expected.""" @@ -444,7 +448,7 @@ def test_metadata(): "key_3": {"key_3_1": "value_3_1"}, } - _ = Token(policy=policy, name="testToken", amount=1, metadata=metadata) + test_token = Token(policy=policy, name="testToken", amount=1, metadata=metadata) # test bad metadata @@ -465,7 +469,44 @@ def test_metadata(): with pytest.raises(MetadataFormattingException): _ = Token(policy=policy, name="testToken", amount=1, metadata=unserializable) - # TODO: add tests for onchain metadata + # Tests for onchain metadata + test_api_response = { + "asset": "6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e083174657374546f6b656e", + "policy_id": "6b0cb18696ccd4de1dcd9664c31ed6e98f7a4a1ff647855fef1e0831", + "asset_name": "74657374546f6b656e", + "fingerprint": "asset000", + "quantity": "1", + "initial_mint_tx_hash": "000", + "mint_or_burn_count": 1, + "onchain_metadata": metadata, + "metadata": None, + } + + with blockfrost_patch: + with patch.object( + BlockFrostApi, + "asset", + return_value=convert_json_to_object(test_api_response), + ): + onchain_meta = test_token.get_on_chain_metadata( + context=BlockFrostChainContext("") + ) + + assert onchain_meta == convert_json_to_object(metadata).to_dict() + + # test for no onchain metadata + with blockfrost_patch: + + with patch.object(BlockFrostApi, "asset") as mock_asset: + + mock_asset.side_effect = mock_blockfrost_api_error() + + onchain_meta = test_token.get_on_chain_metadata( + context=BlockFrostChainContext("") + ) + + assert onchain_meta == {} + def test_outputs(): From 91f0a6a7367cb4e34ac5445a076c079ce9874d0a Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 13 Nov 2022 13:52:23 +0100 Subject: [PATCH 102/130] Add patching for mocking blockfost --- test/pycardano/util.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/pycardano/util.py b/test/pycardano/util.py index edc7b414..f71a7a0d 100644 --- a/test/pycardano/util.py +++ b/test/pycardano/util.py @@ -1,6 +1,10 @@ from typing import Dict, List, Union +from unittest.mock import patch, Mock + import pytest +from blockfrost import BlockFrostApi, ApiError + from pycardano import ExecutionUnits from pycardano.backend.base import ChainContext, GenesisParameters, ProtocolParameters @@ -134,3 +138,11 @@ def evaluate_tx(self, cbor: Union[bytes, str]) -> Dict[str, ExecutionUnits]: @pytest.fixture def chain_context(): return FixedChainContext() + +# Patch BlockFrostApi to avoid network calls +blockfrost_patch = patch.object(BlockFrostApi, "epoch_latest", lambda _: 300,) + +# mock API error +def mock_blockfrost_api_error(): + + return ApiError(response=Mock(status_code=404, text="Mock Error")) From 154b70bfbd6a7bfe2e0c10041aff3ff26e651f78 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 13 Nov 2022 13:52:58 +0100 Subject: [PATCH 103/130] Format --- pycardano/wallet.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 38894ba1..142812ea 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -2,7 +2,6 @@ import json import operator from dataclasses import dataclass, field -from functools import wraps from os import getenv from pathlib import Path from time import sleep From ea4b8183bc880b57dc946e5e4322726e0ad63caf Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 13 Nov 2022 13:54:10 +0100 Subject: [PATCH 104/130] Format --- pycardano/wallet.py | 4 +--- test/pycardano/util.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 142812ea..8cd7e2b5 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -542,9 +542,7 @@ def get_on_chain_metadata(self, context: BlockFrostChainContext) -> dict: """ try: - token_info = context.api.asset( - self.policy_id + self.hex_name - ) + token_info = context.api.asset(self.policy_id + self.hex_name) metadata = token_info.onchain_metadata.to_dict() except ApiError as e: logger.error( diff --git a/test/pycardano/util.py b/test/pycardano/util.py index f71a7a0d..0c7a6884 100644 --- a/test/pycardano/util.py +++ b/test/pycardano/util.py @@ -1,10 +1,8 @@ from typing import Dict, List, Union -from unittest.mock import patch, Mock - +from unittest.mock import Mock, patch import pytest -from blockfrost import BlockFrostApi, ApiError - +from blockfrost import ApiError, BlockFrostApi from pycardano import ExecutionUnits from pycardano.backend.base import ChainContext, GenesisParameters, ProtocolParameters @@ -96,7 +94,7 @@ def epoch(self) -> int: def slot(self) -> int: """Current slot number""" return 2000 - + @property def last_block_slot(self) -> int: """Slot number of last block""" @@ -139,10 +137,15 @@ def evaluate_tx(self, cbor: Union[bytes, str]) -> Dict[str, ExecutionUnits]: def chain_context(): return FixedChainContext() + # Patch BlockFrostApi to avoid network calls -blockfrost_patch = patch.object(BlockFrostApi, "epoch_latest", lambda _: 300,) +blockfrost_patch = patch.object( + BlockFrostApi, + "epoch_latest", + lambda _: 300, +) # mock API error def mock_blockfrost_api_error(): - + return ApiError(response=Mock(status_code=404, text="Mock Error")) From 8a83a8d46cf8056de2cd4701c2c13cccd89b5329 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Sun, 13 Nov 2022 13:56:07 +0100 Subject: [PATCH 105/130] Format tests --- test/pycardano/test_wallet.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index 2a542a1e..d43e0fe3 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -1,16 +1,18 @@ import datetime import pathlib -from unittest.mock import patch, Mock +from test.pycardano.util import ( + blockfrost_patch, + chain_context, + mock_blockfrost_api_error, +) +from unittest.mock import patch -from blockfrost import BlockFrostApi, ApiError -from blockfrost.utils import convert_json_to_object import pytest -from requests import Response - +from blockfrost import BlockFrostApi +from blockfrost.utils import convert_json_to_object -from test.pycardano.util import chain_context, blockfrost_patch, mock_blockfrost_api_error -from pycardano.backend.blockfrost import BlockFrostChainContext from pycardano.address import Address, VerificationKeyHash +from pycardano.backend.blockfrost import BlockFrostChainContext from pycardano.nativescript import ScriptAll, ScriptPubkey from pycardano.wallet import ( Ada, @@ -62,6 +64,7 @@ def test_load_wallet(): context="null", ) + def test_amount(): """Check that the Ada / Lovelace math works as expected.""" @@ -493,20 +496,19 @@ def test_metadata(): ) assert onchain_meta == convert_json_to_object(metadata).to_dict() - + # test for no onchain metadata with blockfrost_patch: - + with patch.object(BlockFrostApi, "asset") as mock_asset: - + mock_asset.side_effect = mock_blockfrost_api_error() - + onchain_meta = test_token.get_on_chain_metadata( context=BlockFrostChainContext("") ) - + assert onchain_meta == {} - def test_outputs(): From 04dfdec27f40b1ca97f1817a6a0f53b0c62af518 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 14 Nov 2022 00:35:52 +0100 Subject: [PATCH 106/130] Add typechecking to all helper classes --- pycardano/wallet.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 8cd7e2b5..1c786f1f 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -5,7 +5,8 @@ from os import getenv from pathlib import Path from time import sleep -from typing import List, Literal, Optional, Union +from typing import List, Optional, Sequence, Union +from typing_extensions import Literal from blockfrost import ApiError @@ -280,7 +281,7 @@ class TokenPolicy: """ name: str - policy_id: Optional[str] = (None,) + policy_id: Optional[str] = None script: Optional[Union[NativeScript, dict]] = field(repr=False, default=None) policy_dir: Optional[Union[str, Path]] = field( repr=False, default=Path("./priv/policies") @@ -369,7 +370,13 @@ def is_expired(self, context: ChainContext): def generate_minting_policy( self, - signers: Union["Wallet", Address, List["Wallet"], List[Address]], + signers: Union[ + "Wallet", + Address, + List["Wallet"], + List[Address], + List[Union["Wallet", Address]], + ], expiration: Optional[Union[datetime.datetime, int]] = None, context: Optional[ChainContext] = None, ): @@ -383,7 +390,10 @@ def generate_minting_policy( context (Optional[ChainContext]): A context is needed to estimate the expiration slot from a datetime. """ - script_filepath = Path(self.policy_dir / f"{self.name}.script") + if not self.script or not self.policy_dir: + raise TypeError("The script of this policy is not set.") + + script_filepath = Path(self.policy_dir) / f"{self.name}.script" if script_filepath.exists() or self.script: raise FileExistsError(f"Policy named {self.name} already exists") @@ -409,7 +419,12 @@ def generate_minting_policy( else: time_until_expiration = expiration - get_now() - last_block_slot = context.last_block_slot + if context: + last_block_slot = context.last_block_slot + else: + raise AttributeError( + "If input expiration is provided as a datetime, please also provide a context." + ) must_before_slot = InvalidHereAfter( last_block_slot + int(time_until_expiration.total_seconds()) @@ -433,8 +448,11 @@ def generate_minting_policy( @staticmethod def _get_pub_key_hash(signer: Union["Wallet", Address]): - if hasattr(signer, "verification_key"): - return signer.verification_key.hash() + if isinstance(signer, Wallet): + if signer.verification_key: + return signer.verification_key.hash() + else: + TypeError("Singing wallet does not have associated keys.") elif isinstance(signer, Address): return str(signer.payment_part) else: @@ -569,7 +587,7 @@ class Output: address: Union["Wallet", Address, str] amount: Union[Lovelace, Ada, int] - tokens: Optional[Union[Token, List[Token]]] = field(default_factory=list) + tokens: Optional[Union[Token, List[Token]]] = None def __post_init__(self): @@ -1299,11 +1317,11 @@ def transact( outputs: Optional[Union[Output, List[Output]]] = None, mints: Optional[Union[Token, List[Token]]] = None, signers: Optional[ - Union["Wallet", List["Wallet"], SigningKey, List[SigningKey]] + Union["Wallet", List["Wallet"], SigningKey, List[SigningKey], Sequence["Wallet", SigningKey]] ] = None, stake_registration: Optional[ Union[ - bool, "Wallet", Address, str, List[Address], List["Wallet"], List[str] + bool, "Wallet", Address, str, List[Address], List["Wallet"], List[str], Sequence[Address, "Wallet", str] ] ] = None, delegations: Optional[Union[str, dict, PoolKeyHash]] = None, From 64df21629635625e682e0eebea8b52a949887554 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Mon, 14 Nov 2022 00:36:26 +0100 Subject: [PATCH 107/130] Change type hints from List to Sequence --- pycardano/nativescript.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pycardano/nativescript.py b/pycardano/nativescript.py index c1694d73..507e6666 100644 --- a/pycardano/nativescript.py +++ b/pycardano/nativescript.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import ClassVar, List, Type, Union +from typing import ClassVar, List, Sequence, Type, Union from nacl.encoding import RawEncoder from nacl.hash import blake2b @@ -153,7 +153,7 @@ class ScriptAll(NativeScript): json_field: ClassVar[str] = "scripts" _TYPE: int = field(default=1, init=False) - native_scripts: List[ + native_scripts: Sequence[ Union[ ScriptPubkey, ScriptAll, @@ -172,7 +172,7 @@ class ScriptAny(NativeScript): _TYPE: int = field(default=2, init=False) native_scripts: List[ - Union[ + Sequence[ ScriptPubkey, ScriptAll, ScriptAny, @@ -192,7 +192,7 @@ class ScriptNofK(NativeScript): n: int native_scripts: List[ - Union[ + Sequence[ ScriptPubkey, ScriptAll, ScriptAny, From 6801ada8453108bfb2046cd95d38b792b6a37de2 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 16 Nov 2022 00:33:45 +0100 Subject: [PATCH 108/130] Add strict typing with mypy --- pycardano/wallet.py | 329 +++++++++++++++++++++++++++++--------------- 1 file changed, 218 insertions(+), 111 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 1c786f1f..45a641d1 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -5,19 +5,25 @@ from os import getenv from pathlib import Path from time import sleep -from typing import List, Optional, Sequence, Union -from typing_extensions import Literal +from typing import List, Optional, Union from blockfrost import ApiError +from typing_extensions import Literal -from pycardano.address import Address +from pycardano.address import Address, PointerAddress from pycardano.backend.base import ChainContext from pycardano.backend.blockfrost import BlockFrostChainContext -from pycardano.certificate import StakeCredential, StakeDelegation, StakeRegistration +from pycardano.certificate import ( + StakeCredential, + StakeDelegation, + StakeDeregistration, + StakeRegistration, +) from pycardano.cip.cip8 import sign from pycardano.exception import PyCardanoException from pycardano.hash import PoolKeyHash, ScriptHash, TransactionId from pycardano.key import ( + ExtendedSigningKey, PaymentKeyPair, PaymentSigningKey, PaymentVerificationKey, @@ -38,6 +44,7 @@ Asset, AssetName, MultiAsset, + TransactionBody, TransactionOutput, UTxO, Value, @@ -283,9 +290,7 @@ class TokenPolicy: name: str policy_id: Optional[str] = None script: Optional[Union[NativeScript, dict]] = field(repr=False, default=None) - policy_dir: Optional[Union[str, Path]] = field( - repr=False, default=Path("./priv/policies") - ) + policy_dir: Union[str, Path] = field(repr=False, default=Path("./priv/policies")) def __post_init__(self): @@ -390,9 +395,6 @@ def generate_minting_policy( context (Optional[ChainContext]): A context is needed to estimate the expiration slot from a datetime. """ - if not self.script or not self.policy_dir: - raise TypeError("The script of this policy is not set.") - script_filepath = Path(self.policy_dir) / f"{self.name}.script" if script_filepath.exists() or self.script: @@ -413,18 +415,13 @@ def generate_minting_policy( if expiration: if isinstance(expiration, int): # assume this is directly the block no. must_before_slot = InvalidHereAfter(expiration) - elif isinstance(expiration, datetime.datetime): + elif isinstance(expiration, datetime.datetime) and context: if expiration.tzinfo: time_until_expiration = expiration - get_now(expiration.tzinfo) else: time_until_expiration = expiration - get_now() - if context: - last_block_slot = context.last_block_slot - else: - raise AttributeError( - "If input expiration is provided as a datetime, please also provide a context." - ) + last_block_slot = context.last_block_slot must_before_slot = InvalidHereAfter( last_block_slot + int(time_until_expiration.total_seconds()) @@ -741,6 +738,15 @@ def stake_address(self): else: return None + @property + def full_address(self): + + return Address( + payment_part=self.address.payment_part, + staking_part=self.address.staking_part, + network=self._network, + ) + @property def verification_key_hash(self): return self.address.payment_part @@ -848,9 +854,9 @@ def _find_context(self, context: Optional[ChainContext] = None): By default, will return `self.context` unless a context variable has been specifically specified. """ - if not context and not self.context: + if context is None and self.context is None: raise TypeError("Please pass `context` or set Wallet.context.") - elif not self.context: + elif context is not None or not self.context: return context else: return self.context @@ -917,10 +923,10 @@ def sync(self, context: Optional[ChainContext] = None): """ - context = self._find_context(context) + current_context = self._find_context(context) try: - self.utxos = context.utxos(str(self.address)) + self.utxos = current_context.utxos(str(self.address)) except Exception as e: logger.warning( f"Error getting UTxOs. Address has likely not transacted yet. Details: {e}" @@ -955,10 +961,10 @@ def get_utxo_creators(self, context: Optional[ChainContext] = None): context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. """ - context = self._find_context(context) + current_context = self._find_context(context) for utxo in self.utxos: - utxo.creator = get_utxo_creator(utxo, context) + utxo.creator = get_utxo_creator(utxo, current_context) def get_utxo_block_times(self, context: Optional[ChainContext] = None): """Get a list of the creation block number for each UTxO in the wallet. @@ -967,10 +973,10 @@ def get_utxo_block_times(self, context: Optional[ChainContext] = None): context (Optional[ChainContext]): The context to use for the query. Defaults to the wallet's context. """ - context = self._find_context(context) + current_context = self._find_context(context) for utxo in self.utxos: - utxo.block_time = get_utxo_block_time(utxo, context) + utxo.block_time = get_utxo_block_time(utxo, current_context) self.sort_utxos() @@ -1031,6 +1037,11 @@ def sign_data( f"Data signing mode must be `payment` or `stake`, not {mode}." ) + if not signing_key: + raise TypeError( + f"Wallet {self.name} does not have a compatible signing key." + ) + return sign( message, signing_key, @@ -1044,7 +1055,7 @@ def send_ada( amount: Union[Ada, Lovelace], utxos: Optional[Union[UTxO, List[UTxO]]] = None, **kwargs, - ) -> str: + ) -> Union[str, TransactionBody]: """Create a simple transaction in which Ada is sent to a single recipient. @@ -1064,7 +1075,7 @@ def send_ada( # streamline inputs, use either specific utxos or all wallet utxos if utxos: if isinstance(utxos, UTxO): - inputs = [utxos] + inputs: Union[List[UTxO], List["Wallet"]] = [utxos] else: inputs = utxos else: @@ -1074,7 +1085,7 @@ def send_ada( def send_utxo( self, to: Union[str, Address], utxos: Union[UTxO, List[UTxO]], **kwargs - ) -> str: + ) -> Union[str, TransactionBody]: """Send all of the contents (ADA and tokens) of specified UTxO(s) to a single recipient. Args: @@ -1091,7 +1102,7 @@ def empty_wallet( self, to: Union[str, Address], **kwargs, - ) -> str: + ) -> Union[str, TransactionBody]: """Send all of the contents (ADA and tokens) of the wallet to a single recipient. The current wallet will be left completely empty @@ -1109,10 +1120,10 @@ def delegate( self, pool_hash: Union[PoolKeyHash, str], register: Optional[bool] = True, - amount: Optional[Union[Ada, Lovelace]] = Lovelace(2000000), + amount: Union[Ada, Lovelace] = Lovelace(2000000), utxos: Optional[Union[UTxO, List[UTxO]]] = None, **kwargs, - ) -> str: + ) -> Union[str, TransactionBody]: """Delegate the current wallet to a pool. Args: @@ -1135,7 +1146,7 @@ def delegate( # streamline inputs, use either specific utxos or all wallet utxos if utxos: if isinstance(utxos, UTxO): - inputs = [utxos] + inputs: Union[List[UTxO], List["Wallet"]] = [utxos] else: inputs = utxos else: @@ -1161,9 +1172,9 @@ def delegate( def withdraw_rewards( self, withdrawal_amount: Optional[Union[Ada, Lovelace]] = None, - output_amount: Optional[Union[Ada, Lovelace]] = Lovelace(1000000), + output_amount: Union[Ada, Lovelace] = Lovelace(1000000), **kwargs, - ) -> str: + ) -> Union[str, TransactionBody]: """Withdraw staking rewards. Args: @@ -1203,7 +1214,7 @@ def mint_tokens( amount: Optional[Union[Ada, Lovelace]] = None, utxos: Optional[Union[UTxO, List[UTxO]]] = None, **kwargs, - ): + ) -> Union[str, TransactionBody]: """Mints (or burns) tokens of a policy owned by a user wallet. To attach metadata, set it in Token class directly. @@ -1233,7 +1244,7 @@ def mint_tokens( # streamline inputs, use either specific utxos or all wallet utxos if utxos: if isinstance(utxos, UTxO): - inputs = [utxos] + inputs: Union[List[UTxO], List["Wallet"]] = [utxos] else: inputs = utxos else: @@ -1249,11 +1260,11 @@ def mint_tokens( def burn_tokens( self, tokens: Union[Token, List[Token]], - change_address: Union[Address, str] = None, + change_address: Union[Address, str], amount: Optional[Union[Ada, Lovelace]] = None, utxos: Optional[Union[UTxO, List[UTxO]]] = None, **kwargs, - ): + ) -> Union[str, TransactionBody]: """Burns tokens of a policy owned by a user wallet. Same as mint_tokens but automatically sets Token class amount to a negative value. @@ -1277,12 +1288,14 @@ def burn_tokens( if not amount: # attach 1 ADA to burn transactions - amount = Ada(1) + attach_amount: Union[Ada, Lovelace] = Ada(1) + else: + attach_amount = amount # streamline inputs, use either specific utxos or all wallet utxos if utxos: if isinstance(utxos, UTxO): - inputs = [utxos] + inputs: Union[List[UTxO], List["Wallet"]] = [utxos] else: inputs = utxos else: @@ -1297,7 +1310,7 @@ def burn_tokens( return self.transact( inputs=inputs, - outputs=Output(change_address, amount, tokens=tokens), + outputs=Output(change_address, attach_amount, tokens=tokens), mints=tokens, **kwargs, ) @@ -1317,11 +1330,24 @@ def transact( outputs: Optional[Union[Output, List[Output]]] = None, mints: Optional[Union[Token, List[Token]]] = None, signers: Optional[ - Union["Wallet", List["Wallet"], SigningKey, List[SigningKey], Sequence["Wallet", SigningKey]] + Union[ + "Wallet", + List["Wallet"], + SigningKey, + List[SigningKey], + List[Union["Wallet", SigningKey]], + ] ] = None, stake_registration: Optional[ Union[ - bool, "Wallet", Address, str, List[Address], List["Wallet"], List[str], Sequence[Address, "Wallet", str] + bool, + "Wallet", + Address, + str, + List[Address], + List["Wallet"], + List[str], + List[Union[Address, "Wallet", str]], ] ] = None, delegations: Optional[Union[str, dict, PoolKeyHash]] = None, @@ -1334,7 +1360,7 @@ def transact( submit: Optional[bool] = True, await_confirmation: Optional[bool] = False, context: Optional[ChainContext] = None, - ) -> str: + ) -> Union[str, TransactionBody]: """ Construct fully manual transactions. @@ -1372,40 +1398,53 @@ def transact( """ # streamline inputs - context = self._find_context(context) + tx_context = self._find_context(context) if not isinstance(inputs, list): - inputs = [inputs] + input_list = [inputs] + else: + input_list = [i for i in inputs] if outputs and not isinstance(outputs, list): - outputs = [outputs] + output_list = [outputs] + elif outputs and isinstance(outputs, list): + output_list = [o for o in outputs] + else: + output_list = [] if mints and not isinstance(mints, list): - mints = [mints] - elif not mints: - mints = [] + mint_list = [mints] + elif mints and isinstance(mints, list): + mint_list = [m for m in mints] + else: + mint_list = [] if ( stake_registration and not isinstance(stake_registration, list) and not isinstance(stake_registration, bool) ): - stake_registration = [stake_registration] - elif not stake_registration: - stake_registration = [] + stake_registration_info = [stake_registration] + elif stake_registration and isinstance(stake_registration, list): + stake_registration_info = [s for s in stake_registration] + else: + stake_registration_info = [] if signers and not isinstance(signers, list): - signers = [signers] - elif not signers: - signers = [] + signers_list = [signers] + elif signers and isinstance(signers, list): + signers_list = [s for s in signers] + else: + signers_list = [] if not change_address: - change_address = self.address + output_change_address = self.full_address + elif isinstance(change_address, str): + output_change_address = Address.from_primitive(change_address) + elif isinstance(change_address, Address): + output_change_address = change_address else: - if isinstance(change_address, str): - change_address = Address.from_primitive(change_address) - elif not isinstance(change_address, Address): - change_address = change_address.address + output_change_address = change_address.full_address if other_metadata is None: other_metadata = {} @@ -1413,10 +1452,10 @@ def transact( all_metadata = {} # sort out mints - mints_dict = {} - mint_metadata = {} - native_scripts = [] - for token in mints: + mints_dict: dict = {} + mint_metadata: dict = {} + native_scripts: list = [] + for token in mint_list: if isinstance(token.policy, NativeScript): policy_hash = token.policy.hash() elif isinstance(token.policy, TokenPolicy): @@ -1429,10 +1468,11 @@ def transact( if not mints_dict.get(policy_hash): mints_dict[policy_hash] = {} - if token.policy.script not in native_scripts: - if isinstance(token.policy, NativeScript): + if isinstance(token.policy, NativeScript): + if token.policy not in native_scripts: native_scripts.append(token.policy) - else: + else: + if token.policy.script not in native_scripts: native_scripts.append(token.policy.script) mints_dict[policy_hash][token.name] = token @@ -1480,23 +1520,30 @@ def transact( else: auxiliary_data = AuxiliaryData(Metadata()) - # create stake registrations, delegations - certificates = [] - if stake_registration: # add registrations - if isinstance(stake_registration, bool): + # create stake_registrations, delegations + certificates: List[ + Union[StakeDelegation, StakeRegistration, StakeDeregistration] + ] = [] + if stake_registration_info: # add registrations + if isinstance(stake_registration_info, bool): # register current wallet stake_credential = StakeCredential(self.stake_verification_key.hash()) certificates.append(StakeRegistration(stake_credential)) - if self.stake_signing_key not in signers: - signers.append(self.stake_signing_key) + if self.stake_signing_key not in signers_list: + signers_list.append(self.stake_signing_key) else: - for stake in stake_registration: + for stake in stake_registration_info: if isinstance(stake, str): stake_hash = Address.from_primitive(stake).staking_part - elif isinstance(stake, self.__class__): + elif isinstance(stake, self.__class__) or isinstance(stake, Wallet): stake_hash = self.stake_verification_key.hash() else: stake_hash = stake.staking_part + + if isinstance(stake_hash, PointerAddress) or stake_hash is None: + raise ValueError( + f"Stake hash of type {type(stake_hash)} is not valid for stake registration." + ) stake_credential = StakeCredential(stake_hash) certificates.append(StakeRegistration(stake_credential)) if delegations: # add delegations @@ -1506,8 +1553,8 @@ def transact( certificates.append( StakeDelegation(stake_credential, pool_keyhash=pool_hash) ) - if self.stake_signing_key not in signers: - signers.append(self.stake_signing_key) + if self.stake_signing_key not in signers_list: + signers_list.append(self.stake_signing_key) elif isinstance(delegations, PoolKeyHash): # register current wallet stake_credential = StakeCredential(self.stake_verification_key.hash()) certificates.append( @@ -1523,6 +1570,11 @@ def transact( else: stake_hash = key.staking_part + if isinstance(stake_hash, PointerAddress) or stake_hash is None: + raise ValueError( + f"Stake hash of type {type(stake_hash)} is not valid for stake registration." + ) + # get pool hash from value: if isinstance(value, str): pool_hash = PoolKeyHash(bytes.fromhex(value)) @@ -1540,8 +1592,8 @@ def transact( withdraw[ self.stake_address.to_primitive() ] = self.withdrawable_amount.lovelace - if self.stake_signing_key not in signers: - signers.append(self.stake_signing_key) + if self.stake_signing_key not in signers_list: + signers_list.append(self.stake_signing_key) elif isinstance(withdrawals, dict): for key, value in withdrawals.items(): if isinstance(key, Address): @@ -1554,11 +1606,27 @@ def transact( if isinstance(value, Amount): withdrawal_amount = value elif isinstance(value, bool) or value == "all": # withdraw all - account_info = get_stake_info(stake_address, self.context) - if account_info.get("withdrawable_amount"): - withdrawal_amount = Lovelace( - int(account_info.get("withdrawable_amount")) + if not isinstance(tx_context, BlockFrostChainContext): + raise ValueError( + "Withdraw all is only supported with BlockFrostChainContext at the moment." ) + account_info = get_stake_info(stake_address, tx_context) + withdrawable_amount = account_info.get("withdrawable_amount") + + if withdrawable_amount: + if isinstance(withdrawable_amount, (int, float)): + withdrawal_amount = Lovelace(int(withdrawable_amount)) + elif ( + isinstance(withdrawable_amount, str) + and withdrawable_amount.isdigit() + ): + withdrawal_amount = Lovelace(int(withdrawable_amount)) + + else: + logger.warn( + f"Unable to parse a withdrawal amount of {withdrawable_amount}. Setting to 0." + ) + withdrawal_amount = Lovelace(0) else: logger.warn( f"Stake address {stake_address} is not registered yet." @@ -1567,24 +1635,31 @@ def transact( else: withdrawal_amount = Lovelace(0) - withdraw[ - stake_address.staking_part.to_primitive() - ] = withdrawal_amount.as_lovelace().amount + if not stake_address.staking_part: + raise ValueError(f"Stake Address {stake_address} is invalid.") + + withdraw[stake_address.staking_part.to_primitive()] = ( + withdrawal_amount.as_lovelace().amount + if isinstance(withdrawal_amount, Lovelace) + else withdrawal_amount.lovelace + ) # build the transaction - builder = TransactionBuilder(context) + builder = TransactionBuilder(tx_context) # add transaction inputs - for input_thing in inputs: + for input_thing in input_list: if isinstance(input_thing, Address) or isinstance(input_thing, str): builder.add_input_address(input_thing) elif isinstance(input_thing, Wallet): + if input_thing.address is None: + raise ValueError(f"Transaction input {input_thing} has no address.") builder.add_input_address(input_thing.address) elif isinstance(input_thing, UTxO): builder.add_input(input_thing) # set builder ttl to the min of the included policies - if mints: + if mint_list: builder.ttl = min( [TokenPolicy("", policy).expiration_slot for policy in native_scripts] ) @@ -1596,13 +1671,19 @@ def transact( builder.auxiliary_data = auxiliary_data # format tokens and lovelace of outputs - if outputs: - for output in outputs: - multi_asset = {} + if output_list: + for output in output_list: + multi_asset = MultiAsset() if output.tokens: multi_asset = MultiAsset() - output_policies = {} - for token in output.tokens: + output_policies: dict = {} + + if isinstance(output.tokens, list): + output_tokens: List[Token] = [token for token in output.tokens] + else: + output_tokens = [output.tokens] + + for token in output_tokens: if not output_policies.get(token.policy_id): output_policies[token.policy_id] = {} @@ -1620,19 +1701,29 @@ def transact( multi_asset[ScriptHash.from_primitive(policy)] = asset + if not isinstance(output.amount, Amount): + output.amount = Lovelace(output.amount) + + if isinstance(output.address, str): + output_address: Address = Address.from_primitive(output.address) + elif isinstance(output.address, Wallet): + output_address = output.address.full_address + else: + output_address = output.address + if not output.amount.lovelace: # Calculate min lovelace if necessary output.amount = Lovelace( min_lovelace( - context=context, + context=tx_context, output=TransactionOutput( - output.address, Value(1000000, mint_multiasset) + output_address, Value(1000000, mint_multiasset) ), ) ) builder.add_output( TransactionOutput( - output.address, Value(output.amount.lovelace, multi_asset) + output_address, Value(output.amount.lovelace, multi_asset) ) ) @@ -1646,34 +1737,50 @@ def transact( if build_only: return builder.build( - change_address=change_address, merge_change=merge_change + change_address=output_change_address, merge_change=merge_change ) - if signers: - signing_keys = [] - for signer in signers: - if isinstance(signer, self.__class__): + if signers_list: + signing_keys: List[Union[SigningKey, ExtendedSigningKey]] = [] + for signer in signers_list: + if isinstance(signer, Wallet): + if not signer.signing_key: + raise ValueError(f"Wallet {signer} has no signing key.") signing_keys.append(signer.signing_key) - else: + elif isinstance(signer, SigningKey) or isinstance( + signer, ExtendedSigningKey + ): signing_keys.append(signer) + else: + raise ValueError(f"Signer {signer} is not a valid signing key.") if self.signing_key not in signing_keys: - signing_keys.insert(0, self.signing_key) - + if isinstance(self.signing_key, SigningKey): + signing_keys.insert(0, self.signing_key) + else: + logger.warn( + f"Unable to add wallet's own signature to the transaction. Signing key: {self.signing_key}" + ) else: + if not self.signing_key: + raise ValueError( + f"Unable to add wallet's own signature to the transaction. Signing key: {self.signing_key}" + ) signing_keys = [self.signing_key] signed_tx = builder.build_and_sign( - signing_keys, change_address=change_address, merge_change=merge_change + signing_keys, + change_address=output_change_address, + merge_change=merge_change, ) if not submit: - return signed_tx.to_cbor() + return str(signed_tx.to_cbor()) - context.submit_tx(signed_tx.to_cbor()) + tx_context.submit_tx(signed_tx.to_cbor()) if await_confirmation: - _ = wait_for_confirmation(str(signed_tx.id), self.context) + _ = wait_for_confirmation(str(signed_tx.id), tx_context) self.sync() return str(signed_tx.id) @@ -1898,7 +2005,7 @@ def confirm_tx( def wait_for_confirmation( tx_id: Union[str, TransactionId], context: BlockFrostChainContext, - delay: Optional[int] = 10, + delay: int = 10, ) -> bool: """Wait for a transaction to be confirmed, checking every `delay` seconds. From a7cf5ce286a050c31048dc74312addd73f0e5397 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 16 Nov 2022 00:41:21 +0100 Subject: [PATCH 109/130] Update native script typing with Sequence --- pycardano/nativescript.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pycardano/nativescript.py b/pycardano/nativescript.py index f6d14989..f496a297 100644 --- a/pycardano/nativescript.py +++ b/pycardano/nativescript.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import ClassVar, List, Type, Union, cast +from typing import ClassVar, List, Sequence, Type, Union, cast from nacl.encoding import RawEncoder from nacl.hash import blake2b @@ -152,8 +152,8 @@ class ScriptAny(NativeScript): json_field: ClassVar[str] = "scripts" _TYPE: int = field(default=2, init=False) - native_scripts: List[ - Sequence[ + native_scripts: Sequence[ + Union[ ScriptPubkey, ScriptAll, ScriptAny, @@ -172,8 +172,8 @@ class ScriptNofK(NativeScript): n: int - native_scripts: List[ - Sequence[ + native_scripts: Sequence[ + Union[ ScriptPubkey, ScriptAll, ScriptAny, From 8f435d70a67d7a955777ae03865f86add3a67cf9 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 16 Nov 2022 22:38:31 +0100 Subject: [PATCH 110/130] Test required signatures when non sig scripts are present --- test/pycardano/test_wallet.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index d43e0fe3..44173597 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -13,7 +13,7 @@ from pycardano.address import Address, VerificationKeyHash from pycardano.backend.blockfrost import BlockFrostChainContext -from pycardano.nativescript import ScriptAll, ScriptPubkey +from pycardano.nativescript import ScriptAll, ScriptPubkey, InvalidBefore from pycardano.wallet import ( Ada, Lovelace, @@ -281,6 +281,11 @@ def test_policy(chain_context): policy_dir=third_policy_dir, ) assert their_policy.policy_id == policy.policy_id + + # test a policy with more than one condition + after_script = ScriptAll([ScriptPubkey(WALLET.verification_key.hash()), InvalidBefore(1000)]) + after_policy = TokenPolicy(name="after", script=after_script, policy_dir=str(policy_dir)) + assert after_policy.required_signatures == [WALLET.verification_key.hash()] # try loading an already existing policy reloaded_policy = TokenPolicy(name="testToken", policy_dir=str(policy_dir)) @@ -300,6 +305,7 @@ def test_policy(chain_context): signers=WALLET, expiration=datetime.datetime.now() ) + # test policy with expiration exp_filepath = policy_dir / f"expiring.script" if exp_filepath.exists(): From e66fa81547cd97de2c1f8a731ccd950642d99c7a Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 16 Nov 2022 23:10:51 +0100 Subject: [PATCH 111/130] Add __all__ --- pycardano/wallet.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 45a641d1..756fce23 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -53,6 +53,16 @@ from pycardano.txbuilder import TransactionBuilder from pycardano.utils import min_lovelace +__all__ = [ + "Amount", + "Lovelace", + "Ada", + "TokenPolicy", + "Token", + "Output", + "Wallet", +] + @dataclass(frozen=True) class Amount: From 41de108bfbc30909ccbc8fcd5d0207f00e29a6b6 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Wed, 16 Nov 2022 23:11:04 +0100 Subject: [PATCH 112/130] Add note about bad isinstance --- pycardano/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 756fce23..d083c90a 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -455,7 +455,7 @@ def generate_minting_policy( @staticmethod def _get_pub_key_hash(signer: Union["Wallet", Address]): - if isinstance(signer, Wallet): + if isinstance(signer, Wallet): # TODO: This isinstance is not working if signer.verification_key: return signer.verification_key.hash() else: From c93d3272623b9084f12205f840b562fd1b07a973 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 25 Nov 2022 23:26:35 +0100 Subject: [PATCH 113/130] Fix policy script not being passed properly to ttl --- pycardano/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index d083c90a..07adc301 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1671,7 +1671,7 @@ def transact( # set builder ttl to the min of the included policies if mint_list: builder.ttl = min( - [TokenPolicy("", policy).expiration_slot for policy in native_scripts] + [TokenPolicy("", script=policy).expiration_slot for policy in native_scripts] ) builder.mint = all_assets From 527e81837563be1419d5f13a474977308c385268 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 25 Nov 2022 23:27:22 +0100 Subject: [PATCH 114/130] Remove full address property --- pycardano/wallet.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 07adc301..7627e31f 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -748,15 +748,6 @@ def stake_address(self): else: return None - @property - def full_address(self): - - return Address( - payment_part=self.address.payment_part, - staking_part=self.address.staking_part, - network=self._network, - ) - @property def verification_key_hash(self): return self.address.payment_part From b0afc63419412851c82c35781aa25968ed30a990 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 25 Nov 2022 23:27:58 +0100 Subject: [PATCH 115/130] Add Wallet.to_address method to return full addr --- pycardano/wallet.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 7627e31f..52b30bb2 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -954,6 +954,16 @@ def sync(self, context: Optional[ChainContext] = None): self.lovelace = Lovelace(0) self.ada = Ada(0) + + + def to_address(self): + + return Address( + payment_part=self.address.payment_part, + staking_part=self.address.staking_part, + network=self._network, + ) + def get_utxo_creators(self, context: Optional[ChainContext] = None): """Get a list of all addresses that created each of the UTxOs in the wallet. @@ -1439,13 +1449,13 @@ def transact( signers_list = [] if not change_address: - output_change_address = self.full_address + output_change_address = self.to_address() elif isinstance(change_address, str): output_change_address = Address.from_primitive(change_address) elif isinstance(change_address, Address): output_change_address = change_address else: - output_change_address = change_address.full_address + output_change_address = change_address.to_address() if other_metadata is None: other_metadata = {} @@ -1708,7 +1718,7 @@ def transact( if isinstance(output.address, str): output_address: Address = Address.from_primitive(output.address) elif isinstance(output.address, Wallet): - output_address = output.address.full_address + output_address = output.address.to_address() else: output_address = output.address From 2c9f4009a39f7636ba9e208ac347f32283a85d33 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 25 Nov 2022 23:28:47 +0100 Subject: [PATCH 116/130] Format with black --- pycardano/wallet.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 52b30bb2..39dd541e 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -954,8 +954,7 @@ def sync(self, context: Optional[ChainContext] = None): self.lovelace = Lovelace(0) self.ada = Ada(0) - - + def to_address(self): return Address( @@ -964,7 +963,6 @@ def to_address(self): network=self._network, ) - def get_utxo_creators(self, context: Optional[ChainContext] = None): """Get a list of all addresses that created each of the UTxOs in the wallet. @@ -1672,7 +1670,10 @@ def transact( # set builder ttl to the min of the included policies if mint_list: builder.ttl = min( - [TokenPolicy("", script=policy).expiration_slot for policy in native_scripts] + [ + TokenPolicy("", script=policy).expiration_slot + for policy in native_scripts + ] ) builder.mint = all_assets From 17de2477fb62e04a58d51ecb624b7f98d34e2197 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 25 Nov 2022 23:40:12 +0100 Subject: [PATCH 117/130] Update usage of input parameters for TokenPolicy --- pycardano/wallet.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 39dd541e..2a4f81fd 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -907,7 +907,7 @@ def _get_tokens(self): else: token_list.append( Token( - TokenPolicy(name=policy_id[:8], policy=policy_id), + TokenPolicy(name=policy_id[:8], policy_id=policy_id), amount=amount, name=asset, ) @@ -1987,7 +1987,9 @@ def get_all_policies( if isinstance(policy_path, str): policy_path = Path(policy_path) - policies = [TokenPolicy(skey.stem) for skey in list(policy_path.glob("*.script"))] + policies = [ + TokenPolicy(name=skey.stem) for skey in list(policy_path.glob("*.script")) + ] return policies From ab26c4896e3949c2572e2e99837e9cdc84d3f043 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 30 Jun 2023 00:22:53 +0200 Subject: [PATCH 118/130] Test a policy with multiple conditions --- test/pycardano/test_wallet.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index 44173597..bbf62a07 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -281,10 +281,14 @@ def test_policy(chain_context): policy_dir=third_policy_dir, ) assert their_policy.policy_id == policy.policy_id - + # test a policy with more than one condition - after_script = ScriptAll([ScriptPubkey(WALLET.verification_key.hash()), InvalidBefore(1000)]) - after_policy = TokenPolicy(name="after", script=after_script, policy_dir=str(policy_dir)) + after_script = ScriptAll( + [ScriptPubkey(WALLET.verification_key.hash()), InvalidBefore(1000)] + ) + after_policy = TokenPolicy( + name="after", script=after_script, policy_dir=str(policy_dir) + ) assert after_policy.required_signatures == [WALLET.verification_key.hash()] # try loading an already existing policy From 2cc096976ba48db8f2b3ad37490f02c5a95e5525 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 30 Jun 2023 00:23:29 +0200 Subject: [PATCH 119/130] Test a policy with multiple conditions --- test/pycardano/test_wallet.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index bbf62a07..05593888 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -13,7 +13,7 @@ from pycardano.address import Address, VerificationKeyHash from pycardano.backend.blockfrost import BlockFrostChainContext -from pycardano.nativescript import ScriptAll, ScriptPubkey, InvalidBefore +from pycardano.nativescript import InvalidBefore, ScriptAll, ScriptPubkey from pycardano.wallet import ( Ada, Lovelace, @@ -309,7 +309,6 @@ def test_policy(chain_context): signers=WALLET, expiration=datetime.datetime.now() ) - # test policy with expiration exp_filepath = policy_dir / f"expiring.script" if exp_filepath.exists(): From 1f256fcd4a61c633cf4d51f86a9d0595ba0cc272 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 30 Jun 2023 00:24:06 +0200 Subject: [PATCH 120/130] set wallet context to null to avoid auto-checking chain --- test/pycardano/test_wallet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index 05593888..6949b914 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -557,6 +557,7 @@ def test_wallet_init(): not_my_wallet = Wallet( name="theirs", address="addr1q8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3uk2g9z3d4kaf0j5l6rxunxt43x28pssehhqds2x05mwld45s399sr7", + context="null", ) # try different networks From c948fbdbf44a63a29d63eb6da0cc856cf4c4bb68 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 30 Jun 2023 00:24:46 +0200 Subject: [PATCH 121/130] Try using the blockfrost patch to check chain --- test/pycardano/test_wallet.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index 6949b914..643518cb 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -561,9 +561,11 @@ def test_wallet_init(): ) # try different networks - wallet_preprod = Wallet(name="payment", network="preprod", keys_dir=keys_dir) - wallet_preview = Wallet(name="payment", network="preview", keys_dir=keys_dir) - wallet_testnet = Wallet(name="payment", network="testnet", keys_dir=keys_dir) + with blockfrost_patch: + wallet_mainnet = Wallet(name="payment", network="mainnet", keys_dir=keys_dir) + wallet_preprod = Wallet(name="payment", network="preprod", keys_dir=keys_dir) + wallet_preview = Wallet(name="payment", network="preview", keys_dir=keys_dir) + wallet_testnet = Wallet(name="payment", network="testnet", keys_dir=keys_dir) assert wallet_preprod.address == wallet_preview.address From 312a88f54a311c0dc62ce18de196a965b07c637f Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 30 Jun 2023 00:51:52 +0200 Subject: [PATCH 122/130] Fix typechecking issue with getting stake address --- pycardano/wallet.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 2a4f81fd..6000cd46 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1850,6 +1850,9 @@ def get_stake_info( if isinstance(stake_address, str): stake_address = Address.from_primitive(stake_address) + if not type(stake_address) == Address: + raise TypeError(f"Address {stake_address} is not a valid stake address.") + if not stake_address.staking_part: raise TypeError(f"Address {stake_address} has no staking part.") @@ -1871,6 +1874,9 @@ def get_stake_address(address: Union[str, Address]) -> Address: if isinstance(address, str): address = Address.from_primitive(address) + if not type(address) == Address: + raise TypeError(f"Address {address} is not a valid address.") + return Address.from_primitive( bytes.fromhex(f"e{address.network.value}" + str(address.staking_part)) ) From 8021ebea3297b19841751d0f665f751e6deac99b Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Fri, 30 Jun 2023 00:52:55 +0200 Subject: [PATCH 123/130] Format and style --- examples/automatic_royalty_token.py | 6 ------ pycardano/wallet.py | 21 --------------------- test/pycardano/test_wallet.py | 9 --------- test/pycardano/util.py | 2 +- 4 files changed, 1 insertion(+), 37 deletions(-) diff --git a/examples/automatic_royalty_token.py b/examples/automatic_royalty_token.py index 6c63a6be..f2f4d222 100644 --- a/examples/automatic_royalty_token.py +++ b/examples/automatic_royalty_token.py @@ -74,7 +74,6 @@ def create_royalty_metadata(royalty_address: str, royalty_percent: str): def launch(): - logger = logging.getLogger(__name__) logger.info("Welcome to the royalty NFT generation script!") @@ -104,7 +103,6 @@ def launch(): time.sleep(2) while not DONE: - loop_start_time = dt.datetime.now(dt.timezone.utc) logger.info(f"Starting loop {loop_start_time}") @@ -112,7 +110,6 @@ def launch(): tmp_wallet.get_utxo_creators() for utxo in tmp_wallet.utxos: - # check whether or not to mint can_mint = False if wallet.Lovelace(utxo.output.amount.coin) == CODED_AMOUNT: @@ -126,7 +123,6 @@ def launch(): ) if can_mint: - ORIGINAL_SENDER = utxo.creator logger.info( f"Original sender of {CODED_AMOUNT.ada} ADA is {ORIGINAL_SENDER}" @@ -169,7 +165,6 @@ def launch(): can_burn = True if can_burn: - # get original sender utxo_info = tmp_wallet.context.api.transaction_utxos( str(utxo.input.transaction_id) @@ -212,5 +207,4 @@ def launch(): if __name__ == "__main__": - launch() diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 6000cd46..d59fc08b 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -72,7 +72,6 @@ class Amount: _amount_type: str = "lovelace" def __post_init__(self): - if self._amount_type == "lovelace": self._lovelace = int(self._amount) self._ada = self._amount / 1000000 @@ -249,7 +248,6 @@ class Lovelace(Amount): """Stores an amount of Lovelace and automatically handles most currency math.""" def __init__(self, amount: int = 0): - if not isinstance(amount, int): raise TypeError("Lovelace must be an integer.") @@ -303,7 +301,6 @@ class TokenPolicy: policy_dir: Union[str, Path] = field(repr=False, default=Path("./priv/policies")) def __post_init__(self): - # streamline inputs if isinstance(self.policy_dir, str): self.policy_dir = Path(self.policy_dir) @@ -454,7 +451,6 @@ def generate_minting_policy( @staticmethod def _get_pub_key_hash(signer: Union["Wallet", Address]): - if isinstance(signer, Wallet): # TODO: This isinstance is not working if signer.verification_key: return signer.verification_key.hash() @@ -486,7 +482,6 @@ class Token: metadata: Optional[dict] = field(default_factory=dict, compare=False) def __post_init__(self): - if not isinstance(self.amount, int): raise TypeError("Expected token amount to be of type: integer.") @@ -512,7 +507,6 @@ def _check_metadata( if isinstance(to_check, dict): for key, value in to_check.items(): - if len(str(key)) > 64: raise MetadataFormattingException( f"Metadata key is too long (> 64 characters): {key}\nConsider splitting into an array of " @@ -529,7 +523,6 @@ def _check_metadata( ) elif isinstance(to_check, list): - for item in to_check: self._check_metadata(to_check=item) @@ -597,7 +590,6 @@ class Output: tokens: Optional[Union[Token, List[Token]]] = None def __post_init__(self): - if isinstance(self.amount, int): self.amount = Lovelace(self.amount) @@ -680,7 +672,6 @@ class Wallet: context: Optional[BlockFrostChainContext] = field(repr=False, default=None) def __post_init__(self): - # convert address into pycardano format if isinstance(self.address, str): self.address = Address.from_primitive(self.address) @@ -735,12 +726,10 @@ def __post_init__(self): @property def payment_address(self): - return Address(payment_part=self.address.payment_part, network=self._network) @property def stake_address(self): - if self.stake_signing_key or self.address.staking_part: return Address( staking_part=self.address.staking_part, network=self._network @@ -869,13 +858,10 @@ def _get_tokens(self): tokens = {} for utxo in self.utxos: - for script_hash, assets in utxo.output.amount.multi_asset.items(): - policy_id = str(script_hash) for asset, amount in assets.items(): - asset_name = asset.to_primitive().decode("utf-8") if not tokens.get(policy_id): @@ -936,7 +922,6 @@ def sync(self, context: Optional[ChainContext] = None): # calculate total ada if self.utxos: - self.lovelace = Lovelace( sum([utxo.output.amount.coin for utxo in self.utxos]) ) @@ -956,7 +941,6 @@ def sync(self, context: Optional[ChainContext] = None): self.ada = Ada(0) def to_address(self): - return Address( payment_part=self.address.payment_part, staking_part=self.address.staking_part, @@ -1112,7 +1096,6 @@ def empty_wallet( to: Union[str, Address], **kwargs, ) -> Union[str, TransactionBody]: - """Send all of the contents (ADA and tokens) of the wallet to a single recipient. The current wallet will be left completely empty @@ -1494,7 +1477,6 @@ def transact( all_assets = MultiAsset() for policy_hash, tokens in mints_dict.items(): - mint_assets = Asset() assets = Asset() for token in tokens.values(): @@ -1705,7 +1687,6 @@ def transact( output_policies[token.policy_id][token.name] = token.amount for policy, token_info in output_policies.items(): - asset = Asset() for token_name, token_amount in token_info.items(): @@ -1923,7 +1904,6 @@ def check_metadata(to_check: Union[dict, list, str], top_level: bool = False): if isinstance(to_check, dict): for key, value in to_check.items(): - if len(str(key)) > 64: raise MetadataFormattingException( f"Metadata key is too long (> 64 characters): {key}\nConsider splitting into an array of shorter " @@ -1940,7 +1920,6 @@ def check_metadata(to_check: Union[dict, list, str], top_level: bool = False): ) elif isinstance(to_check, list): - for item in to_check: if len(str(item)) > 64: raise MetadataFormattingException( diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index 643518cb..9ac1f16f 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -26,7 +26,6 @@ def test_load_wallet(): - w = Wallet( name="payment", keys_dir=str(pathlib.Path(__file__).parent / "../resources/keys"), @@ -208,7 +207,6 @@ def test_amount_math(): def test_wallet_sign_data(): - assert ( str(WALLET.address) == "addr1q8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3uk2g9z3d4kaf0j5l6rxunxt43x28pssehhqds2x05mwld45s399sr7" @@ -224,7 +222,6 @@ def test_wallet_sign_data(): def test_policy(chain_context): - policy_dir = pathlib.Path(__file__).parent / "../resources/policy" script_filepath = policy_dir / f"testToken.script" @@ -429,7 +426,6 @@ def test_policy(chain_context): def test_token(): - script = ScriptAll([ScriptPubkey(WALLET.verification_key.hash())]) policy = TokenPolicy(name="testToken", script=script) @@ -449,7 +445,6 @@ def test_token(): def test_metadata(): - script = ScriptAll([ScriptPubkey(WALLET.verification_key.hash())]) policy = TokenPolicy(name="testToken", script=script) @@ -508,9 +503,7 @@ def test_metadata(): # test for no onchain metadata with blockfrost_patch: - with patch.object(BlockFrostApi, "asset") as mock_asset: - mock_asset.side_effect = mock_blockfrost_api_error() onchain_meta = test_token.get_on_chain_metadata( @@ -521,7 +514,6 @@ def test_metadata(): def test_outputs(): - output1 = Output(address=WALLET, amount=5000000) output2 = Output(address=WALLET, amount=Lovelace(5000000)) output3 = Output(address=WALLET, amount=Ada(5)) @@ -545,7 +537,6 @@ def test_outputs(): def test_wallet_init(): - keys_dir = str(pathlib.Path(__file__).parent / "../resources/keys") wallet = Wallet( diff --git a/test/pycardano/util.py b/test/pycardano/util.py index 2823da15..ac4b75fe 100644 --- a/test/pycardano/util.py +++ b/test/pycardano/util.py @@ -145,7 +145,7 @@ def chain_context(): lambda _: 300, ) + # mock API error def mock_blockfrost_api_error(): - return ApiError(response=Mock(status_code=404, text="Mock Error")) From 8c2860b3492d04c92c982aad856dedbda5018e48 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Tue, 8 Aug 2023 16:45:57 +0900 Subject: [PATCH 124/130] Remove burn tokens from outputs in mint transactions --- pycardano/wallet.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index d59fc08b..586fcefd 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1241,10 +1241,19 @@ def mint_tokens( inputs = utxos else: inputs = [self] + + # if token amounts are negative, remove them from the outputs + if not isinstance(mints, list): + mints = [mints] + + tokens = [] + for mint in mints: + if mint.amount > 0: + tokens.append(mint) return self.transact( inputs=inputs, - outputs=Output(to, amount, tokens=mints), + outputs=Output(to, amount, tokens=tokens), mints=mints, **kwargs, ) From 4c1cf9ec3d84ba4a88586f102907c47f7ad259f0 Mon Sep 17 00:00:00 2001 From: Jarred Green Date: Tue, 8 Aug 2023 16:46:12 +0900 Subject: [PATCH 125/130] Remove burned tokens from output in burn transactions --- pycardano/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 586fcefd..266711c0 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -1311,7 +1311,7 @@ def burn_tokens( return self.transact( inputs=inputs, - outputs=Output(change_address, attach_amount, tokens=tokens), + outputs=Output(change_address, attach_amount), mints=tokens, **kwargs, ) From 7d3ea22960962782dd4b625b56bde323fac13a8b Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Mon, 17 Jun 2024 17:30:34 -0500 Subject: [PATCH 126/130] lint: fix mypy issues --- pycardano/wallet.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 266711c0..00ad686c 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -16,8 +16,8 @@ from pycardano.certificate import ( StakeCredential, StakeDelegation, - StakeDeregistration, StakeRegistration, + Certificate, ) from pycardano.cip.cip8 import sign from pycardano.exception import PyCardanoException @@ -1241,11 +1241,9 @@ def mint_tokens( inputs = utxos else: inputs = [self] - # if token amounts are negative, remove them from the outputs if not isinstance(mints, list): mints = [mints] - tokens = [] for mint in mints: if mint.amount > 0: @@ -1521,9 +1519,7 @@ def transact( auxiliary_data = AuxiliaryData(Metadata()) # create stake_registrations, delegations - certificates: List[ - Union[StakeDelegation, StakeRegistration, StakeDeregistration] - ] = [] + certificates: List[Certificate] = [] if stake_registration_info: # add registrations if isinstance(stake_registration_info, bool): # register current wallet @@ -1589,9 +1585,9 @@ def transact( # withdrawals withdraw = {} if withdrawals and isinstance(withdrawals, bool): # withdraw current wallet - withdraw[ - self.stake_address.to_primitive() - ] = self.withdrawable_amount.lovelace + withdraw[self.stake_address.to_primitive()] = ( + self.withdrawable_amount.lovelace + ) if self.stake_signing_key not in signers_list: signers_list.append(self.stake_signing_key) elif isinstance(withdrawals, dict): @@ -1840,7 +1836,7 @@ def get_stake_info( if isinstance(stake_address, str): stake_address = Address.from_primitive(stake_address) - if not type(stake_address) == Address: + if not isinstance(stake_address, Address): raise TypeError(f"Address {stake_address} is not a valid stake address.") if not stake_address.staking_part: @@ -1864,7 +1860,7 @@ def get_stake_address(address: Union[str, Address]) -> Address: if isinstance(address, str): address = Address.from_primitive(address) - if not type(address) == Address: + if not isinstance(address, Address): raise TypeError(f"Address {address} is not a valid address.") return Address.from_primitive( From ef11f0670dba1c37adf20fbbda8c1cf6fe2a8b4b Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Tue, 18 Jun 2024 12:32:16 -0500 Subject: [PATCH 127/130] feat: add query stake address info to chain contexts --- pycardano/backend/base.py | 39 +++++++++++++++++++++++++++++++- pycardano/backend/blockfrost.py | 20 ++++++++++++++++ pycardano/backend/cardano_cli.py | 34 ++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/pycardano/backend/base.py b/pycardano/backend/base.py index aec6948a..0f51a940 100644 --- a/pycardano/backend/base.py +++ b/pycardano/backend/base.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from fractions import Fraction -from typing import Dict, List, Union +from typing import Dict, List, Union, Optional from pycardano.address import Address from pycardano.exception import InvalidArgumentException @@ -110,6 +110,19 @@ class ProtocolParameters: The value will be a dict of cost model parameters.""" +@dataclass(frozen=True) +class StakeAddressInfo: + """The current delegation and reward account for a stake address""" + + address: str + + delegation: str + + reward_account_balance: int + + delegation_deposit: Optional[int] = None + + @typechecked class ChainContext: """Interfaces through which the library interacts with Cardano blockchain.""" @@ -139,6 +152,30 @@ def last_block_slot(self) -> int: """Slot number of last block""" raise NotImplementedError() + def stake_address_info( + self, address: Union[str, Address] + ) -> List[StakeAddressInfo]: + """Get the current delegation and reward account for a stake address. + + Args: + address (Union[str, Address]): An address, potentially bech32 encoded + + Returns: + List[StakeAddressInfo]: A list of StakeAddressInfo objects + """ + return self._stake_address_info(str(address)) + + def _stake_address_info(self, address: str) -> List[StakeAddressInfo]: + """Get the current delegation and reward account for a stake address. + + Args: + address (str): An address encoded with bech32. + + Returns: + List[StakeAddressInfo]: A list of StakeAddressInfo objects + """ + raise NotImplementedError() + def utxos(self, address: Union[str, Address]) -> List[UTxO]: """Get all UTxOs associated with an address. diff --git a/pycardano/backend/blockfrost.py b/pycardano/backend/blockfrost.py index 40c9ccc7..cc0a9c88 100644 --- a/pycardano/backend/blockfrost.py +++ b/pycardano/backend/blockfrost.py @@ -15,6 +15,7 @@ ChainContext, GenesisParameters, ProtocolParameters, + StakeAddressInfo, ) from pycardano.exception import TransactionFailedException from pycardano.hash import SCRIPT_HASH_SIZE, DatumHash, ScriptHash @@ -305,3 +306,22 @@ def evaluate_tx_cbor(self, cbor: Union[bytes, str]) -> Dict[str, ExecutionUnits] getattr(result.EvaluationResult, k).steps, ) return return_val + + def _stake_address_info(self, address: str) -> List[StakeAddressInfo]: + """Get the current delegation and reward account for a stake address. + + Args: + address (str): An address encoded with bech32. + + Returns: + List[StakeAddressInfo]: A list of StakeAddressInfo objects + """ + results = self.api.accounts(address).to_dict() + + return [ + StakeAddressInfo( + address=results.get("stake_address"), + delegation=results.get("pool_id"), + reward_account_balance=results.get("withdrawable_amount"), + ) + ] diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py index 5c0072ea..4d3ceb50 100644 --- a/pycardano/backend/cardano_cli.py +++ b/pycardano/backend/cardano_cli.py @@ -23,6 +23,7 @@ ChainContext, GenesisParameters, ProtocolParameters, + StakeAddressInfo, ) from pycardano.exception import ( CardanoCliError, @@ -530,3 +531,36 @@ def submit_tx_cbor(self, cbor: Union[bytes, str]) -> str: ) from err return txid + + def _stake_address_info(self, address: str) -> List[StakeAddressInfo]: + """Get the current delegation and reward account for a stake address. + + Args: + address (str): An address encoded with bech32. + + Returns: + List[StakeAddressInfo]: A list of StakeAddressInfo objects + """ + results = self._run_command( + [ + "query", + "stake-address-info", + "--address", + address, + "--out-file", + "/dev/stdout", + ] + + self._network_args + ) + + result_json = json.loads(results) + + return [ + StakeAddressInfo( + address=stake_info["address"], + delegation=stake_info["delegation"], + delegation_deposit=stake_info["delegationDeposit"], + reward_account_balance=stake_info["rewardAccountBalance"], + ) + for stake_info in result_json + ] From 8cb1824ff45dddbba7bd3e18592ef3adc2a86bd7 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Tue, 18 Jun 2024 12:33:22 -0500 Subject: [PATCH 128/130] fix: use query stake address info and other minor fixes --- pycardano/wallet.py | 55 ++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/pycardano/wallet.py b/pycardano/wallet.py index 00ad686c..356c1121 100644 --- a/pycardano/wallet.py +++ b/pycardano/wallet.py @@ -11,7 +11,7 @@ from typing_extensions import Literal from pycardano.address import Address, PointerAddress -from pycardano.backend.base import ChainContext +from pycardano.backend.base import ChainContext, StakeAddressInfo from pycardano.backend.blockfrost import BlockFrostChainContext from pycardano.certificate import ( StakeCredential, @@ -20,7 +20,7 @@ Certificate, ) from pycardano.cip.cip8 import sign -from pycardano.exception import PyCardanoException +from pycardano.exception import PyCardanoException, CardanoCliError from pycardano.hash import PoolKeyHash, ScriptHash, TransactionId from pycardano.key import ( ExtendedSigningKey, @@ -668,10 +668,11 @@ class Wallet: ada: Optional[Ada] = field(repr=True, default=Ada(0)) signing_key: Optional[SigningKey] = field(repr=False, default=None) verification_key: Optional[VerificationKey] = field(repr=False, default=None) - uxtos: Optional[list] = field(repr=False, default_factory=list) - context: Optional[BlockFrostChainContext] = field(repr=False, default=None) + uxtos: Optional[List[UTxO]] = None + context: Optional[ChainContext] = field(repr=False, default=None) def __post_init__(self): + self.utxos = [] # convert address into pycardano format if isinstance(self.address, str): self.address = Address.from_primitive(self.address) @@ -763,20 +764,18 @@ def stake_info(self): @property def pool_id(self): account_info = get_stake_info(self.stake_address, self.context) - if account_info.get("pool_id"): - return account_info.get("pool_id") - else: - logger.warn("Stake address is not registered yet.") - return None + if len(account_info): + return account_info[0].delegation + logger.warn("Stake address is not registered yet.") + return None @property def withdrawable_amount(self): account_info = get_stake_info(self.stake_address, self.context) - if account_info.get("withdrawable_amount"): - return Lovelace(int(account_info.get("withdrawable_amount"))) - else: - logger.warn("Stake address is not registered yet.") - return Lovelace(0) + if len(account_info): + return Lovelace(int(account_info[0].reward_account_balance)) + logger.warn("Stake address is not registered yet.") + return Lovelace(0) def _load_or_create_key_pair(self, stake=True): """Look for a key pair in the keys directory. If not found, create a new key pair.""" @@ -918,12 +917,11 @@ def sync(self, context: Optional[ChainContext] = None): logger.warning( f"Error getting UTxOs. Address has likely not transacted yet. Details: {e}" ) - self.utxos = [] # calculate total ada if self.utxos: self.lovelace = Lovelace( - sum([utxo.output.amount.coin for utxo in self.utxos]) + sum(utxo.output.amount.coin for utxo in self.utxos) ) self.ada = self.lovelace.as_ada() @@ -937,8 +935,8 @@ def sync(self, context: Optional[ChainContext] = None): else: logger.info(f"Wallet {self.name} has no UTxOs.") - self.lovelace = Lovelace(0) - self.ada = Ada(0) + self.lovelace = Lovelace() + self.ada = Ada() def to_address(self): return Address( @@ -1145,7 +1143,7 @@ def delegate( inputs = [self] # check registration, do not register if already registered - active = self.stake_info.get("active") + active = len(self.stake_info) > 0 if register: register = not active elif not active: @@ -1195,7 +1193,7 @@ def withdraw_rewards( return self.transact( inputs=[self], outputs=Output(self, output_amount), - withdrawals={self: withdrawal_amount}, + withdrawals={str(self.stake_address): withdrawal_amount}, **kwargs, ) @@ -1607,7 +1605,7 @@ def transact( "Withdraw all is only supported with BlockFrostChainContext at the moment." ) account_info = get_stake_info(stake_address, tx_context) - withdrawable_amount = account_info.get("withdrawable_amount") + withdrawable_amount = account_info[0].reward_account_balance if withdrawable_amount: if isinstance(withdrawable_amount, (int, float)): @@ -1634,7 +1632,7 @@ def transact( if not stake_address.staking_part: raise ValueError(f"Stake Address {stake_address} is invalid.") - withdraw[stake_address.staking_part.to_primitive()] = ( + withdraw[str(stake_address)] = ( withdrawal_amount.as_lovelace().amount if isinstance(withdrawal_amount, Lovelace) else withdrawal_amount.lovelace @@ -1820,8 +1818,8 @@ def get_utxo_block_time(utxo: UTxO, context: BlockFrostChainContext) -> int: def get_stake_info( - stake_address: Union[str, Address], context: BlockFrostChainContext -) -> dict: + stake_address: Union[str, Address], context: ChainContext +) -> List[StakeAddressInfo]: """Get the stake info of a stake address from Blockfrost. For more info see: https://docs.blockfrost.io/#tag/Cardano-Accounts/paths/~1accounts~1{stake_address}/get @@ -1830,7 +1828,7 @@ def get_stake_info( context (ChainContext): The context to use for the query. For now must be BlockFrost. Returns: - dict: Info regarding the given stake address. + List[StakeAddressInfo]: A list of StakeAddressInfo objects """ if isinstance(stake_address, str): @@ -1843,9 +1841,10 @@ def get_stake_info( raise TypeError(f"Address {stake_address} has no staking part.") try: - return context.api.accounts(str(stake_address)).to_dict() - except ApiError: - return {} + # return context.api.accounts(str(stake_address)).to_dict() + return context.stake_address_info(str(stake_address)) + except (ApiError, CardanoCliError): + return [] def get_stake_address(address: Union[str, Address]) -> Address: From f7b39d2645acc884e8d403ff2b39754e0e94c9d1 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Tue, 18 Jun 2024 12:34:27 -0500 Subject: [PATCH 129/130] test: add some more tests around wallet --- test/conftest.py | 73 ++++++++++++++++- test/pycardano/test_coinselection.py | 1 - test/pycardano/test_util.py | 2 - test/pycardano/test_wallet.py | 113 ++++++++++++++++++++++++++- test/pycardano/util.py | 33 ++++++-- 5 files changed, 208 insertions(+), 14 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index aff5d585..81de1d3b 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,5 +1,9 @@ +import contextlib +import os +import pathlib +from datetime import datetime, timedelta from fractions import Fraction -from test.pycardano.util import FixedChainContext +from unittest import mock import pytest @@ -14,6 +18,9 @@ RewardAccountHash, VerificationKeyHash, VrfKeyHash, + Address, + ScriptPubkey, + ScriptAll, ) from pycardano.pool_params import ( MultiHostName, @@ -22,6 +29,8 @@ SingleHostAddr, SingleHostName, ) +from pycardano.wallet import Wallet, Token, TokenPolicy +from test.pycardano.util import FixedChainContext @pytest.fixture @@ -29,6 +38,55 @@ def chain_context(): return FixedChainContext() +@pytest.fixture +def address() -> Address: + return Address.from_primitive( + "addr_test1vr2p8st5t5cxqglyjky7vk98k7jtfhdpvhl4e97cezuhn0cqcexl7" + ) + + +@pytest.fixture +def stake_address() -> Address: + return Address.from_primitive( + "stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc" + ) + + +@pytest.fixture +def pool_id() -> Address: + return "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy" + + +@pytest.fixture +def wallet(chain_context) -> Wallet: + test_wallet = Wallet( + name="payment", + keys_dir=str(pathlib.Path(__file__).parent / "./resources/keys"), + context=chain_context, + ) + test_wallet.sync() + return test_wallet + + +@pytest.fixture +def token(wallet) -> Token: + # script = ScriptAll([ScriptPubkey(wallet.verification_key.hash())]) + + policy = TokenPolicy(name="Token1") + + with contextlib.suppress(FileExistsError): + policy.generate_minting_policy( + signers=wallet, + expiration=datetime(2025, 5, 12, 12, 0, 0), + context=wallet.context, + ) + yield Token(policy=policy, name="Token1", amount=1, metadata={"key": "value"}) + + script_filepath = pathlib.Path(policy.policy_dir) / f"{policy.name}.script" + + script_filepath.unlink(missing_ok=True) + + @pytest.fixture def pool_params(): return PoolParams( @@ -49,3 +107,16 @@ def pool_params(): pool_metadata_hash=PoolMetadataHash(b"1" * POOL_METADATA_HASH_SIZE), ), ) + + +@pytest.fixture(scope="session", autouse=True) +def mock_setting_env_vars(): + with mock.patch.dict( + os.environ, + { + "BLOCKFROST_ID_MAINNET": "mainnet_fakeapikey", + "BLOCKFROST_ID_PREPROD": "preprod_fakeapikey", + "BLOCKFROST_ID_PREVIEW": "preview_fakeapikey", + }, + ): + yield diff --git a/test/pycardano/test_coinselection.py b/test/pycardano/test_coinselection.py index 19e67d39..137dadbe 100644 --- a/test/pycardano/test_coinselection.py +++ b/test/pycardano/test_coinselection.py @@ -1,5 +1,4 @@ from functools import reduce -from test.pycardano.util import chain_context from typing import List import pytest diff --git a/test/pycardano/test_util.py b/test/pycardano/test_util.py index 118c8e13..7a70be46 100644 --- a/test/pycardano/test_util.py +++ b/test/pycardano/test_util.py @@ -1,5 +1,3 @@ -from test.pycardano.util import chain_context - from pycardano.hash import SCRIPT_HASH_SIZE, ScriptDataHash from pycardano.plutus import ExecutionUnits, PlutusData, Redeemer, RedeemerTag, Unit from pycardano.transaction import Value diff --git a/test/pycardano/test_wallet.py b/test/pycardano/test_wallet.py index 9ac1f16f..0f65a960 100644 --- a/test/pycardano/test_wallet.py +++ b/test/pycardano/test_wallet.py @@ -1,11 +1,25 @@ import datetime import pathlib +from typing import Literal + +from pycardano import ( + Network, + PoolKeyHash, + POOL_KEY_HASH_SIZE, + UTxO, + TransactionInput, + Value, + TransactionOutput, + MultiAsset, + ScriptHash, + AssetName, + Asset, +) from test.pycardano.util import ( blockfrost_patch, - chain_context, mock_blockfrost_api_error, ) -from unittest.mock import patch +from unittest.mock import patch, MagicMock import pytest from blockfrost import BlockFrostApi @@ -107,6 +121,7 @@ def test_amount(): assert -Lovelace(5) == Lovelace(-5) assert -Ada(5) == Ada(-5) assert round(Ada(5.66)) == Ada(6) + assert repr(Lovelace(2)) == "Lovelace(2)" with pytest.raises(TypeError): Lovelace(500) == "500" @@ -576,3 +591,97 @@ def test_wallet_init(): assert wallet.stake_verification_key_hash == VerificationKeyHash.from_primitive( "4828a2dadba97ca9fd0cdc99975899470c219bdc0d828cfa6ddf6d69" ) + + +@pytest.mark.parametrize( + "test_input, expected", + [ + ("mainnet", Network.MAINNET), + ("preprod", Network.TESTNET), + ("preview", Network.TESTNET), + ], +) +def test_none_context_init( + test_input: Literal["mainnet", "preview", "preprod"], expected: Network +): + with patch( + "pycardano.backend.blockfrost.BlockFrostApi", + return_value=MagicMock(), + ): + wallet = Wallet( + name="payment", + keys_dir=str(pathlib.Path(__file__).parent / "../resources/keys"), + network=test_input, + context=None, + ) + assert wallet.context.network == expected + + +def test_pool_id(wallet, pool_id): + assert wallet.pool_id == pool_id + + +def test_withdrawable_amount(wallet): + assert wallet.withdrawable_amount == 1000000 + + +def test_empty_wallet(wallet, address): + tx_cbor = wallet.empty_wallet(address, submit=False) + assert tx_cbor is not None + + +def test_send_utxo(wallet, address): + tx_cbor = wallet.send_utxo(address, wallet.utxos[0], submit=False) + assert tx_cbor is not None + + +def test_send_ada(wallet): + tx_cbor_1 = wallet.send_ada(WALLET.address, Ada(1), submit=False) + tx_cbor_2 = wallet.send_ada(WALLET.address, Ada(1), wallet.utxos[0], submit=False) + assert tx_cbor_1 is not None + assert tx_cbor_2 is not None + + +def test_delegate(wallet): + tx_cbor_1 = wallet.delegate( + PoolKeyHash(b"1" * POOL_KEY_HASH_SIZE), True, submit=False + ) + tx_cbor_2 = wallet.delegate( + PoolKeyHash(b"1" * POOL_KEY_HASH_SIZE), + True, + utxos=wallet.utxos[0], + submit=False, + ) + assert tx_cbor_1 is not None + assert tx_cbor_2 is not None + + +def test_withdraw_rewards(wallet): + tx_cbor = wallet.withdraw_rewards(submit=False) + assert tx_cbor is not None + + +def test_mint_tokens(wallet, token): + tx_cbor = wallet.mint_tokens(wallet.address, token, submit=False) + assert tx_cbor is not None + + +def test_burn_tokens(wallet, token): + multi_asset = MultiAsset() + assets = Asset() + asset_name = AssetName(token.bytes_name) + assets[asset_name] = int(token.amount) + policy = ScriptHash.from_primitive(token.policy_id) + multi_asset[policy] = assets + + tx_in = TransactionInput.from_primitive([b"1" * 32, 0]) + tx_out = TransactionOutput( + address=wallet.address, + amount=Value( + coin=6000000, + multi_asset=multi_asset, + ), + ) + utxo = UTxO(tx_in, tx_out) + tx_cbor = wallet.burn_tokens(token, wallet.address, utxos=utxo, build_only=True) + assert tx_cbor is not None diff --git a/test/pycardano/util.py b/test/pycardano/util.py index 570e27c4..d9eee4c4 100644 --- a/test/pycardano/util.py +++ b/test/pycardano/util.py @@ -1,14 +1,26 @@ from typing import Dict, List, Union from unittest.mock import Mock, patch -import pytest from blockfrost import ApiError, BlockFrostApi -from pycardano import ExecutionUnits -from pycardano.backend.base import ChainContext, GenesisParameters, ProtocolParameters +from pycardano import ExecutionUnits, Address, ScriptHash +from pycardano.backend.base import ( + ChainContext, + GenesisParameters, + ProtocolParameters, + StakeAddressInfo, +) from pycardano.network import Network from pycardano.serialization import CBORSerializable -from pycardano.transaction import TransactionInput, TransactionOutput, UTxO +from pycardano.transaction import ( + TransactionInput, + TransactionOutput, + UTxO, + Value, + MultiAsset, + AssetName, + Asset, +) TEST_ADDR = "addr_test1vr2p8st5t5cxqglyjky7vk98k7jtfhdpvhl4e97cezuhn0cqcexl7" @@ -132,10 +144,15 @@ def submit_tx_cbor(self, cbor: Union[bytes, str]): def evaluate_tx_cbor(self, cbor: Union[bytes, str]) -> Dict[str, ExecutionUnits]: return {"spend:0": ExecutionUnits(399882, 175940720)} - -@pytest.fixture -def chain_context(): - return FixedChainContext() + def stake_address_info(self, address: str) -> List[StakeAddressInfo]: + return [ + StakeAddressInfo( + address="stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc", + delegation="pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", + delegation_deposit=2000000, + reward_account_balance=1000000, + ) + ] # Patch BlockFrostApi to avoid network calls From bf3bb7981c2bdec8fe7d8242abf50c11f8672464 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Tue, 18 Jun 2024 12:34:46 -0500 Subject: [PATCH 130/130] test: add test for cli stake address info --- test/pycardano/backend/test_cardano_cli.py | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/pycardano/backend/test_cardano_cli.py b/test/pycardano/backend/test_cardano_cli.py index b1004dd1..ac657047 100644 --- a/test/pycardano/backend/test_cardano_cli.py +++ b/test/pycardano/backend/test_cardano_cli.py @@ -482,6 +482,15 @@ }, } +STAKE_ADDRESS_INFO_RESULT = [ + { + "address": "stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc", + "delegation": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", + "delegationDeposit": 2000000, + "rewardAccountBalance": 1000000000, + } +] + def override_run_command(cmd: List[str]): """ @@ -498,6 +507,8 @@ def override_run_command(cmd: List[str]): return json.dumps(QUERY_PROTOCOL_PARAMETERS_RESULT) if "utxo" in cmd: return json.dumps(QUERY_UTXO_RESULT) + if "stake-address-info" in cmd: + return json.dumps(STAKE_ADDRESS_INFO_RESULT) if "txid" in cmd: return "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" if "version" in cmd: @@ -732,3 +743,19 @@ def test_submit_tx(self, chain_context): def test_epoch(self, chain_context): assert chain_context.epoch == 98 + + def test_stake_address_info(self, chain_context): + results = chain_context.stake_address_info( + "stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc" + ) + + assert ( + results[0].address + == "stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc" + ) + assert ( + results[0].delegation + == "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy" + ) + assert results[0].delegation_deposit == 2000000 + assert results[0].reward_account_balance == 1000000000