meshtrade 1.21.0__py3-none-any.whl → 1.23.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of meshtrade might be problematic. Click here for more details.

Files changed (44) hide show
  1. meshtrade/common/grpc_client.py +93 -0
  2. meshtrade/common/service_options.py +46 -0
  3. meshtrade/compliance/client/v1/__init__.py +0 -2
  4. meshtrade/compliance/client/v1/client_roles.py +123 -0
  5. meshtrade/compliance/client/v1/service_meshpy.py +1 -1
  6. meshtrade/iam/api_user/v1/__init__.py +0 -2
  7. meshtrade/iam/api_user/v1/api_user_state_machine.py +104 -0
  8. meshtrade/iam/api_user/v1/service_meshpy.py +1 -1
  9. meshtrade/iam/group/v1/__init__.py +0 -2
  10. meshtrade/iam/group/v1/service_meshpy.py +1 -1
  11. meshtrade/iam/role/v1/__init__.py +2 -2
  12. meshtrade/iam/role/v1/role.py +162 -11
  13. meshtrade/iam/user/v1/__init__.py +0 -2
  14. meshtrade/iam/user/v1/service_meshpy.py +1 -1
  15. meshtrade/ledger/transaction/v1/__init__.py +0 -2
  16. meshtrade/ledger/transaction/v1/service_meshpy.py +1 -1
  17. meshtrade/ledger/transaction/v1/transaction_state_machine.py +75 -0
  18. meshtrade/reporting/account_report/v1/__init__.py +0 -2
  19. meshtrade/reporting/account_report/v1/income_entry.py +35 -0
  20. meshtrade/reporting/account_report/v1/service_meshpy.py +1 -1
  21. meshtrade/trading/limit_order/v1/__init__.py +0 -2
  22. meshtrade/trading/limit_order/v1/service_meshpy.py +1 -1
  23. meshtrade/trading/market_order/v1/__init__.py +0 -2
  24. meshtrade/trading/market_order/v1/service_meshpy.py +1 -1
  25. meshtrade/type/v1/amount.py +429 -5
  26. meshtrade/type/v1/decimal_built_in_conversions.py +8 -3
  27. meshtrade/type/v1/decimal_operations.py +354 -0
  28. meshtrade/type/v1/ledger.py +76 -1
  29. meshtrade/type/v1/token.py +144 -0
  30. meshtrade/wallet/account/v1/__init__.py +0 -2
  31. meshtrade/wallet/account/v1/service_meshpy.py +1 -1
  32. {meshtrade-1.21.0.dist-info → meshtrade-1.23.0.dist-info}/METADATA +1 -1
  33. {meshtrade-1.21.0.dist-info → meshtrade-1.23.0.dist-info}/RECORD +35 -37
  34. meshtrade/compliance/client/v1/service_options_meshpy.py +0 -65
  35. meshtrade/iam/api_user/v1/service_options_meshpy.py +0 -65
  36. meshtrade/iam/group/v1/service_options_meshpy.py +0 -65
  37. meshtrade/iam/user/v1/service_options_meshpy.py +0 -65
  38. meshtrade/ledger/transaction/v1/service_options_meshpy.py +0 -65
  39. meshtrade/reporting/account_report/v1/service_options_meshpy.py +0 -65
  40. meshtrade/trading/limit_order/v1/service_options_meshpy.py +0 -65
  41. meshtrade/trading/market_order/v1/service_options_meshpy.py +0 -65
  42. meshtrade/wallet/account/v1/service_options_meshpy.py +0 -65
  43. {meshtrade-1.21.0.dist-info → meshtrade-1.23.0.dist-info}/WHEEL +0 -0
  44. {meshtrade-1.21.0.dist-info → meshtrade-1.23.0.dist-info}/top_level.txt +0 -0
@@ -28,7 +28,6 @@ from .service_meshpy import (
28
28
  TransactionServiceGRPCClient,
29
29
  TransactionServiceGRPCClientInterface,
30
30
  )
31
- from .service_options_meshpy import ClientOptions
32
31
 
33
32
  # ===================================================================
34
33
  # END OF AUTO-GENERATED SECTION
@@ -50,7 +49,6 @@ from .service_options_meshpy import ClientOptions
50
49
  # Combined auto-generated and manual exports
51
50
  __all__ = [
52
51
  # Generated exports
53
- "ClientOptions",
54
52
  "GetTransactionStateRequest",
55
53
  "GetTransactionStateResponse",
56
54
  "MonitorTransactionStateRequest",
@@ -15,7 +15,7 @@ from typing import Optional
15
15
  from meshtrade.common import BaseGRPCClient
16
16
  from meshtrade.iam.api_user.v1.api_credentials import find_credentials
17
17
 
18
- from .service_options_meshpy import ServiceOptions
18
+ from meshtrade.common.service_options import ServiceOptions
19
19
  from .service_pb2 import (
20
20
  GetTransactionStateRequest,
21
21
  GetTransactionStateResponse,
@@ -0,0 +1,75 @@
1
+ """Transaction state machine validation and transition logic.
2
+
3
+ This module provides state machine functions for transaction states and actions,
4
+ implementing the transaction lifecycle management logic.
5
+ """
6
+
7
+ from meshtrade.ledger.transaction.v1.transaction_action_pb2 import TransactionAction
8
+ from meshtrade.ledger.transaction.v1.transaction_state_pb2 import TransactionState
9
+
10
+
11
+ def transaction_state_can_perform_action_at_state(state: TransactionState | None, action: TransactionAction | None) -> bool:
12
+ """Check if the given action can be performed at the current transaction state.
13
+
14
+ This implements the state machine logic for transaction lifecycle management.
15
+
16
+ State Transitions:
17
+ - DRAFT -> BUILD/COMMIT -> SIGNING_IN_PROGRESS
18
+ - SIGNING_IN_PROGRESS -> SIGN/MARK_PENDING -> PENDING
19
+ - PENDING -> SUBMIT -> SUBMISSION_IN_PROGRESS
20
+ - SUBMISSION_IN_PROGRESS -> SUBMIT (retry) -> INDETERMINATE or SUCCESS/FAILED
21
+ - INDETERMINATE -> SUBMIT (retry) -> SUCCESS/FAILED
22
+ - FAILED/SUCCESSFUL -> No further actions allowed (terminal states)
23
+
24
+ Args:
25
+ state: The current TransactionState (can be None)
26
+ action: The TransactionAction to perform (can be None)
27
+
28
+ Returns:
29
+ True if the action can be performed at the given state, False otherwise
30
+
31
+ None Safety:
32
+ Returns False if either state or action is None
33
+
34
+ Example:
35
+ >>> transaction_state_can_perform_action_at_state(
36
+ ... TransactionState.TRANSACTION_STATE_DRAFT,
37
+ ... TransactionAction.TRANSACTION_ACTION_BUILD
38
+ ... )
39
+ True
40
+ >>> transaction_state_can_perform_action_at_state(
41
+ ... TransactionState.TRANSACTION_STATE_SUCCESSFUL,
42
+ ... TransactionAction.TRANSACTION_ACTION_SUBMIT
43
+ ... )
44
+ False
45
+ """
46
+ if state is None or action is None:
47
+ return False
48
+
49
+ if state == TransactionState.TRANSACTION_STATE_DRAFT:
50
+ return action in {
51
+ TransactionAction.TRANSACTION_ACTION_BUILD,
52
+ TransactionAction.TRANSACTION_ACTION_COMMIT,
53
+ }
54
+
55
+ elif state == TransactionState.TRANSACTION_STATE_SIGNING_IN_PROGRESS:
56
+ return action in {
57
+ TransactionAction.TRANSACTION_ACTION_SIGN,
58
+ TransactionAction.TRANSACTION_ACTION_MARK_PENDING,
59
+ }
60
+
61
+ elif state in {
62
+ TransactionState.TRANSACTION_STATE_PENDING,
63
+ TransactionState.TRANSACTION_STATE_SUBMISSION_IN_PROGRESS,
64
+ TransactionState.TRANSACTION_STATE_INDETERMINATE,
65
+ }:
66
+ return action == TransactionAction.TRANSACTION_ACTION_SUBMIT
67
+
68
+ elif state in {
69
+ TransactionState.TRANSACTION_STATE_FAILED,
70
+ TransactionState.TRANSACTION_STATE_SUCCESSFUL,
71
+ }:
72
+ return False
73
+
74
+ else:
75
+ return False
@@ -26,7 +26,6 @@ from .service_meshpy import (
26
26
  AccountReportServiceGRPCClient,
27
27
  AccountReportServiceGRPCClientInterface,
28
28
  )
29
- from .service_options_meshpy import ClientOptions
30
29
 
31
30
  # ===================================================================
32
31
  # END OF AUTO-GENERATED SECTION
@@ -52,7 +51,6 @@ __all__ = [
52
51
  "AccountReportService",
53
52
  "AccountReportServiceGRPCClient",
54
53
  "AccountReportServiceGRPCClientInterface",
55
- "ClientOptions",
56
54
  "Disclaimer",
57
55
  "FeeEntry",
58
56
  "GetAccountReportRequest",
@@ -0,0 +1,35 @@
1
+ """Income entry utility functions."""
2
+
3
+ from meshtrade.reporting.account_report.v1.income_entry_pb2 import IncomeNarrative
4
+
5
+
6
+ def income_narrative_pretty_string(narrative: IncomeNarrative) -> str:
7
+ """Generate human-readable string for income narrative.
8
+
9
+ Converts enum values to concise, display-friendly strings suitable for
10
+ reports and user interfaces.
11
+
12
+ Args:
13
+ narrative: IncomeNarrative enum value
14
+
15
+ Returns:
16
+ Pretty-printed string representation:
17
+ - "-" for unspecified narratives
18
+ - Descriptive names for known types (e.g., "Yield", "Dividend")
19
+ - Empty string for unknown values
20
+ """
21
+ if narrative == IncomeNarrative.INCOME_NARRATIVE_UNSPECIFIED:
22
+ return "-"
23
+ if narrative == IncomeNarrative.INCOME_NARRATIVE_YIELD:
24
+ return "Yield"
25
+ if narrative == IncomeNarrative.INCOME_NARRATIVE_DIVIDEND:
26
+ return "Dividend"
27
+ if narrative == IncomeNarrative.INCOME_NARRATIVE_INTEREST:
28
+ return "Interest"
29
+ if narrative == IncomeNarrative.INCOME_NARRATIVE_PRINCIPAL:
30
+ return "Principal"
31
+ if narrative == IncomeNarrative.INCOME_NARRATIVE_DISTRIBUTION:
32
+ return "Distribution"
33
+ if narrative == IncomeNarrative.INCOME_NARRATIVE_PROFIT_DISTRIBUTION:
34
+ return "Profit Distribution"
35
+ return ""
@@ -16,7 +16,7 @@ from meshtrade.common import BaseGRPCClient
16
16
  from meshtrade.iam.api_user.v1.api_credentials import find_credentials
17
17
 
18
18
  from .account_report_pb2 import AccountReport
19
- from .service_options_meshpy import ServiceOptions
19
+ from meshtrade.common.service_options import ServiceOptions
20
20
  from .service_pb2 import (
21
21
  GetAccountReportRequest,
22
22
  GetExcelAccountReportRequest,
@@ -22,7 +22,6 @@ from .service_meshpy import (
22
22
  LimitOrderServiceGRPCClient,
23
23
  LimitOrderServiceGRPCClientInterface,
24
24
  )
25
- from .service_options_meshpy import ClientOptions
26
25
 
27
26
  # ===================================================================
28
27
  # END OF AUTO-GENERATED SECTION
@@ -44,7 +43,6 @@ from .service_options_meshpy import ClientOptions
44
43
  # Combined auto-generated and manual exports
45
44
  __all__ = [
46
45
  # Generated exports
47
- "ClientOptions",
48
46
  "GetLimitOrderRequest",
49
47
  "LimitOrder",
50
48
  "LimitOrderService",
@@ -16,7 +16,7 @@ from meshtrade.common import BaseGRPCClient
16
16
  from meshtrade.iam.api_user.v1.api_credentials import find_credentials
17
17
 
18
18
  from .limit_order_pb2 import LimitOrder
19
- from .service_options_meshpy import ServiceOptions
19
+ from meshtrade.common.service_options import ServiceOptions
20
20
  from .service_pb2 import (
21
21
  GetLimitOrderRequest,
22
22
  )
@@ -22,7 +22,6 @@ from .service_meshpy import (
22
22
  MarketOrderServiceGRPCClient,
23
23
  MarketOrderServiceGRPCClientInterface,
24
24
  )
25
- from .service_options_meshpy import ClientOptions
26
25
 
27
26
  # ===================================================================
28
27
  # END OF AUTO-GENERATED SECTION
@@ -44,7 +43,6 @@ from .service_options_meshpy import ClientOptions
44
43
  # Combined auto-generated and manual exports
45
44
  __all__ = [
46
45
  # Generated exports
47
- "ClientOptions",
48
46
  "GetMarketOrderRequest",
49
47
  "MarketOrder",
50
48
  "MarketOrderService",
@@ -16,7 +16,7 @@ from meshtrade.common import BaseGRPCClient
16
16
  from meshtrade.iam.api_user.v1.api_credentials import find_credentials
17
17
 
18
18
  from .market_order_pb2 import MarketOrder
19
- from .service_options_meshpy import ServiceOptions
19
+ from meshtrade.common.service_options import ServiceOptions
20
20
  from .service_pb2 import (
21
21
  GetMarketOrderRequest,
22
22
  )
@@ -1,16 +1,21 @@
1
- """
2
- This module provides a factory function for creating Amount protobuf messages.
1
+ """Amount utility functions for Mesh API.
2
+
3
+ This module provides utility functions for working with Amount protobuf messages,
4
+ including creation, validation, comparison, and arithmetic operations.
3
5
  """
4
6
 
5
- from decimal import ROUND_DOWN, Decimal
7
+ from decimal import ROUND_DOWN
8
+ from decimal import Decimal as PyDecimal
6
9
 
7
10
  from .amount_pb2 import Amount
8
11
  from .decimal_built_in_conversions import built_in_to_decimal, decimal_to_built_in
12
+ from .decimal_pb2 import Decimal
9
13
  from .ledger import get_ledger_no_decimal_places
14
+ from .token import new_undefined_token
10
15
  from .token_pb2 import Token
11
16
 
12
17
 
13
- def new_amount(value: Decimal, token: Token, precision_loss_tolerance: Decimal = Decimal("0.00000001")) -> Amount:
18
+ def new_amount(value: PyDecimal, token: Token, precision_loss_tolerance: PyDecimal = PyDecimal("0.00000001")) -> Amount:
14
19
  """Creates a new Amount, ensuring the value conforms to system-wide limits.
15
20
 
16
21
  This function is the safe constructor for creating Amount protobuf messages.
@@ -67,7 +72,7 @@ def new_amount(value: Decimal, token: Token, precision_loss_tolerance: Decimal =
67
72
  # Truncate the validated value to the number of decimal places specified by the
68
73
  # token's ledger. ROUND_DOWN ensures the value is never inflated.
69
74
  truncated_value = value_after_roundtrip.quantize(
70
- Decimal(10) ** -get_ledger_no_decimal_places(token.ledger),
75
+ PyDecimal(10) ** -get_ledger_no_decimal_places(token.ledger),
71
76
  rounding=ROUND_DOWN,
72
77
  )
73
78
 
@@ -76,3 +81,422 @@ def new_amount(value: Decimal, token: Token, precision_loss_tolerance: Decimal =
76
81
  token=token,
77
82
  value=built_in_to_decimal(truncated_value),
78
83
  )
84
+
85
+
86
+ def new_undefined_amount(value: PyDecimal) -> Amount:
87
+ """Create a new Amount with the specified value and an undefined token.
88
+
89
+ This is useful as a placeholder or when the token type is not yet known.
90
+ Since undefined tokens don't have a valid ledger, this bypasses ledger
91
+ validation and creates the amount directly with full precision.
92
+
93
+ Args:
94
+ value: The decimal value for the amount
95
+
96
+ Returns:
97
+ A new Amount with the specified value and an undefined token
98
+
99
+ Example:
100
+ >>> from decimal import Decimal as PyDecimal
101
+ >>> amount = new_undefined_amount(PyDecimal("100"))
102
+ >>> amount_is_undefined(amount)
103
+ True
104
+ """
105
+ # Undefined tokens don't have a valid ledger, so we create the Amount directly
106
+ # without going through new_amount() which requires ledger validation
107
+ return Amount(
108
+ value=built_in_to_decimal(value),
109
+ token=new_undefined_token(),
110
+ )
111
+
112
+
113
+ def amount_set_value(
114
+ amount: Amount,
115
+ value: PyDecimal,
116
+ precision_loss_tolerance: PyDecimal = PyDecimal("0.00000001"),
117
+ ) -> Amount:
118
+ """Create a new Amount with the given value and the same token as the input amount.
119
+
120
+ Despite its name, this function does NOT modify the input - it creates and returns a NEW Amount.
121
+
122
+ Args:
123
+ amount: The amount whose token to use
124
+ value: The decimal value for the new amount
125
+ precision_loss_tolerance: The maximum acceptable difference after validation
126
+ round-trip. Defaults to a small tolerance for robustness.
127
+
128
+ Returns:
129
+ A new Amount with the specified value and the same token
130
+
131
+ Raises:
132
+ ValueError: If amount is None
133
+ AssertionError: If the value exceeds system precision limits
134
+
135
+ Example:
136
+ >>> from decimal import Decimal as PyDecimal
137
+ >>> original = new_undefined_amount(PyDecimal("100"))
138
+ >>> modified = amount_set_value(original, PyDecimal("200"))
139
+ >>> # original is unchanged, modified is a new Amount with value 200
140
+ """
141
+ if amount is None:
142
+ raise ValueError("amount cannot be None")
143
+
144
+ # Check if the token is undefined (doesn't have a valid ledger)
145
+ from .token import token_is_undefined
146
+
147
+ if token_is_undefined(amount.token):
148
+ # For undefined tokens, create Amount directly without ledger validation
149
+ return Amount(
150
+ value=built_in_to_decimal(value),
151
+ token=amount.token,
152
+ )
153
+
154
+ return new_amount(value, amount.token, precision_loss_tolerance)
155
+
156
+
157
+ def amount_is_undefined(amount: Amount | None) -> bool:
158
+ """Check whether this amount has an undefined token.
159
+
160
+ An amount is considered undefined if its associated token is undefined.
161
+
162
+ Args:
163
+ amount: Amount to check (can be None)
164
+
165
+ Returns:
166
+ True if the amount's token is undefined or if amount is None, False otherwise
167
+
168
+ None Safety:
169
+ Returns True if amount is None
170
+
171
+ Example:
172
+ >>> amount = new_undefined_amount(PyDecimal("100"))
173
+ >>> amount_is_undefined(amount)
174
+ True
175
+ """
176
+ if amount is None:
177
+ return True
178
+
179
+ from .token import token_is_undefined
180
+
181
+ return token_is_undefined(amount.token)
182
+
183
+
184
+ def amount_is_same_type_as(amount1: Amount | None, amount2: Amount | None) -> bool:
185
+ """Check if two amounts have the same token type (same currency/asset).
186
+
187
+ This is useful for validating that amounts can be compared or combined arithmetically.
188
+
189
+ Args:
190
+ amount1: First amount (can be None)
191
+ amount2: Second amount (can be None)
192
+
193
+ Returns:
194
+ True if both amounts have equal tokens, False otherwise
195
+
196
+ None Safety:
197
+ Returns False if either amount is None
198
+
199
+ Example:
200
+ >>> usd_amount1 = new_undefined_amount(PyDecimal("100"))
201
+ >>> usd_amount2 = new_undefined_amount(PyDecimal("200"))
202
+ >>> amount_is_same_type_as(usd_amount1, usd_amount2)
203
+ True
204
+ """
205
+ if amount1 is None or amount2 is None:
206
+ return False
207
+
208
+ from .token import token_is_equal_to
209
+
210
+ return token_is_equal_to(amount1.token, amount2.token)
211
+
212
+
213
+ def amount_is_equal_to(amount1: Amount | None, amount2: Amount | None) -> bool:
214
+ """Check if two amounts are equal in both value and token type.
215
+
216
+ Two amounts are considered equal if they have the same decimal value AND the same token.
217
+
218
+ Args:
219
+ amount1: First amount (can be None)
220
+ amount2: Second amount (can be None)
221
+
222
+ Returns:
223
+ True if both amounts have equal values and tokens (or both are None), False otherwise
224
+
225
+ None Safety:
226
+ Returns True if both are None, False if only one is None
227
+
228
+ Example:
229
+ >>> amount1 = new_undefined_amount(PyDecimal("100"))
230
+ >>> amount2 = new_undefined_amount(PyDecimal("100"))
231
+ >>> amount_is_equal_to(amount1, amount2)
232
+ True
233
+ """
234
+ if amount1 is None and amount2 is None:
235
+ return True
236
+ if amount1 is None or amount2 is None:
237
+ return False
238
+
239
+ from .decimal_operations import decimal_equal
240
+ from .token import token_is_equal_to
241
+
242
+ return token_is_equal_to(amount1.token, amount2.token) and decimal_equal(amount1.value, amount2.value)
243
+
244
+
245
+ def amount_is_negative(amount: Amount | None) -> bool:
246
+ """Check whether the amount's value is less than zero.
247
+
248
+ Args:
249
+ amount: Amount to check (can be None)
250
+
251
+ Returns:
252
+ True if the value is negative (< 0), False otherwise
253
+
254
+ None Safety:
255
+ Returns False if amount is None
256
+
257
+ Example:
258
+ >>> amount = new_undefined_amount(PyDecimal("-50"))
259
+ >>> amount_is_negative(amount)
260
+ True
261
+ """
262
+ if amount is None:
263
+ return False
264
+
265
+ from .decimal_operations import decimal_is_negative
266
+
267
+ return decimal_is_negative(amount.value)
268
+
269
+
270
+ def amount_is_zero(amount: Amount | None) -> bool:
271
+ """Check whether the amount's value is exactly zero.
272
+
273
+ Args:
274
+ amount: Amount to check (can be None)
275
+
276
+ Returns:
277
+ True if the value is zero, False otherwise
278
+
279
+ None Safety:
280
+ Returns False if amount is None
281
+
282
+ Example:
283
+ >>> amount = new_undefined_amount(PyDecimal("0"))
284
+ >>> amount_is_zero(amount)
285
+ True
286
+ """
287
+ if amount is None:
288
+ return False
289
+
290
+ from .decimal_operations import decimal_is_zero
291
+
292
+ return decimal_is_zero(amount.value)
293
+
294
+
295
+ def amount_contains_fractions(amount: Amount | None) -> bool:
296
+ """Check whether the amount's value has any fractional (decimal) component.
297
+
298
+ This is useful for determining if an amount can be represented as a whole number.
299
+
300
+ Args:
301
+ amount: Amount to check (can be None)
302
+
303
+ Returns:
304
+ True if the value has fractional/decimal places, False otherwise
305
+
306
+ None Safety:
307
+ Returns False if amount is None
308
+
309
+ Example:
310
+ >>> amount1 = new_undefined_amount(PyDecimal("100.50"))
311
+ >>> amount_contains_fractions(amount1)
312
+ True
313
+ >>> amount2 = new_undefined_amount(PyDecimal("100"))
314
+ >>> amount_contains_fractions(amount2)
315
+ False
316
+ """
317
+ if amount is None:
318
+ return False
319
+
320
+ # Convert protobuf Decimal to Python Decimal
321
+ value = PyDecimal(amount.value.value) if amount.value and amount.value.value else PyDecimal(0)
322
+
323
+ # Check if truncating to 0 decimal places changes the value
324
+ return value.quantize(PyDecimal("1"), rounding=ROUND_DOWN) != value
325
+
326
+
327
+ def amount_add(
328
+ amount1: Amount,
329
+ amount2: Amount,
330
+ precision_loss_tolerance: PyDecimal = PyDecimal("0.00000001"),
331
+ ) -> Amount:
332
+ """Add two amounts and return a new amount with the result.
333
+
334
+ The amounts must have the same token type (currency/asset).
335
+
336
+ Args:
337
+ amount1: First amount
338
+ amount2: Second amount (must have same token as amount1)
339
+ precision_loss_tolerance: The maximum acceptable difference after validation
340
+ round-trip. Defaults to a small tolerance for robustness.
341
+
342
+ Returns:
343
+ A new Amount containing the sum (amount1 + amount2)
344
+
345
+ Raises:
346
+ ValueError: If either amount is None or if the amounts have different token types
347
+ AssertionError: If the result exceeds system precision limits
348
+
349
+ Example:
350
+ >>> amount1 = new_undefined_amount(PyDecimal("100"))
351
+ >>> amount2 = new_undefined_amount(PyDecimal("30"))
352
+ >>> result = amount_add(amount1, amount2)
353
+ >>> # result value is 130
354
+ """
355
+ if amount1 is None:
356
+ raise ValueError("amount1 cannot be None")
357
+ if amount2 is None:
358
+ raise ValueError("amount2 cannot be None")
359
+
360
+ from .decimal_operations import decimal_add
361
+ from .token import token_is_equal_to, token_pretty_string
362
+
363
+ if not token_is_equal_to(amount1.token, amount2.token):
364
+ raise ValueError(
365
+ f"cannot do arithmetic on amounts of different token denominations: "
366
+ f"{token_pretty_string(amount1.token)} vs. {token_pretty_string(amount2.token)}"
367
+ )
368
+
369
+ new_value_decimal = decimal_add(amount1.value, amount2.value)
370
+ new_value_py = PyDecimal(new_value_decimal.value) if new_value_decimal.value else PyDecimal(0)
371
+
372
+ return amount_set_value(amount1, new_value_py, precision_loss_tolerance)
373
+
374
+
375
+ def amount_sub(
376
+ amount1: Amount,
377
+ amount2: Amount,
378
+ precision_loss_tolerance: PyDecimal = PyDecimal("0.00000001"),
379
+ ) -> Amount:
380
+ """Subtract amount2 from amount1 and return a new amount with the result.
381
+
382
+ The amounts must have the same token type (currency/asset).
383
+
384
+ Args:
385
+ amount1: First amount
386
+ amount2: Second amount to subtract (must have same token as amount1)
387
+ precision_loss_tolerance: The maximum acceptable difference after validation
388
+ round-trip. Defaults to a small tolerance for robustness.
389
+
390
+ Returns:
391
+ A new Amount containing the difference (amount1 - amount2)
392
+
393
+ Raises:
394
+ ValueError: If either amount is None or if the amounts have different token types
395
+ AssertionError: If the result exceeds system precision limits
396
+
397
+ Example:
398
+ >>> amount1 = new_undefined_amount(PyDecimal("100"))
399
+ >>> amount2 = new_undefined_amount(PyDecimal("30"))
400
+ >>> result = amount_sub(amount1, amount2)
401
+ >>> # result value is 70
402
+ """
403
+ if amount1 is None:
404
+ raise ValueError("amount1 cannot be None")
405
+ if amount2 is None:
406
+ raise ValueError("amount2 cannot be None")
407
+
408
+ from .decimal_operations import decimal_sub
409
+ from .token import token_is_equal_to, token_pretty_string
410
+
411
+ if not token_is_equal_to(amount1.token, amount2.token):
412
+ raise ValueError(
413
+ f"cannot do arithmetic on amounts of different token denominations: "
414
+ f"{token_pretty_string(amount1.token)} vs. {token_pretty_string(amount2.token)}"
415
+ )
416
+
417
+ new_value_decimal = decimal_sub(amount1.value, amount2.value)
418
+ new_value_py = PyDecimal(new_value_decimal.value) if new_value_decimal.value else PyDecimal(0)
419
+
420
+ return amount_set_value(amount1, new_value_py, precision_loss_tolerance)
421
+
422
+
423
+ def amount_decimal_mul(
424
+ amount: Amount,
425
+ multiplier: PyDecimal,
426
+ precision_loss_tolerance: PyDecimal = PyDecimal("0.00000001"),
427
+ ) -> Amount:
428
+ """Multiply this amount by a decimal value and return a new amount with the result.
429
+
430
+ The token type is preserved.
431
+
432
+ Args:
433
+ amount: The amount to multiply
434
+ multiplier: The decimal multiplier
435
+ precision_loss_tolerance: The maximum acceptable difference after validation
436
+ round-trip. Defaults to a small tolerance for robustness.
437
+
438
+ Returns:
439
+ A new Amount containing the product (amount * multiplier)
440
+
441
+ Raises:
442
+ ValueError: If amount is None
443
+ AssertionError: If the result exceeds system precision limits
444
+
445
+ Example:
446
+ >>> amount = new_undefined_amount(PyDecimal("100"))
447
+ >>> result = amount_decimal_mul(amount, PyDecimal("2"))
448
+ >>> # result value is 200
449
+ """
450
+ if amount is None:
451
+ raise ValueError("amount cannot be None")
452
+
453
+ from .decimal_operations import decimal_mul
454
+
455
+ multiplier_decimal = Decimal(value=str(multiplier))
456
+ new_value_decimal = decimal_mul(amount.value, multiplier_decimal)
457
+ new_value_py = PyDecimal(new_value_decimal.value) if new_value_decimal.value else PyDecimal(0)
458
+
459
+ return amount_set_value(amount, new_value_py, precision_loss_tolerance)
460
+
461
+
462
+ def amount_decimal_div(
463
+ amount: Amount,
464
+ divisor: PyDecimal,
465
+ precision_loss_tolerance: PyDecimal = PyDecimal("0.00000001"),
466
+ ) -> Amount:
467
+ """Divide this amount by a decimal value and return a new amount with the result.
468
+
469
+ The token type is preserved.
470
+
471
+ Args:
472
+ amount: The amount to divide
473
+ divisor: The decimal divisor (must not be zero)
474
+ precision_loss_tolerance: The maximum acceptable difference after validation
475
+ round-trip. Defaults to a small tolerance for robustness.
476
+
477
+ Returns:
478
+ A new Amount containing the quotient (amount / divisor)
479
+
480
+ Raises:
481
+ ValueError: If amount is None
482
+ ZeroDivisionError: If divisor is zero
483
+ AssertionError: If the result exceeds system precision limits
484
+
485
+ Example:
486
+ >>> amount = new_undefined_amount(PyDecimal("100"))
487
+ >>> result = amount_decimal_div(amount, PyDecimal("4"))
488
+ >>> # result value is 25
489
+ """
490
+ if amount is None:
491
+ raise ValueError("amount cannot be None")
492
+
493
+ if divisor == 0:
494
+ raise ZeroDivisionError("cannot divide amount by zero")
495
+
496
+ from .decimal_operations import decimal_div
497
+
498
+ divisor_decimal = Decimal(value=str(divisor))
499
+ new_value_decimal = decimal_div(amount.value, divisor_decimal)
500
+ new_value_py = PyDecimal(new_value_decimal.value) if new_value_decimal.value else PyDecimal(0)
501
+
502
+ return amount_set_value(amount, new_value_py, precision_loss_tolerance)