Skip to content

Commit 70bf96f

Browse files
authored
Support AMMClawback amendment (#802)
1 parent 9ceb380 commit 70bf96f

File tree

8 files changed

+272
-0
lines changed

8 files changed

+272
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [[Unreleased]]
99

1010
### Added
11+
- Support `AMMClawback` amendment (XLS-73d).
1112
- Support for the `simulate` RPC ([XLS-69](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0069d-simulate))
1213

1314
### Fixed

tests/integration/it_utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ def _get_non_decorator_code(function):
321321

322322
def create_amm_pool(
323323
client: SyncClient = JSON_RPC_CLIENT,
324+
enable_amm_clawback: bool = False,
324325
) -> Dict[str, Any]:
325326
issuer_wallet = Wallet.create()
326327
fund_wallet(issuer_wallet)
@@ -337,6 +338,16 @@ def create_amm_pool(
337338
issuer_wallet,
338339
)
339340

341+
# The below flag is required for AMMClawback tests
342+
if enable_amm_clawback:
343+
sign_and_reliable_submission(
344+
AccountSet(
345+
account=issuer_wallet.classic_address,
346+
set_flag=AccountSetAsfFlag.ASF_ALLOW_TRUSTLINE_CLAWBACK,
347+
),
348+
issuer_wallet,
349+
)
350+
340351
sign_and_reliable_submission(
341352
TrustSet(
342353
account=lp_wallet.classic_address,
@@ -388,11 +399,13 @@ def create_amm_pool(
388399
"asset": asset,
389400
"asset2": asset2,
390401
"issuer_wallet": issuer_wallet,
402+
"lp_wallet": lp_wallet,
391403
}
392404

393405

394406
async def create_amm_pool_async(
395407
client: AsyncClient = ASYNC_JSON_RPC_CLIENT,
408+
enable_amm_clawback: bool = False,
396409
) -> Dict[str, Any]:
397410
issuer_wallet = Wallet.create()
398411
await fund_wallet_async(issuer_wallet)
@@ -409,6 +422,16 @@ async def create_amm_pool_async(
409422
issuer_wallet,
410423
)
411424

425+
# The below flag is required for AMMClawback tests
426+
if enable_amm_clawback:
427+
await sign_and_reliable_submission_async(
428+
AccountSet(
429+
account=issuer_wallet.classic_address,
430+
set_flag=AccountSetAsfFlag.ASF_ALLOW_TRUSTLINE_CLAWBACK,
431+
),
432+
issuer_wallet,
433+
)
434+
412435
await sign_and_reliable_submission_async(
413436
TrustSet(
414437
account=lp_wallet.classic_address,
@@ -460,6 +483,7 @@ async def create_amm_pool_async(
460483
"asset": asset,
461484
"asset2": asset2,
462485
"issuer_wallet": issuer_wallet,
486+
"lp_wallet": lp_wallet,
463487
}
464488

465489

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from tests.integration.integration_test_case import IntegrationTestCase
2+
from tests.integration.it_utils import (
3+
create_amm_pool_async,
4+
sign_and_reliable_submission_async,
5+
test_async_and_sync,
6+
)
7+
from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount
8+
from xrpl.models.currencies.issued_currency import IssuedCurrency
9+
from xrpl.models.currencies.xrp import XRP
10+
from xrpl.models.transactions import AMMDeposit
11+
from xrpl.models.transactions.amm_clawback import AMMClawback
12+
from xrpl.models.transactions.amm_deposit import AMMDepositFlag
13+
14+
15+
class TestAMMClawback(IntegrationTestCase):
16+
@test_async_and_sync(globals())
17+
async def test_positive_workflow(self, client):
18+
amm_pool = await create_amm_pool_async(client, enable_amm_clawback=True)
19+
20+
# Asset-1 is XRP, Asset-2 is an IssuedCurrency titled "USD"
21+
# The Issuer of Asset-2 is the issuer_wallet
22+
# For the purposes of this test, the issuer_wallet has set the
23+
# Allow Trust Line Clawback flag
24+
issuer_wallet = amm_pool["issuer_wallet"]
25+
holder_wallet = amm_pool["lp_wallet"]
26+
27+
# "holder" account deposits both assets into the AMM pool
28+
# Deposit assets into AMM pool
29+
amm_deposit = AMMDeposit(
30+
account=holder_wallet.address,
31+
asset=IssuedCurrency(
32+
currency="USD",
33+
issuer=issuer_wallet.address,
34+
),
35+
asset2=XRP(),
36+
amount=IssuedCurrencyAmount(
37+
currency="USD",
38+
issuer=issuer_wallet.address,
39+
value="10",
40+
),
41+
flags=AMMDepositFlag.TF_SINGLE_ASSET,
42+
)
43+
deposit_response = await sign_and_reliable_submission_async(
44+
amm_deposit, holder_wallet, client
45+
)
46+
self.assertEqual(deposit_response.result["engine_result"], "tesSUCCESS")
47+
48+
# Clawback one of the assets from the AMM pool
49+
amm_clawback = AMMClawback(
50+
account=issuer_wallet.address,
51+
holder=holder_wallet.address,
52+
asset=IssuedCurrency(
53+
currency="USD",
54+
issuer=issuer_wallet.address,
55+
),
56+
asset2=XRP(),
57+
)
58+
clawback_response = await sign_and_reliable_submission_async(
59+
amm_clawback, issuer_wallet, client
60+
)
61+
self.assertEqual(clawback_response.result["engine_result"], "tesSUCCESS")
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from unittest import TestCase
2+
3+
from xrpl.models.amounts import IssuedCurrencyAmount
4+
from xrpl.models.currencies import XRP, IssuedCurrency
5+
from xrpl.models.exceptions import XRPLModelException
6+
from xrpl.models.transactions import AMMClawback
7+
from xrpl.models.transactions.amm_clawback import AMMClawbackFlag
8+
9+
_ISSUER_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ"
10+
_ASSET2 = XRP()
11+
_INVALID_ASSET = IssuedCurrency(
12+
currency="ETH", issuer="rpGtkFRXhgVaBzC5XCR7gyE2AZN5SN3SEW"
13+
)
14+
_VALID_ASSET = IssuedCurrency(currency="ETH", issuer=_ISSUER_ACCOUNT)
15+
_HOLDER_ACCOUNT = "rNZdsTBP5tH1M6GHC6bTreHAp6ouP8iZSh"
16+
17+
18+
class TestAMMClawback(TestCase):
19+
def test_identical_issuer_holder_wallets(self):
20+
with self.assertRaises(XRPLModelException) as error:
21+
AMMClawback(
22+
account=_ISSUER_ACCOUNT,
23+
holder=_ISSUER_ACCOUNT,
24+
asset=_VALID_ASSET,
25+
asset2=_ASSET2,
26+
)
27+
self.assertEqual(
28+
error.exception.args[0],
29+
"{'AMMClawback': 'Issuer and holder wallets must be distinct.'}",
30+
)
31+
32+
def test_incorrect_asset_issuer(self):
33+
with self.assertRaises(XRPLModelException) as error:
34+
AMMClawback(
35+
account=_ISSUER_ACCOUNT,
36+
holder=_HOLDER_ACCOUNT,
37+
asset=_INVALID_ASSET,
38+
asset2=_ASSET2,
39+
)
40+
self.assertEqual(
41+
error.exception.args[0],
42+
"{'AMMClawback': 'Asset.issuer and AMMClawback transaction sender must be "
43+
+ "identical.'}",
44+
)
45+
46+
def test_incorrect_asset_amount(self):
47+
with self.assertRaises(XRPLModelException) as error:
48+
AMMClawback(
49+
account=_ISSUER_ACCOUNT,
50+
holder=_HOLDER_ACCOUNT,
51+
asset=_VALID_ASSET,
52+
asset2=_ASSET2,
53+
amount=IssuedCurrencyAmount(
54+
currency="BTC",
55+
issuer="rfpFv97Dwu89FTyUwPjtpZBbuZxTqqgTmH",
56+
value="100",
57+
),
58+
)
59+
self.assertEqual(
60+
error.exception.args[0],
61+
"{'AMMClawback': 'Amount.issuer and Amount.currency must match "
62+
+ "corresponding Asset fields.'}",
63+
)
64+
65+
def test_valid_txn(self):
66+
txn = AMMClawback(
67+
account=_ISSUER_ACCOUNT,
68+
holder=_HOLDER_ACCOUNT,
69+
asset=_VALID_ASSET,
70+
asset2=_ASSET2,
71+
flags=AMMClawbackFlag.TF_CLAW_TWO_ASSETS,
72+
)
73+
self.assertTrue(txn.is_valid())

xrpl/core/binarycodec/definitions/definitions.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3053,6 +3053,7 @@
30533053
},
30543054
"TRANSACTION_TYPES": {
30553055
"AMMBid": 39,
3056+
"AMMClawback": 31,
30563057
"AMMCreate": 35,
30573058
"AMMDelete": 40,
30583059
"AMMDeposit": 36,

xrpl/models/transactions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
AccountSetFlagInterface,
1212
)
1313
from xrpl.models.transactions.amm_bid import AMMBid, AuthAccount
14+
from xrpl.models.transactions.amm_clawback import AMMClawback
1415
from xrpl.models.transactions.amm_create import AMMCreate
1516
from xrpl.models.transactions.amm_delete import AMMDelete
1617
from xrpl.models.transactions.amm_deposit import (
@@ -119,6 +120,7 @@
119120
"AccountSetFlag",
120121
"AccountSetFlagInterface",
121122
"AMMBid",
123+
"AMMClawback",
122124
"AMMCreate",
123125
"AMMDelete",
124126
"AMMDeposit",
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""Model for AMMClawback transaction type."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass, field
6+
from enum import Enum
7+
from typing import Dict, Optional
8+
9+
from typing_extensions import Self
10+
11+
from xrpl.models.amounts import IssuedCurrencyAmount
12+
from xrpl.models.currencies import Currency
13+
from xrpl.models.currencies.issued_currency import IssuedCurrency
14+
from xrpl.models.flags import FlagInterface
15+
from xrpl.models.required import REQUIRED
16+
from xrpl.models.transactions.transaction import Transaction
17+
from xrpl.models.transactions.types import TransactionType
18+
from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init
19+
20+
21+
class AMMClawbackFlag(int, Enum):
22+
"""
23+
Claw back the specified amount of Asset, and a corresponding amount of Asset2 based
24+
on the AMM pool's asset proportion; both assets must be issued by the issuer in the
25+
Account field. If this flag isn't enabled, the issuer claws back the specified
26+
amount of Asset, while a corresponding proportion of Asset2 goes back to the Holder.
27+
"""
28+
29+
TF_CLAW_TWO_ASSETS = 0x00000001
30+
31+
32+
class AMMClawbackFlagInterface(FlagInterface):
33+
"""
34+
Claw back the specified amount of Asset, and a corresponding amount of Asset2 based
35+
on the AMM pool's asset proportion; both assets must be issued by the issuer in the
36+
Account field. If this flag isn't enabled, the issuer claws back the specified
37+
amount of Asset, while a corresponding proportion of Asset2 goes back to the Holder.
38+
"""
39+
40+
TF_CLAW_TWO_ASSETS: bool
41+
42+
43+
@require_kwargs_on_init
44+
@dataclass(frozen=True, **KW_ONLY_DATACLASS)
45+
class AMMClawback(Transaction):
46+
"""
47+
Claw back tokens from a holder who has deposited your issued tokens into an AMM
48+
pool.
49+
"""
50+
51+
holder: str = REQUIRED # type: ignore
52+
"""The account holding the asset to be clawed back."""
53+
54+
asset: IssuedCurrency = REQUIRED # type: ignore
55+
"""
56+
Specifies the asset that the issuer wants to claw back from the AMM pool. In JSON,
57+
this is an object with currency and issuer fields. The issuer field must match with
58+
Account.
59+
"""
60+
61+
asset2: Currency = REQUIRED # type: ignore
62+
"""
63+
Specifies the other asset in the AMM's pool. In JSON, this is an object with
64+
currency and issuer fields (omit issuer for XRP).
65+
"""
66+
67+
amount: Optional[IssuedCurrencyAmount] = None
68+
"""
69+
The maximum amount to claw back from the AMM account. The currency and issuer
70+
subfields should match the Asset subfields. If this field isn't specified, or the
71+
value subfield exceeds the holder's available tokens in the AMM, all of the
72+
holder's tokens are clawed back.
73+
"""
74+
75+
transaction_type: TransactionType = field(
76+
default=TransactionType.AMM_CLAWBACK,
77+
init=False,
78+
)
79+
80+
def _get_errors(self: Self) -> Dict[str, str]:
81+
return {
82+
key: value
83+
for key, value in {
84+
**super()._get_errors(),
85+
"AMMClawback": self._validate_wallet_and_amount_fields(),
86+
}.items()
87+
if value is not None
88+
}
89+
90+
def _validate_wallet_and_amount_fields(self: Self) -> Optional[str]:
91+
errors = ""
92+
if self.account == self.holder:
93+
errors += "Issuer and holder wallets must be distinct."
94+
95+
if self.account != self.asset.issuer:
96+
errors += (
97+
"Asset.issuer and AMMClawback transaction sender must be identical."
98+
)
99+
100+
if self.amount is not None and (
101+
self.amount.issuer != self.asset.issuer
102+
or self.amount.currency != self.asset.currency
103+
):
104+
errors += (
105+
"Amount.issuer and Amount.currency must match corresponding Asset "
106+
+ "fields."
107+
)
108+
109+
return errors if errors else None

xrpl/models/transactions/types/transaction_type.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class TransactionType(str, Enum):
1010
ACCOUNT_SET = "AccountSet"
1111
AMM_BID = "AMMBid"
1212
AMM_CREATE = "AMMCreate"
13+
AMM_CLAWBACK = "AMMClawback"
1314
AMM_DELETE = "AMMDelete"
1415
AMM_DEPOSIT = "AMMDeposit"
1516
AMM_VOTE = "AMMVote"

0 commit comments

Comments
 (0)