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.
- meshtrade/common/grpc_client.py +93 -0
- meshtrade/common/service_options.py +46 -0
- meshtrade/compliance/client/v1/__init__.py +0 -2
- meshtrade/compliance/client/v1/client_roles.py +123 -0
- meshtrade/compliance/client/v1/service_meshpy.py +1 -1
- meshtrade/iam/api_user/v1/__init__.py +0 -2
- meshtrade/iam/api_user/v1/api_user_state_machine.py +104 -0
- meshtrade/iam/api_user/v1/service_meshpy.py +1 -1
- meshtrade/iam/group/v1/__init__.py +0 -2
- meshtrade/iam/group/v1/service_meshpy.py +1 -1
- meshtrade/iam/role/v1/__init__.py +2 -2
- meshtrade/iam/role/v1/role.py +162 -11
- meshtrade/iam/user/v1/__init__.py +0 -2
- meshtrade/iam/user/v1/service_meshpy.py +1 -1
- meshtrade/ledger/transaction/v1/__init__.py +0 -2
- meshtrade/ledger/transaction/v1/service_meshpy.py +1 -1
- meshtrade/ledger/transaction/v1/transaction_state_machine.py +75 -0
- meshtrade/reporting/account_report/v1/__init__.py +0 -2
- meshtrade/reporting/account_report/v1/income_entry.py +35 -0
- meshtrade/reporting/account_report/v1/service_meshpy.py +1 -1
- meshtrade/trading/limit_order/v1/__init__.py +0 -2
- meshtrade/trading/limit_order/v1/service_meshpy.py +1 -1
- meshtrade/trading/market_order/v1/__init__.py +0 -2
- meshtrade/trading/market_order/v1/service_meshpy.py +1 -1
- meshtrade/type/v1/amount.py +429 -5
- meshtrade/type/v1/decimal_built_in_conversions.py +8 -3
- meshtrade/type/v1/decimal_operations.py +354 -0
- meshtrade/type/v1/ledger.py +76 -1
- meshtrade/type/v1/token.py +144 -0
- meshtrade/wallet/account/v1/__init__.py +0 -2
- meshtrade/wallet/account/v1/service_meshpy.py +1 -1
- {meshtrade-1.21.0.dist-info → meshtrade-1.23.0.dist-info}/METADATA +1 -1
- {meshtrade-1.21.0.dist-info → meshtrade-1.23.0.dist-info}/RECORD +35 -37
- meshtrade/compliance/client/v1/service_options_meshpy.py +0 -65
- meshtrade/iam/api_user/v1/service_options_meshpy.py +0 -65
- meshtrade/iam/group/v1/service_options_meshpy.py +0 -65
- meshtrade/iam/user/v1/service_options_meshpy.py +0 -65
- meshtrade/ledger/transaction/v1/service_options_meshpy.py +0 -65
- meshtrade/reporting/account_report/v1/service_options_meshpy.py +0 -65
- meshtrade/trading/limit_order/v1/service_options_meshpy.py +0 -65
- meshtrade/trading/market_order/v1/service_options_meshpy.py +0 -65
- meshtrade/wallet/account/v1/service_options_meshpy.py +0 -65
- {meshtrade-1.21.0.dist-info → meshtrade-1.23.0.dist-info}/WHEEL +0 -0
- {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 .
|
|
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 .
|
|
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 .
|
|
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 .
|
|
19
|
+
from meshtrade.common.service_options import ServiceOptions
|
|
20
20
|
from .service_pb2 import (
|
|
21
21
|
GetMarketOrderRequest,
|
|
22
22
|
)
|
meshtrade/type/v1/amount.py
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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)
|