-
Notifications
You must be signed in to change notification settings - Fork 46
/
mint.py
253 lines (228 loc) · 7.83 KB
/
mint.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
import logging
import math
from pathlib import Path
import click
from algosdk.error import AlgodHTTPError
from algosdk.util import algos_to_microalgos
from algokit.cli.common.constants import AlgorandNetwork, ExplorerEntityType
from algokit.cli.common.utils import get_explorer_url
from algokit.cli.tasks.utils import (
get_account_with_private_key,
load_algod_client,
validate_balance,
)
from algokit.core.tasks.ipfs import (
PinataBadRequestError,
PinataForbiddenError,
PinataHttpError,
PinataInternalServerError,
PinataUnauthorizedError,
get_pinata_jwt,
)
from algokit.core.tasks.mint.mint import mint_token
from algokit.core.tasks.mint.models import TokenMetadata
logger = logging.getLogger(__name__)
MAX_UNIT_NAME_BYTE_LENGTH = 8
MAX_ASSET_NAME_BYTE_LENGTH = 32
ASSET_MINTING_MBR = 0.2 # Algos, 0.1 for base account, 0.1 for asset creation
def _validate_supply(total: int, decimals: int) -> None:
"""
Validate the total supply and decimal places of a token.
Args:
total (int): The total supply of the token.
decimals (int): The number of decimal places for the token.
Raises:
click.ClickException: If the validation fails.
"""
if not (total == 1 or (total % 10 == 0 and total != 0)):
raise click.ClickException("Total must be 1 or a power of 10 larger than 1 (10, 100, 1000, ...).")
if not ((total == 1 and decimals == 0) or (total != 1 and decimals == int(math.log10(total)))):
raise click.ClickException(
"Number of digits after the decimal point must be 0 for a pure NFT, or "
"equal to the logarithm in base 10 of total number of units for a fractional NFT."
)
def _validate_unit_name(context: click.Context, param: click.Parameter, value: str) -> str:
"""
Validate the unit name by checking if its byte length is less than or equal to a predefined maximum value.
Args:
context (click.Context): The click context.
param (click.Parameter): The click parameter.
value (str): The value of the parameter.
Returns:
str: The value of the parameter if it passes the validation.
"""
if len(value.encode("utf-8")) <= MAX_UNIT_NAME_BYTE_LENGTH:
return value
else:
raise click.BadParameter(
f"Unit name must be {MAX_UNIT_NAME_BYTE_LENGTH} bytes or less.", ctx=context, param=param
)
def _validate_asset_name(context: click.Context, param: click.Parameter, value: str) -> str:
"""
Validate the asset name by checking if its byte length is less than or equal to a predefined maximum value.
Args:
context (click.Context): The click context.
param (click.Parameter): The click parameter.
value (str): The value of the parameter.
Returns:
str: The value of the parameter if it passes the validation.
"""
if len(value.encode("utf-8")) <= MAX_ASSET_NAME_BYTE_LENGTH:
return value
else:
raise click.BadParameter(
f"Unit name must be {MAX_UNIT_NAME_BYTE_LENGTH} bytes or less.", ctx=context, param=param
)
@click.command(
name="mint",
help="Mint new fungible or non-fungible assets on Algorand.",
)
@click.option(
"--creator",
required=True,
prompt="Provide the address or alias of the asset creator",
help="Address or alias of the asset creator.",
type=click.STRING,
)
@click.option(
"-n",
"--name",
"asset_name",
type=click.STRING,
required=True,
callback=_validate_asset_name,
prompt="Provide the asset name",
help="Asset name.",
)
@click.option(
"-u",
"--unit",
"unit_name",
type=click.STRING,
required=True,
callback=_validate_unit_name,
prompt="Provide the unit name",
help="Unit name of the asset.",
)
@click.option(
"-t",
"--total",
type=click.INT,
required=False,
default=1,
prompt="Provide the total supply",
help="Total supply of the asset. Defaults to 1.",
)
@click.option(
"-d",
"--decimals",
type=click.INT,
required=False,
default=0,
prompt="Provide the number of decimals",
help="Number of decimals. Defaults to 0.",
)
@click.option(
"-i",
"--image",
"image_path",
type=click.Path(exists=True, dir_okay=False, file_okay=True, resolve_path=True, path_type=Path),
prompt="Provide the path to the asset image file",
help="Path to the asset image file to be uploaded to IPFS.",
required=True,
)
@click.option(
"-m",
"--metadata",
"token_metadata_path",
type=click.Path(exists=True, dir_okay=False, file_okay=True, resolve_path=True, path_type=Path),
help="""Path to the ARC19 compliant asset metadata file to be uploaded to IPFS. If not provided,
a default metadata object will be generated automatically based on asset-name, decimals and image.
For more details refer to https://arc.algorand.foundation/ARCs/arc-0003#json-metadata-file-schema.""",
default=None,
required=False,
)
@click.option(
"--mutable/--immutable",
type=click.BOOL,
prompt="Would you like to make the asset mutable?",
default=False,
help="Whether the asset should be mutable or immutable. Refers to `ARC19` by default.",
)
@click.option(
"--nft/--ft",
"non_fungible",
type=click.BOOL,
prompt="Validate asset as NFT? Checks values of `total` and `decimals` as per ARC3 if set to True.",
default=False,
help="""Whether the asset should be validated as NFT or FT. Refers to NFT by default and validates canonical
definitions of pure or fractional NFTs as per ARC3 standard.""",
)
@click.option(
"-n",
"--network",
type=click.Choice(AlgorandNetwork.to_list()),
prompt="Provide the network to use",
default=AlgorandNetwork.LOCALNET,
required=False,
help=f"Network to use. Refers to `{AlgorandNetwork.LOCALNET}` by default.",
)
def mint( # noqa: PLR0913
*,
creator: str,
asset_name: str,
unit_name: str,
total: int,
decimals: int,
image_path: Path,
token_metadata_path: Path | None,
mutable: bool,
non_fungible: bool,
network: AlgorandNetwork,
) -> None:
if non_fungible:
_validate_supply(total, decimals)
creator_account = get_account_with_private_key(creator)
pinata_jwt = get_pinata_jwt()
if not pinata_jwt:
raise click.ClickException("You are not logged in! Please login using `algokit task ipfs login`.")
client = load_algod_client(network)
validate_balance(
client,
creator_account,
0,
algos_to_microalgos(ASSET_MINTING_MBR), # type: ignore[no-untyped-call]
)
token_metadata = TokenMetadata.from_json_file(token_metadata_path)
if not token_metadata_path:
token_metadata.name = asset_name
token_metadata.decimals = decimals
try:
asset_id, txn_id = mint_token(
client=client,
jwt=pinata_jwt,
creator_account=creator_account,
token_metadata=token_metadata,
image_path=image_path,
unit_name=unit_name,
asset_name=asset_name,
mutable=mutable,
total=total,
)
click.echo("\nSuccessfully minted the asset!")
click.echo(f"Browse your asset at: {get_explorer_url(asset_id, network, ExplorerEntityType.ASSET)}")
click.echo(f"Check transaction status at: {get_explorer_url(txn_id, network, ExplorerEntityType.TRANSACTION)}")
except (
PinataBadRequestError,
PinataUnauthorizedError,
PinataForbiddenError,
PinataInternalServerError,
PinataHttpError,
) as ex:
logger.debug(ex)
raise click.ClickException(repr(ex)) from ex
except AlgodHTTPError as ex:
raise click.ClickException(str(ex)) from ex
except Exception as ex:
logger.debug(ex, exc_info=True)
raise click.ClickException("Failed to mint the asset!") from ex